C++で単語の一般化された略語を生成するアルゴリズムを解説
ある単語が与えられたとき、その単語の一般化された略語(generalized abbreviations)をすべて生成する関数を定義することを考えます。
一般化された略語とは、単語の中の任意の連続する文字を、その文字数を表す数字に置き換えたものです。例えば、入力が "word" の場合、出力は次のようになります。
["word", "1ord", "w1rd", "wo1d", "wor1", "2rd", "w2d", "wo2", "1o1d", "1or1", "w1r1", "1o2", "2r1", "3d", "w3", "4"]
このように、元の単語そのものも含め、各位置の文字を「そのまま残す」か「数字に置き換える」かの組み合わせによって、すべての略語パターンが生成されます。
解法のアプローチ
この問題は、バックトラッキング(backtracking)を用いた再帰的な手法で解くことができます。各文字について選択肢を一つずつ試しながら、すべての組み合わせを体系的に探索していきます。
具体的な手順は以下の通りです。
- 結果を格納する配列
retを定義します。 - 関数
solve()を定義します。引数として文字列sと現在のインデックスidxを受け取ります。 idxがsのサイズ以上になった場合:sをretの末尾に追加し、処理を終了して戻ります。
y:=sの先頭からidx - 1までの部分文字列を取得します。i:=yのサイズ - 1 とします。num:= 空文字列で初期化します。i >= 0かつy[i]が '0' ~ '9' の数字である間、以下を繰り返します。num:=y[i]+num(数字を前方に連結)iを 1 減らします。
iがyのサイズ - 1 と等しくない場合(=直前の位置に数字が存在する場合):- 既存の数字に 1 を加えた新しい文字列を構築します。具体的には、
sの先頭部分 +(num+ 1 の文字列)+sの残りの部分を連結します。 s1:=(num+ 1)の文字列表現s2:=numの文字列表現s1とs2の桁数が同じ場合はsolve(ret, idx)を呼び出し、桁数が変わった場合はsolve(ret, idx + 1)を呼び出します。
- 既存の数字に 1 を加えた新しい文字列を構築します。具体的には、
- それ以外の場合(=直前の位置に数字がない場合):
prev:=s[idx](現在の文字を退避)s[idx]:= '1'(現在の位置を数字 '1' に置き換え)solve(s, idx + 1)を呼び出します。s[idx]:=prev(元の文字に復元=バックトラック)
- 最後に、現在の文字をそのまま残すケースとして
solve(s, idx + 1)を呼び出します。
メイン関数では、solve(word, 0) を呼び出した後、ret を返すだけで完成です。
実装例
理解を深めるために、以下の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:
vector<string> ret;
void solve(string s, int idx){
if (idx >= s.size()) {
ret.push_back(s);
return;
}
string y = s.substr(0, idx);
int i = y.size() - 1;
string num = "";
while (i >= 0 && y[i] <= '9' && y[i] >= '0') {
num = y[i] + num;
i--;
}
if (i != y.size() - 1) {
string ret = s.substr(0, idx - (y.size() - 1 - i)) + to_string(stoi(num) + 1) + s.substr(idx + 1);
string s1 = to_string(stoi(num) + 1);
string s2 = to_string(stoi(num));
if (s1.size() == s2.size())
solve(ret, idx);
else
solve(ret, idx + 1);
}
else {
char prev = s[idx];
s[idx] = '1';
solve(s, idx + 1);
s[idx] = prev;
}
solve(s, idx + 1);
}
vector<string> generateAbbreviations(string word){
solve(word, 0);
return ret;
}
};
main(){
Solution ob;
print_vector(ob.generateAbbreviations("hello"));
}
入力
hello
出力
[5, 4o, 3l1, 3lo, 2l2, 2l1o, 2ll1, 2llo, 1e3, 1e2o, 1e1l1, 1e1lo, 1el2, 1el1o, 1ell1, 1ello, h4, h3o, h2l1, h2lo, h1l2, h1l1o, h1ll1, h1llo, he3, he2o, he1l1, he1lo, hel2, hel1o, hell1, hello]
まとめ
このアルゴリズムでは、各文字位置ごとに「文字を残す」か「数字に置き換える」かの選択を再帰的に行うことで、すべての一般化略語を網羅的に生成しています。特に重要なポイントは、直前の位置にすでに数字が存在する場合、新たに '1' を挿入するのではなく既存の数字をインクリメントすることで、連続する文字数を正しく表現できる点です。
計算量については、各文字位置で2通りの選択肢が存在するため、全体の時間計算量は O(2n)(n は文字数)となります。このため、非常に長い単語に対しては生成される略語の数が急激に増える点に注意が必要です。
-
C++のstatic_assertとは?使い方と実例をわかりやすく解説
static_assertは、プログラムのコンパイル時に条件を検証し、条件が満たされない場合にコンパイルエラーとしてメッセージを表示するC++の機能です。実行時ではなくコンパイル時に問題を検出できるため、テンプレートメタプログラミングなどで特に有用です。プログラムの実行結果を汚すことなく、開発者に明確なエラーメッセージを伝えられる点が大きな魅力です。C++11およびC++14では、static_assertを定義する際に必ずエラーメッセージを記述する必要がありました。しかし、C++17からはメッセージを省略してstatic_assertを呼び出すことが可能になりました。また、static_as
-
C++で文字列から二分木を構築する方法
括弧と整数から構成される文字列が与えられたとき、その文字列から二分木を構築する問題を考えてみましょう。入力文字列全体が一つの二分木を表しており、整数の後に0個、1個、または2組の括弧が続く形式になっています。整数はルート(根)ノードの値を表し、各括弧のペアは同じ構造を持つ子の部分木を含んでいます。問題の例例えば、入力が 4(2(3)(1))(6(5)) のような文字列だった場合、出力は [3,2,1,4,5,6](中順走査・inorder traversal の結果)となります。解決アプローチこの問題を解くために、以下の手順に従います。再帰関数 solve() を定義します。引数として文字列