Typical DP Contest G - 辞書順
問題
文字列sが与えられる。
sの(必ずしも連続しない)部分文字列でK番目のものを求めよ。
部分文字列は、文字列が同じならどこから切り出したかは区別しない。
K番目が存在しない場合はEelと出力せよ。
制約条件
sは英小文字からなる
sの長さ≦10^6
方針
まずsのユニークな部分文字列の個数を数える。
dp[i]をi文字目を使い、i文字目以降のユニークな部分文字列の個数とする。
dp[i]は次の'a'〜次の'z'の位置をpos1〜pos26とすれば、
dp[i] = 1+Σ{dp[pos_j] | 1 ≦ j ≦ 26}と書ける。
(ユニークな文字列の個数を数えたいので、aが二つあったら、最初のaを飛ばして二個目以降のaを使うのを禁止すればよい。すなわち、同じ文字は最初のもののみを使えるようにすれば、重複なしで数えられる)
したがって、iを後ろからまわして、現在のところのΣa〜zの個数 = totをもっておき、
dp[i]を求めたら、totから前回の同じ文字のdp[j]を引いて、かわりにdp[i]を足して……としていけばよい。
(日本語が怪しいのでソース参照)
で、個数が数えられたら、先頭から使う文字を1文字ずつ決めていけばよい。
dpテーブルでn番目を復元する典型通り、個数がKを超えたところでその文字を使う、としていく。
ソースコード
string s; int n, next[1000010][26]; ll K, dp[1000010], prev[26]; int main(){ cin >> s >> K; n = s.size(); memset(next, -1, sizeof(next)); for(int i = n - 1; i >= 0; i--) rep(j, 26){ next[i][j] = s[i] - 'a' == j ? i : next[i+1][j]; } ll tot = 1; dp[n] = 1; for(int i = n - 1; i >= 0; i--){ int c = s[i] - 'a'; dp[i] = tot; tot += dp[i] - min(prev[c], inf); tot = min(tot, 2 * inf); prev[c] = dp[i]; } K = min(K, tot); string ans; int pos = -1; while(K > 0){ int i = 0; for(; i < 26; i++){ int np = next[pos+1][i]; if(np < 0) continue; if(dp[np] >= K){ ans += 'a' + i; pos = np; K--; break; } K -= dp[np]; } if(i >= 26){ break; } } if(ans == "") ans = "Eel"; cout << ans << endl; return 0; }