C++で単語リストの全単語を連結した文字列の部分文字列の開始インデックスを検索する方法
問題の概要
文字列 s と、同じ長さの複数の単語を含むリスト words が与えられます。このとき、words 内の各単語をちょうど1回ずつ使用し、間に余計な文字を挟まずに連結してできる部分文字列が、s の中に出現する開始インデックスをすべて求めるのが目的です。
たとえば、入力が "wordgoodgoodgoodword"、単語リストが ["word", "good"] の場合、出力は [0, 12] になります。これは、インデックス 0 から始まる部分文字列が "wordgood"、インデックス 12 から始まる部分文字列が "goodword" であり、どちらも条件を満たすためです。
解法のアプローチ
この問題は、スライディングウィンドウとハッシュマップ(単語の出現回数カウント)を組み合わせることで効率的に解けます。全体の流れは以下の通りです。
補助関数 ok() の定義
まず、ある部分文字列が条件を満たしているかどうかを判定する補助関数 ok() を定義します。引数は文字列 s、単語の出現回数を格納するマップ wordCnt、各単語の長さ n です。
- s の先頭 n 文字を temp にコピーします。
- i が n から s のサイズ − 1 までの範囲でループします。
- temp のサイズが n の倍数であるとき:
- temp が wordCnt に存在しない場合は false を返します。
- 存在する場合は、wordCnt[temp] が 1 なら wordCnt から temp を削除し、そうでなければ wordCnt[temp] を 1 減らします。いずれの場合も temp を空文字列にリセットします。
- temp に s[i] を追加します。
- ループ終了後も同様に、残った temp が wordCnt に存在しなければ false を返し、存在すれば削除またはカウント減算を行います。
- 最後に wordCnt のサイズが 0(すべての単語を消費し切った)であれば true を返します。
メイン処理 findSubstring() の流れ
- 文字列 a または単語リスト b のサイズが 0 の場合は空の配列を返します。
- マップ wordCnt を作成し、b に含まれる各単語の出現頻度を記録します。
- 結果を格納する配列 ans を用意します。
- window = 単語数 × 各単語の文字数 としてウィンドウ幅を計算します。
- 文字列 a の先頭 window 文字分を temp にコピーします。
- i が window から a のサイズ − 1 までの範囲でループします。
- temp のサイズが window の倍数であり、ok(temp, wordCnt, b[0] のサイズ) が true を返す場合は、i − window を ans に追加します。
- temp に a[i] を追加し、temp のサイズが window を超えたら先頭の 1 文字を削除します。
- ループ後、temp のサイズが window の倍数かつ ok() が true なら、a のサイズ − window を ans に追加します。
- ans を返します。
C++での実装例
理解を深めるために、以下の実装を見てみましょう。
#include <bits/stdc++.h>
using namespace std;
void print_vector(vector<auto> v){
cout << "[";
for(int i = 0; i<v.size(); i++){
cout << v[i] << ", ";
}
cout << "]"<<endl;
}
class Solution {
public:
bool ok(string s, unordered_map <string, int> wordCnt, int n){
string temp = "";
for(int i = 0; i < n; i++){
temp += s[i];
}
for(int i = n; i < s.size(); i++){
if(temp.size() % n == 0){
if(wordCnt.find(temp) == wordCnt.end())return false;
else{
if(wordCnt[temp] == 1){
wordCnt.erase(temp);
temp = "";
} else {
wordCnt[temp]--;
temp = "";
}
}
}
temp += s[i];
}
if(wordCnt.find(temp) == wordCnt.end())return false;
else{
if(wordCnt[temp] == 1){
wordCnt.erase(temp);
temp = "";
} else {
wordCnt[temp]--;
temp = "";
}
}
return wordCnt.size() == 0;
}
vector<int>findSubstring(string a, vector<string> &b) {
if(a.size() == 0 || b.size() == 0)return {};
unordered_map <string, int> wordCnt;
for(int i = 0; i < b.size(); i++)wordCnt[b[i]]++;
vector <int> ans;
int window = b.size() * b[0].size();
string temp ="";
for(int i = 0; i < window; i++)temp += a[i];
for(int i = window; i < a.size(); i++){
if(temp.size() % window == 0 && ok(temp, wordCnt, b[0].size())){
ans.push_back(i - window);
}
temp += a[i];
if(temp.size() > window)temp.erase(0, 1);
}
if(temp .size() % window ==0 && ok(temp, wordCnt, b[0].size()))ans.push_back(a.size() - window);
return ans;
}
};
main(){
vector<string> v = {"word","good"};
Solution ob;
print_vector(ob.findSubstring("wordgoodgoodgoodword", v));
}入力
"wordgoodgoodgoodword", {"word","good"}出力
[0, 12]
まとめ
このアルゴリズムでは、ハッシュマップによる単語カウント管理とスライディングウィンドウを組み合わせることで、すべての候補位置を効率的にチェックできます。計算量はおおよそ O(N × M)(N は文字列の長さ、M は単語数 × 単語長)となり、素朴な全探索アプローチよりも大幅に高速に動作します。重複する単語や繰り返しパターンを含む入力にも正しく対応できる点がポイントです。
-
C++で一方の文字列の部分文字列がもう一方の文字列にいくつ含まれるかを調べる方法
この記事では、2つの文字列が与えられたとき、1つ目の文字列の部分文字列のうち、2つ目の文字列内に存在するものがいくつあるかを求める方法を解説します。なお、同じ部分文字列が複数回出現する場合は、その回数もカウント対象となります。具体例入力 : string1 = fogl string2 = google 出力 : 6 説明 : string2 内に存在する string1 の部分文字列は [ o, g, l, og, gl, ogl ] の6個です。 入力 : string1 = ajva string2 = java 出力 : 5 説明 : str
-
C++で文字列の部分文字列の総数を求める方法を解説
この記事では、与えられた文字列から作成できる空でない部分文字列の個数を求める方法について解説します。入力 : string = "moon" 出力 : 10 説明 : 部分文字列は m、o、o、n、mo、oo、on、moo、oon、moon の 10 個です。 入力 : string = "yellow" 出力 : 21解法のアプローチ文字列の長さを n とします。上の例からも分かるように、考えられるすべての部分文字列の個数を求めるには、長さ n、(n-1)、(n-2)、(n-3)、……2、1 の部分文字列の個数を順に加算していく必要があります。部分文