きたまさ法によく似たタイプの DP ダブリング高速化!
ただかなり難しい問題だと思うので、004 まで順調に解いていた方が、この問題で挫折しないように注意!!!
無理に解こうとせずに飛ばすのも一案だと思います......
問題概要
種類の数字
のみを使うことで作れる
桁の正の整数のうち、
の倍数は何個あるか、
で割ったあまりを答えてください。
制約
まずは DP
今回の問題のように、「〜という数字を使って整数を作り、それが の倍数になるようにする」というタイプの問題では、DP が有効な印象があります。
そのような DP では、整数というものを、
倍する
を足す
という つの手続きを繰り返すことで作り上げるものと考えます。たとえば "
" という整数は
- 整数
からスタートして
倍して
を足すと、
になる
倍して
を足すと、
になる
倍して
を足すと、
になる
倍して
を足すと、
になる
というように作れます。これを踏まえて今回の問題は、次の配列 dp
を考えることで (計算量を無視すれば) 解けます。
dp[i][r]
← 所定の 種類の数字のみを用いた
桁の整数であって、
で割ったあまりが
であるようなものの個数
このように、「 の倍数の個数」を聞かれているときに、「
で割ったあまり」を定義に含めた DP を考えると有効なことは多々あります。
この DP の遷移式は次のように考えられます。
dp[i + 1][(r * 10 + c[k]) % B] += dp[i][r]
この時点で計算量は になります。このままでは当然 TLE。
行列累乗することで にはなりますが、
なのでそれでも間に合いません。このようなとき、ダブリング解法を考えることが有効な場合があるようです。
ダブリング解法を適用すると、 になります。
ダブリングの考え方
今回は配列 dp[N]
(dp[N][0]
, dp[N][1]
, ..., dp[N][B-1]
という 個の要素からなる配列) を求めたいということになります。
上の「dp[i + 1][(r * 10 + c[k]) % B] += dp[i][r]
」という漸化式は、配列 dp[0]
からスタートして、dp[0]
→ dp[1]
→ dp[2]
→ ... → dp[N]
という順に計算していくものと解釈できます。しかしこのままでは 回のステップを要することになります。
しかし今回は、ダブリングすることで、 ステップの更新で
dp[N]
が求められるのです!
そこで用いられるアイデアは、繰り返し二乗法と同じものです。繰り返し二乗法とは、たとえば を計算したいときに「
を
回かける」とやるのではなく、
からスタートして
(=
)
(=
)
(=
)
(=
)
(=
)
(=
)
という順に計算することで、わずか 回のかけざんで求められます!
この方法だと一見 を計算するのに
が
の冪乗である必要があるように思われるかもしれません。しかしたとえば
を計算するときにも、
であることに着目して、
というように計算できます。念のために、一般に通用する方法を整理しましょう。 は二進法で表すと
となります。このことから
であることが導かれます。一般に を計算する場合には、
を二進法で表すことで求められます。このような方法をダブリングと呼ぶことがあります。
今回の DP の場合
以上の を計算したような方法を使えるためには、
配列 dp[i]
と配列 dp[j]
とから、配列 dp[i + j]
を計算する
ことが高速にできればよいことになります。これさえできれば、 を計算するときのようなダブリング手法によって、配列
dp[N]
を 回のステップで計算できます。
ここで、dp[i + j]
とは 桁の数を考えていることになります。これを「前半の
桁分」と「後半の
桁分」とに分けて考えてみましょう。
- 前半
桁分を
で割ったあまりが
(
dp[i][p]
通りあります) - 後半
桁分を
で割ったあまりが
(
dp[j][q]
通りあります)
であるとすると、そのような 桁の整数を
で割ったあまりは
%
となります。つまり、
dp[i + j][(p * tj + q) % B] += dp[i][p] * dp[j][q]
という遷移で表せます。ここで を
で割ったあまりを
tj
と書いています。これはまさに、「配列 dp[i]
と配列 dp[j]
とから配列 dp[i + j]
を計算する遷移式」です。そしてこの遷移は の計算量でできます。
よって全体としては、ダブリングによって 回の遷移を行うことになりますので、
の計算量で解けます。
コード (C++ と PyPy3)
C++
#include <iostream> #include <vector> using namespace std; // MOD constexpr int MOD = 1000000007; // N の対数 constexpr int LOG = 62; int main() { // 入力 long long N, B, K; cin >> N >> B >> K; vector<int> C(K); for (auto& ci : C) cin >> ci; // dp[i] と dp[j] を掛け合わせて dp[i+j] を得る処理 // tj: 10^j を B で割ったあまり auto mul = [&](const vector<long long>& dpi, const vector<long long>& dpj, long long tj) -> vector<long long> { vector<long long> res(B, 0); for (long long p = 0; p < B; ++p) { for (long long q = 0; q < B; ++q) { res[(p * tj + q) % B] += dpi[p] * dpj[q]; res[(p * tj + q) % B] %= MOD; } } return res; }; // ten[i]: 10^(2^i) を B で割ったあまり vector<long long> ten(LOG, 10); for (int i = 1; i < LOG; ++i) { ten[i] = (ten[i - 1] * ten[i - 1]) % B; } // dp[2^i][r] を doubling[i][r] で書くことにする vector<vector<long long>> doubling(LOG, vector<long long>(B, 0)); // 初期化 (doubleing[0] := dp[1]) for (int k = 0; k < K; ++k) { doubling[0][C[k] % B] += 1; } // ダブリング for (int i = 1; i < LOG; ++i) { doubling[i] = mul(doubling[i - 1], doubling[i - 1], ten[i - 1]); } // ダブリングした結果をもとに答えを求める vector<long long> res(B, 0); res[0] = 1; for (int i = 0; i < LOG; ++i) { // N を二の冪乗の積で表すときに、2^i を含むかどうか if (N & (1LL << i)) { res = mul(res, doubling[i], ten[i]); } } cout << res[0] << endl; }
Python3 (PyPy3)
# MOD MOD = 1000000007 # N の対数 LOG = 62 # 入力 N, B, K = map(int, input().split()) C = list(map(int, input().split())) # dp[i] と dp[j] を掛け合わせて dp[i+j] を得る関数 # tj: 10^j を B で割ったあまり def mul(dpi, dpj, tj): res = [0] * B for p in range(B): for q in range(B): res[(p * tj + q) % B] += dpi[p] * dpj[q] res[(p * tj + q) % B] %= MOD return res # ten[i]: 10^(2^i) を B で割ったあまり ten = [10] * LOG for i in range(1, LOG): ten[i] = (ten[i - 1] * ten[i - 1]) % B # dp[2^i][r] を doubling[i][r] と書くことにする doubling = [[0] * B for _ in range(LOG)] # 初期化 (doubling[0] = dp[1]) for k in range(K): doubling[0][C[k] % B] += 1 # ダブリング for i in range(1, LOG): doubling[i] = mul(doubling[i - 1], doubling[i - 1], ten[i - 1]) # ダブリングした結果をもとに答えを求める res = [0] * B res[0] = 1 for i in range(LOG): if N & (1 << i): res = mul(res, doubling[i], ten[i]) print(res[0])