Typical DP Contest G - 辞書順

解法

まず,
next_pos[i][c] := i番目以降で,文字 c があらわれる一番前の位置
とします.
ここで,dp[i] を
dp[i] := i 番目の文字を「使った上で」,i 番目以降のユニークな文字列は何通りか
と定義します.
すると,
$dp[i] = 1 + \displaystyle \sum_{c = 'a'} dp[next\_pos[i + 1][c]]$
と表せます.
1 を加えているのは i 文字目の1文字からなる文字列の分です.

あとは K 番目の文字を復元するだけです.
これは,p = 0 (先頭の位置) からはじめて,aからzまで順番に np := next_pos[p][c] をみていって,dp[np] が K を超えるまで飛ばして,超えた段階で遷移(その文字 c を使うことが確定し,p を np + 1に更新する)していくことで復元できます.これはオートマトン(next_pos がオートマトンになっている)の上を辿っていくということです.各ノード i は dp[i] を持っています.

と,色々書いたけどわかりにくい解説だな….
自分もかなり苦手な問題なので….

ソースコード

#include <bits/stdc++.h>
using namespace std;
 
using ll = long long;

constexpr ll INF = 1e18 + 1;

int main() {
    string s;
    ll K;
    cin >> s >> K;

    int const n = s.size();
    vector<ll> dp(n + 2);
    vector<vector<int>> next_pos(n + 1, vector<int>(26, n));
    for(int i = n - 1; i >= 0; --i) {
        next_pos[i] = next_pos[i + 1];
        dp[i] = 1;
        for(int j = 0; j < 26; ++j) {
            dp[i] += dp[next_pos[i][j]];
            dp[i] = min(dp[i], INF);
        }
        next_pos[i][s[i] - 'a'] = i;
    }

    string res;
    int p = 0;
    while(K > 0) {
        int i = 0;
        for(i = 0; i < 26; ++i) {
            if(dp[next_pos[p][i]] < K) {
                K -= dp[next_pos[p][i]];
            } else {
                K--; // c 単体を使う分を減らしている
                p = next_pos[p][i] + 1;
                res += char('a' + i);
                break;
            }
        }
        if(i == 26) {
            break;
        }
    }
    if(res.empty()) {
        res = "Eel";
    }

    cout << res << endl;
}

感想

こういう問題はかなり苦手でつらい.
以下は参考問題です.
http://arc081.contest.atcoder.jp/tasks/arc081_c