AOJ 2884 Tanka Number

解法

二分探索 + 桁DP ……ではなく貪欲に解く。
基本的に上の桁から順番に数字を確定させていって、
n 番目を超えないギリギリを選択する、というよくあるやり方。
当然だが各桁は小さい数字から順番に選択していくようにする。

まず、ちょうど d 桁の短歌数は 81 * (2^(d-1) - 1) 個あるので、答えが何桁かは簡単に決定できる。
(2つの数字の決め方で 9 * 9 通り、2桁目以降の数字の割り当て方で 2^(d-1) 通りで、すべてが1桁目と同じ数になる 1通りを省くと先の式になる)
あとは数字を決めるだけ。

上から1桁目は簡単で、1桁目を決めたら、そのような短歌数は 9 * (2^(d-1) - 1) 個あるわけだから、これを超えないギリギリが1桁目で確定する。

2桁目以降は、小さい数字から(同じように)順番に見ていくのだが、

  • これまでにすでに2つの異なる数字を選んでいるか
  • まだ1つの数字しか選んでいないなら、今の桁に割り当てようとしている数字が1桁目と同じかどうか

に注意して短歌数の個数を数える必要がある。

計算量は O(logN)

ソースコード

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

using ll = long long;

int main() {
    ll n;
    while(cin >> n, n) {
        ll keta = 2, cnt = 0;
        while(cnt + 9 * 9 * ((1LL << (keta - 1)) - 1) < n) {
            cnt += 9 * 9 * ((1LL << (keta - 1)) - 1);
            keta += 1;
        }
        cnt = n - cnt;

        string ans;
        function<void(int, int, int)> solve = [&] (int d, int a, int b) {
            if(d == keta) return;
            if(a == -1) {
                for(a = 1; a <= 9; ++a) {
                    if(cnt > 9 * ((1LL << (keta - 1)) - 1)) {
                        cnt -= 9 * ((1LL << (keta - 1)) - 1);
                        continue;
                    }
                    break;
                }
                ans += '0' + a;
                solve(d + 1, a, b);
            } else if(b == -1) {
                for(b = 0; b <= 9; ++b) {
                    if(a != b) {
                        if(cnt > (1LL << (keta - d - 1))) {
                            cnt -= 1LL << (keta - d - 1);
                            continue;
                        }
                        break;
                    } else {
                        if(cnt > 9 * ((1LL << (keta - d - 1)) - 1)) {
                            cnt -= 9 * ((1LL << (keta - d - 1)) - 1);
                            continue;
                        }
                        break;
                    }
                }
                ans += '0' + b;
                if(a == b) solve(d + 1, a, -1);
                else       solve(d + 1, a, b);
            } else {
                const int ma = max(a, b), mi = min(a, b);
                if(cnt > (1LL << (keta - d - 1))) {
                    cnt -= 1LL << (keta - d - 1);
                    ans += '0' + ma;
                } else {
                    ans += '0' + mi;
                }
                solve(d + 1, a, b);
            }
        };
        solve(0, -1, -1);

        cout << ans << endl;
    }
}

感想

模擬国内本番では二分探索 + 桁DP (数が大きいので文字列同士の足し算をする) で解いてしまったので、まともなほうで解き直した。