配点: 425 点 / 実行時間制限: 2 sec / メモリ制限: 1024 MB / Difficulty: 1018 / NoviSteps: 1Q
問題概要
台のサーバーと
台のPCがある。サーバーおよび各PCはそれぞれ1つずつ文字列を保持しており、最初は全て空文字列である。
個のクエリが与えられるので、全てのクエリを与えられた順に処理したときの最終的なサーバーの文字列を求めよ。各クエリは以下のいずれかの形式である。
1 p:PCの文字列をサーバーの文字列で置き換える。
2 p s:PCの文字列の末尾に文字列
を追加する。
3 p:サーバーの文字列をPCの文字列で置き換える。
制約
は整数
- 全てのクエリについて、
は整数であり
を満たす
- 全ての2種類目のクエリについて、
は英小文字からなる長さ
以上の文字列
- 全ての2種類目のクエリに対する
の長さの総和は
以下
考察
3つのクエリ操作で配列全体に関わるようなものはないので、愚直にシミュレーションしても通りそうだが...

さすがはD問題。そんなにうまい話はない。
実は、クエリ1や3において文字列のコピーが発生しているのが原因である。
これを回避するために、2つの解法を紹介する。
永続配列を利用する
通常のデータ構造で、更新をするときに更新前の状態のデータも保存しておけるデータ構造を「永続データ構造」という。今回は、その配列版を用いることにする。
具体的には、以下のような「状態」を要素に持つ配列を作成する。
- この状態の更新元になった状態の
と、新たに追加される文字列
とのペア :
図にするとこんな感じ。木構造をイメージした方が分かりやすいかもしれない。

サーバーや各PCは、この配列(木構造)の index (頂点番号) を参照するようにする。つまり、各クエリでは以下のような処理を行う。
- クエリ1 :
とする。
- クエリ2 : 新たな状態
を作成する。その後、
の参照をその状態の index とする。
- クエリ3 :
とする。
これで、各クエリの処理は の計算量で済ますことができた。
次に、最終的なサーバーの文字列を復元していく。
これは、全クエリ処理後の が参照している状態の index から始めて、各状態の
を辿っていけばよい(木構造で、葉から親に向かってさかのぼるイメージ)。
実装においては、文字列のコピーが発生しないように注意すること(1敗)。実装例の59行目から71行目を参考にするとよい。
クエリを逆読みする
この問題ではサーバーと各PCは同一視できるので、便宜上サーバーをPC とする。
その上で、 番目のクエリ処理後のPC
の文字列の状態を
とする。これらの状態は、各クエリで以下のように変化する。
- クエリ1 :
- クエリ2 :
- クエリ3 :
最終的に求めたいのは であるが、これは
個のクエリを逆から見ていくことで、結果的に
まで到達できる。
この初期状態に到達するまでに、経由したクエリ2にて追加される文字列 を逆順にして末尾に結合していき、最後に全体を反転させれば答えを得ることができる(実装例を参考のこと)。
実装例
考察1
#include <bits/stdc++.h> using namespace std; #define all(x) (x).begin(), (x).end() #define rep(i, start, end) for (auto i = (start); (i) < (end); (i)++) // ======================================== // struct Node { int prev; string str; }; int main() { int N, Q; cin >> N >> Q; int server = 0; vector<int> pc(N); vector<Node> list; list.push_back({-1, ""}); while (Q--) { int t; cin >> t; if (t == 1) { int p; cin >> p; p--; pc[p] = server; } else if (t == 2) { int p; string s; cin >> p >> s; p--; int cur = pc[p]; pc[p] = list.size(); list.push_back({cur, s}); } else if (t == 3) { int p; cin >> p; p--; server = pc[p]; } } vector<string> ans; int current_id = server; while (current_id != -1) { if (!list[current_id].str.empty()) { ans.push_back(list[current_id].str); } current_id = list[current_id].prev; } reverse(all(ans)); for (auto &&s : ans) { cout << s; } cout << endl; return 0; }
考察2
#include <bits/stdc++.h> using namespace std; #define all(x) (x).begin(), (x).end() #define rep(i, start, end) for (auto i = (start); (i) < (end); (i)++) #define rrep(i, start, end) for (auto i = (start); (i) >= (end); (i)--) // ======================================== // struct Query { int type; int p; string str; }; int main() { int N, Q; cin >> N >> Q; vector<Query> queries(Q); rep(i, 0, Q) { int t, p; cin >> t >> p; if (t == 2) { string s; cin >> s; queries[i] = {t, p, s}; } else { queries[i] = {t, p, ""}; } } string ans = ""; int current_id = 0; rrep(q, Q - 1, 0) { if (queries[q].type == 1) { if (current_id == queries[q].p) { current_id = 0; } } else if (queries[q].type == 2) { if (current_id == queries[q].p) { reverse(all(queries[q].str)); ans += queries[q].str; } } else if (queries[q].type == 3) { if (current_id == 0) { current_id = queries[q].p; } } } reverse(all(ans)); cout << ans << endl; return 0; }
実装時間: 45分
コメント
永続データ構造についての参考記事はこのあたりが分かりやすかった。