GCJ Round3 Problem B. Forest University
問題
Dashboard - Round 3 2016 - Google Code Jam
長さ N の文字列が与えられる。また, 「i 番目の文字は j 番目の文字が現れた後しか現れてはいけない」という条件が与えられる。
このような条件を満たす文字列はたくさん考えられるが, この中で「できた文字列が query[i] を部分文字列として持つ」確率はいくらか求めよ。
解法
許容誤差が大きいので, 乱択でいけそうな気がします。何も考えずに s の random_shuffle だけを考える方針は, 現れる文字列が 0 -> 1 -> 2 -> ... -> n しかないような場合に死ぬので, これは良くないです。そこで, 「乱択はするけど, 考える文字列は確実にあり得るものだけにしたい」という要請があります。これがこの問題の主題です。
ここで問題で与えられる条件を考えてみると, 各 index は一つだけ「それより前に来なければならない index」を指定されています。これは, 木構造で parent を指定する構造と同じなので, 木DP のようなことが使えそうです。
ある頂点 v の後に ch0, ch1, ... が来ることがわかっているとすると, (ch0 以降を使う文字列), (ch1 以降を使う文字列) というのが生成できます。そして, これらの文字列を(chi 以降を使う文字列) としての順番は崩さずにシャッフルします。すると, いい感じにあり得る文字列で乱択をすることが可能です。
下のコードは LOOP が大きすぎたのと自分の PC がややポンコツなことが合わさって結構時間かかりました。
const int LOOP = 50000; string dfs(int v, const vector<vi>& G, const string& s) { string ret; for (int ch : G[v]) { string t = dfs(ch, G, s); vector<int> r(ret.size()+t.size()); fill(r.begin(), r.begin()+t.size(), 1); random_shuffle(r.begin(), r.end()); string next; int tcnt = 0, retcnt = 0; for (int i = 0; i < ret.size()+t.size(); i++) { if (r[i]) next += t[tcnt++]; else next += ret[retcnt++]; } ret = next; } ret = s[v]+ret; return ret; } void solve() { int N; cin >> N; vector<vi> G(N+1); for (int i = 1; i <= N; i++) { int p; cin >> p; G[p].push_back(i); } string s; cin >> s; s = "$"+s; int M; cin >> M; vector<string> query(M); for (int i = 0; i < M; i++) cin >> query[i]; vector<double> ans(M); for (int i = 0; i < LOOP; i++) { string t = dfs(0, G, s); for (int j = 0; j < M; j++) { if (t.find(query[j]) != string::npos) ans[j] += 1; } } for (int i = 0; i < M; i++) { printf("%.3lf", ans[i]/LOOP); if (i < M-1) printf(" "); } printf("\n"); } int main() { int T; cin >> T; for (int t = 1; t <= T; t++) { cout << "Case #" << t << ": " ; solve(); } return 0; }
木DP でモンテカルロする空間を絞れるの結構面白いと思いました。