配点: 350 点 / 実行時間制限: 2 sec / メモリ制限: 1024 MB / Difficulty: 1249
過去開催された ABC のC問題の中でトップレベルの Diff となった問題。ABC の admin である snuke さんもコンテスト終了後にこう発言するレベル。
すみません、ABC-C 難しすぎました・・・
— ꑄ꒖ꐇꌅꏂ🐾 (@snuke_) 2025年1月4日
問題概要
以上の正整数のうち、10進数表記したときに先頭の桁の数字がそれ以外のどの桁の数字よりも真に大きくなるようなものをヘビ数と呼ぶことにする。
以上 以下のヘビ数の個数を求めよ。
制約
- 入力は全て整数。
考察
問題ではある範囲内のヘビ数の個数を問われているが、これを直接求めるのは難しい。したがって、以下のような関数 を定義する。
- 以上 以下のヘビ数の個数
なお、求める答えは と書くことができる(「以上」なので は含むことに注意)。
は最大で オーダーになるので、単純な数え上げでは TLE してしまう。
そこで、 の桁数を 、 の各桁を左から順に として、 個の枠に左から順に から の数字を当てはめていくことを考える( 桁目に当てはめられる数字の上限は )。
先頭の枠を 0 としたとき
具体例で考えよう。 とする。
このとき、左から 番目の枠を とすれば、 と表せる。この下で、(先頭の を無視した)全体がヘビ数となるように に適当な数字を当てはめていく。
- : 計9個
- : 計8個
- : 計2個
- : 計1個
先頭から2桁目を数字を決定したら、3桁目にはそれ未満の数字しか入れることができない。
桁で考えると、2桁目の数字を と決定したら、3 から 桁目には の 個の数字しか入れることができない、ということだ。
では一般化してみよう。
- (先頭の を無視した)全体が 桁の数 に対して、 としたときのヘビ数の個数は、 個である。
※ の 桁に入れられる数字には、 の 個の候補がある。
1桁の数はヘビ数には当てはまらないことを踏まえると、 までの和を取ればよい。つまり、この場合のヘビ数は全部で と計算することができる。
先頭の枠を x_1 未満の数字としたとき
とすると、
がこの場合のヘビ数として当てはまる。
一般化すると、 という場合である。この場合、 の 桁に入れられる数字には の 個の候補があることになる。
...つまり、先ほどの「先頭の枠を としたとき」と同様の考え方である。したがって、この場合のヘビ数は全部で と計算することができる。
それ以外のとき
この問題の本質的な部分。ここでは、複数の例を挙げる。
例えば、 であるとき。
であるので は確定し、 の状態となる。このとき、 から に入れられる数字の候補は である。これは、今までのケースと変わらない。
では、 のときはどうだろうか。
であるので は確定し、 の状態。
ここで とすれば、以降の には のどれを入れても 以下のヘビ数になる。
しかし、 とすると、 では のいずれかしか選べない。もし を入れると元の を超えてしまうからだ()。
この制限は後ろの についても同様。 には 、 には のみ入れることができる。
今度は を考えてみる。
直前と同じように を選んで の状態であるとする。
では しか選べない... と思いきや、この場合は から まで選ぶことができ、さらに も同様に から まで候補となる。
ここまでの例を見ていくと、 に入れられる数字の候補として、
- の 個
- の 個
の2つの場合があることが分かった。これらの違いは何だろうか。
それは、 となる が存在するかどうかである。
から に入れられる数字の最低条件は であるが、もしこのような が存在するのならば、これらの数字のどれを選んでも元の を超えることはない。したがって、この時点で先述の方法で計算できる。
逆に となる桁があった場合は、この条件がさらに厳しくなり となる。ただし、この制約は にのみ適用され、 以降はまた かどうかを見る必要がある。
まとめると、 と見ていって、
- ならば、 から までの 桁それぞれに の 個の数字どれかを入れられることが確定するので、この場合のヘビ数は と計算できる。
- ならば、 には の数字どれかを入れられる。
- の数字を入れた場合、以降の から までの 桁には のいずれかを入れても元の は超えないので、この場合の ヘビ数は と計算できる。
- を入れた場合、 以降の桁に入れる数字によっては元の を超える可能性があり、この時点では計算できないのでスキップ。
最後まで を見たとき、スキップしていたもの(すべての について であるもの)もヘビ数に該当するので、答えに加算する。
実装について
以上で、ヘビ数のすべてのケースについて場合分けすることができた。あとはこれを実装していく。
基本的には上述の計算をコードに落とし込むだけだが、累乗の処理は少し気を付けたい。今回は オーダーまで扱うので、 C++ 標準のpow
関数を使うと浮動小数点の誤差が生じる可能性がある。
実装例では繰り返し二乗法による累乗の処理をしていうが、素直に
long long int_pow(long long x, long long a) { long long res = 1; for (int i = 0; i < a; i++) res *= x; return res; }
のように書いてもよい。
なお、実装例では 0-indexed になっていることに注意。
実装例
#include <bits/stdc++.h> using namespace std; constexpr int INF = 1e+9; using ll = long long; #define rep(i, start, end) for (auto i = (start); (i) < (end); (i)++) ll pow_int(ll x, ll n) { ll res = 1; while (n > 0) { if (n & 1) res *= x; x *= x; n >>= 1; } return res; } // ======================================== // ll f(ll x) { string s = to_string(x); ll n = s.size(); ll top = s[0] - '0'; if (n == 1) return 0; ll res = 0; rep(k, 2, n) rep(a, 1, 10) res += pow_int(a, k - 1); rep(a, 1, top) res += pow_int(a, n - 1); rep(i, 1, n) { ll d = s[i] - '0'; if (d >= top) { res += pow_int(top, n - i); return res; } else { res += d * pow_int(top, n - i - 1); } } res++; return res; } int main() { ll L, R; cin >> L >> R; cout << f(R) - f(L - 1) << endl; return 0; }
実装時間: 70分
コメント
今まで ABC-C の伝説といえば Ideal Sheet だったのだが、それに匹敵するインパクトだった。
場合分けの最後のケースについては「桁DP」のような考え方に近いのだが、私の桁DPについての理解が浅いため、その記述は避けている。