C++で文字列内の全単語を連結した部分文字列の開始インデックスを求める
問題概要
文字列 s と、すべて同じ長さの単語からなるリスト words が与えられたとします。このとき、s の中に存在する部分文字列のうち、words に含まれる各単語をちょうど1回ずつ、間に余計な文字を挟まずに連結したものに一致するものを探し、その開始インデックスをすべて求めます。
たとえば、入力が "barfoothefoobarman"、単語リストが ["foo", "bar"] の場合、出力は [0, 9] となります。これは、インデックス 0 から始まる部分文字列が "barfoo"、インデックス 9 から始まる部分文字列が "foobar" であり、いずれも "bar" と "foo" を1回ずつ連結した形になっているためです。
解法のアプローチ
この問題は、スライディングウィンドウとハッシュマップを組み合わせることで効率的に解けます。手順は以下の通りです。
補助関数 ok() の定義
- 文字列
s、マップwordCnt、整数n(1単語の長さ)を受け取ります。 sの先頭n文字をtempにコピーします。iをnからs.size() - 1まで順に処理します。tempの長さがnの倍数になったタイミングで判定を行います。tempがwordCntに存在しなければfalseを返します。- 存在する場合、
wordCnt[temp]が 1 ならマップから削除し、それ以外ならカウントを 1 減らします。いずれの場合もtempを空文字列に戻します。 - その後、
temp += s[i]で次の文字を追加します。
- ループ終了後、残った
tempに対しても同様の判定を行います。 - 最終的に
wordCntが空になっていればtrueを返します。
メイン関数 findSubstring() の流れ
aまたはbが空の場合は空の配列を返します。- マップ
wordCntを作成し、b内の各単語の出現回数を記録します。 - 結果格納用の配列
ansを用意します。 - ウィンドウ幅
windowを「単語数 × 1単語あたりの文字数」として計算します。 - 先頭
window文字をtempに設定し、iをwindowからa.size() - 1までスライドさせます。tempの長さがwindowの倍数で、かつok()がtrueを返せば、開始位置i - windowをansに追加します。tempの末尾にa[i]を追加し、長さがwindowを超えたら先頭の1文字を削除します。
- 最後のウィンドウについても同様に判定し、該当すれば
a.size() - windowを追加します。 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 = {"foo", "bar"};
Solution ob;
print_vector(ob.findSubstring("barfoothefoobarman", v));
}実行結果
上記のコードを実行すると、以下の出力が得られます。
[0, 9]
この結果は、文字列 "barfoothefoobarman" の中で、インデックス 0 に "barfoo"、インデックス 9 に "foobar" という、単語 "foo" と "bar" をそれぞれ1回ずつ使った連結部分文字列が存在することを示しています。
計算量について
各ウィンドウ位置ごとに ok() 関数が O(m × k)(m は単語数、k は1単語の長さ)の処理を行うため、全体の時間計算量は O(n × m × k) となります(n は文字列 s の長さ)。さらに最適化したい場合は、単語単位でウィンドウをスライドさせる手法を採用することで、O(n × k) まで計算量を抑えることが可能です。
-
C++で特定の条件を満たすグラフを構築するプログラム
2つの整数 N と K が与えられます。ここで、N 個の頂点を持つ無向グラフについて考えます。このグラフは以下の条件をすべて満たす必要があります。グラフは単純グラフであり、かつ連結である頂点には 1 から N までの番号が付けられているグラフの辺の数を M とすると、辺には 1 から M までの番号が付けられており、各辺の長さは 1 です。辺 i は頂点 U[i] と頂点 V[i] を結びますi < j を満たす頂点のペア (i, j) のうち、2 頂点間の最短距離がちょうど 2 になるものが正確に K 組存在するこのようなグラフが存在する場合はそれを構築して出力し、存在しない場合は -
-
C++で双方向リンクリストのサイズ(要素数)を求めるプログラム
本記事では、双方向リンクリスト(Doubly Linked List)が与えられたときに、そのサイズ(要素数)を求めるC++プログラムの作成方法を詳しく解説します。 双方向リンクリストとは、片方向リンクリストと比べて、各ノードが前後両方向のリンクを持つため、前方にも後方にも自由に移動できる特殊なリンクリストです。まず、双方向リンクリストを理解するうえで重要な用語を確認しておきましょう。 リンク(Link):リンクリストの各リンクには、「要素」と呼ばれるデータが格納されます。 ネクスト(Next):各リンクには、次のリンクを指す参照「Next」が含まれます。 プレヴ(Prev):各リンクに