C++で辞書順最小の等価文字列を求めるアルゴリズム(Union-Find活用)
問題概要
同じ長さの2つの文字列 A と B が与えられ、各位置 i において A[i] と B[i] は「等価な文字」であると定義します。例えば、A = "abc"、B = "cde" の場合、「a」と「c」、「b」と「d」、「c」と「e」がそれぞれ等価であることを意味します。
この等価関係は、一般の同値関係と同じ性質を持ちます。
- 反射律: 'a' = 'a'(任意の文字は自分自身と等価)
- 対称律: 'a' = 'b' ならば 'b' = 'a'
- 推移律: 'a' = 'b' かつ 'b' = 'c' ならば 'a' = 'c'
例えば、上記の A と B の等価性情報をもとにすると、S = "eed"、"acd"、"aab" はすべて互いに等価な文字列であり、その中で辞書順に最も小さいのは "aab" です。本記事の課題は、A と B から得られる等価性情報を利用して、文字列 S の辞書順最小の等価文字列を求めることです。
具体例
入力が A = "parker"、B = "morris"、S = "parser" の場合、出力は "makkek" になります。理由は次のとおりです。A と B の等価性情報に基づいて文字をグループ化すると、[m, p]、[a, o]、[k, r, s]、[e, i] の4つのグループに分かれます。各グループ内の文字は互いに等価であり、置き換えにはグループ内で辞書順に最小の文字を使用します。したがって、"parser" の各文字を対応するグループの最小文字に置き換えると "makkek" が得られます。
解法の考え方: Union-Find(素集合データ構造)
この問題は、Union-Find(Disjoint Set Union)と呼ばれるデータ構造を用いると効率的に解けます。手順は以下のとおりです。
- サイズ26の配列
parentを作成し、すべて -1 で初期化します(-1 は「未登録」を表します)。 getParent(x)メソッドを定義します。文字 x の属するグループの代表を返すメソッドです。parent[x - 'a']が -1 なら x 自身が代表なのでx - 'a'をそのまま返します。それ以外の場合は、再帰的に親を辿りながら経路圧縮を行い、結果をparent[x - 'a']にキャッシュして返します。union(a, b)メソッドを定義します。まずparentA = getParent(a)、parentB = getParent(b)を求めます。両者が等しければ既に同じグループなので何もしません。異なる場合は、値が小さい方(辞書順で先の文字)を親に設定します。これにより、各グループの代表は常にグループ内で最小の文字になります。- メイン処理では、まず
parentをサイズ26・全要素 -1 の配列として初期化します。 - i を 0 から A.size() - 1 まで動かしながら
union(A[i], B[i])を実行し、すべての等価ペアを登録します。 - 空の文字列
retを用意し、S の各文字に対してgetParent(S[i]) + 'a'を計算してretに連結します。 retを返します。
ポイントは、統合時に必ず辞書順で小さい方を親に選ぶことです。これにより、最終的に各文字の親を参照すれば、そのグループ内で最小の文字が一意に求まります。
C++による実装例
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
vector<int> parent;
int getParent(char x){
if(parent[x - 'a'] == -1) return x - 'a';
return parent[x - 'a'] = getParent('a' + parent[x - 'a']);
}
void unionn(char a, char b){
int parentA = getParent(a);
int parentB = getParent(b);
if(parentA == parentB) return;
if(parentA < parentB){
parent[parentB] = parentA;
}else{
parent[parentA] = parentB;
}
}
string smallestEquivalentString(string A, string B, string S) {
parent = vector<int>(26, -1);
for(int i = 0; i < A.size(); i++){
unionn(A[i], B[i]);
}
string ret = "";
for(int i = 0; i < S.size(); i++){
ret += getParent(S[i]) + 'a';
}
return ret;
}
};
int main(){
Solution ob;
cout << ob.smallestEquivalentString("parker","morris","parser");
}
入力
"parker" "morris" "parser"
出力
makkek
計算量について
経路圧縮を用いた Union-Find の各操作はほぼ定数時間で実行できるため、A・B の長さを N、S の長さを M とすると、全体の計算量は O(N + M) に近い形で抑えられます。必要なメモリも固定サイズ26の整数配列のみであり、非常に効率的な解法です。
-
C++で文字列をトークン化する方法:stringstreamとgetline()による分割テクニック
この記事では、C++における文字列のトークン化(分割)の方法について解説します。C言語では、文字配列に対してstrtok()関数を使用することで文字列を分割できましたが、C++ではstd::stringクラスを扱うため、少し異なるアプローチが必要です。C++の機能を活用して文字列を分割するには、まずstd::stringをstringstream(文字列ストリーム)に変換します。その後、getline()関数を使うことで、指定した区切り文字(デリミタ)ごとに文字列を切り出すことができます。getline()関数は、以下の3つの引数を受け取ります。入力元となる文字列ストリーム出力結果を格納する文
-
C++で文字列をトークン化(分割)する2つの方法を解説
文字列のトークン化(分割)とは、1つの文字列を区切り文字(スペースやカンマなど)を基準に、複数の部分文字列へ分割する処理のことです。C++では、標準ライブラリだけでもいくつかの方法で実現できます。本記事では、代表的な2つの方法をサンプルコード付きで紹介します。方法1:stringstreamを使って空白で分割する1つ目の方法は、stringstreamを使ってスペースで区切られた単語を順に読み取る方法です。この方法はやや制限がありますが、適切なチェックを加えれば十分に目的を果たすことができます。サンプルコード#include <vector> #include <string