C++で最長のハッピーストリングを構築する方法
問題の概要
文字列の中に「aaa」「bbb」「ccc」のように同じ文字が3つ連続する部分文字列がひとつも含まれていないとき、その文字列をハッピーな文字列と呼びます。
ここで、3つの整数 a、b、c が与えられます。このとき、次の条件をすべて満たす文字列 s を返してください。
- s はハッピーな文字列であり、かつ可能な限り長いこと。
- s には文字 'a' が最大 a 個、'b' が最大 b 個、'c' が最大 c 個含まれること。
- s は 'a'、'b'、'c' の3種類の文字のみで構成されること。
条件を満たす文字列が存在しない場合は、空文字列を返します。
たとえば a = 1、b = 1、c = 7 が入力された場合、出力は「ccaccbcc」や「ccbccacc」のような文字列になります。条件を満たす限り、答えは複数存在し得る点に注意してください。
解法のアプローチ:優先度付きキューを使った貪欲法
この問題は、残り個数が最も多い文字から優先的に配置していく貪欲法(グリーディ法)で効率よく解くことができます。同じ文字が3連続しないように制御するため、優先度付きキュー(priority_queue)を活用します。手順は以下の通りです。
- 文字種(a)、残り個数(cnt)、挿入順序(idx)を保持するデータ構造 Data を定義します。
- cnt の値を優先度とする優先度付きキュー pq を用意します。
- a が 0 以外なら Data('a', a, 0) を pq に挿入します。b、c についても同様に行います。
- idx を 1、結果文字列 ret を空文字列として初期化します。
- 以下の処理を繰り返します。
- pq の先頭要素を取り出し、temp とします。
- ret が空でなく、ret の末尾文字が temp.a と一致する場合は、pq が空ならループを抜けます。空でなければ temp を一時退避させ、次の先頭要素を取り出した後、退避させていた要素を pq に戻します。
- 今回追加する文字数 val を決定します。pq が空でなく、temp.cnt − pq 先頭要素の cnt が 2 未満であれば val = 1、そうでなければ val = min(temp.cnt, 2) とします。
- ret の末尾に temp.a を val 個連結します。
- temp.cnt から val を減算します。pq が空になったらループを抜けます。
- temp.idx に idx を設定し、temp.cnt が 0 より大きければ temp を pq に戻します。
- idx を 1 増やします。
- 最後に ret を返します。
ポイントは、直前に使った文字が再び先頭に来た場合に、あえて2番目に個数の多い文字を挟むことで「3連続」を回避している点です。また、他の文字との残数差が小さい場合は1文字ずつ追加することで、全体のバランスを保ちながら文字列を最大化できます。
C++での実装例
理解を深めるために、実際の実装を見てみましょう。
#include <bits/stdc++.h>
using namespace std;
struct Data {
char a;
int cnt;
int idx;
Data(char c, int x, int k) {
a = c;
cnt = x;
idx = k;
}
};
struct Cmp {
bool operator()(Data& a, Data& b) {
return !(a.cnt > b.cnt);
}
};
class Solution {
public:
string longestDiverseString(int a, int b, int c) {
priority_queue<Data, vector<Data>, Cmp> pq;
if (a)
pq.push(Data('a', a, 0));
if (b)
pq.push(Data('b', b, 0));
if (c)
pq.push(Data('c', c, 0));
int idx = 1;
string ret = "";
while (true) {
Data temp = pq.top();
pq.pop();
if (ret.size() && ret.back() == temp.a) {
if (pq.empty())
break;
Data x = temp;
temp = pq.top();
pq.pop();
pq.push(x);
}
int val = 0;
if (!pq.empty() && temp.cnt - pq.top().cnt < 2) {
val = 1;
}
else
val = min(temp.cnt, 2);
ret += string(val, temp.a);
temp.cnt -= val;
if (pq.empty())
break;
temp.idx = idx;
if (temp.cnt > 0)
pq.push(temp);
idx++;
}
return ret;
}
};
int main() {
Solution ob;
cout << ob.longestDiverseString(1, 1, 7);
}
実行例
入力:
1, 1, 7
出力:
ccbccacc
出力は「c」が最大7個・「a」と「b」がそれぞれ1個という制約を満たし、どこにも3連続する文字が含まれていない、有効な最長ハッピーストリングです。
計算量
各ループで少なくとも1文字が結果に追加され、対応する文字の残数が減っていくため、ループは最大 a + b + c 回程度しか回りません。優先度付きキューには常に3要素以下しか入らないため、各操作は O(log 3) = O(1) とみなせます。したがって、全体の時間計算量は O(a + b + c)、空間計算量も結果文字列を除けば O(1) となります。
-
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