DP 高速化に累積和を使う問題!
問題概要
人の子ども に、 個の飴を分けることにした。
ただし、子ども に分ける飴は、 個以上 個以下のする必要がある。
各子どもへの飴の分け方の総数を、1000000007 で割ったあまりを求めよ。
制約
解法:まずは DP を組む
DP に慣れて来ると、次のような配列 dp
を定義することで解けそうだと思えると思われる。
dp[i][j]
← 子ども に対して、 個の飴を分ける方法の総数
そうすると、DP の遷移は次のように作れる。なお、DP 遷移を作るときは「配る DP」と「集める DP」のどちらでもよいのだが、DP 高速化を見据える場合は、集める DP にするといいケースが多いように思う。
集める DP で考えると、次のようになる。
dp[i + 1][j] = dp[i][j] + dp[i][j - 1] + ... + dp[i][j - min(j, a[i])]
ここで、右辺において
dp[i][j]
は「 人目の子どもに飴を 0 個あげる」場合dp[i][j - 1]
は「 人目の子どもに飴を 1 個あげる」場合- ...
dp[i][j - min(j, a[i])]
は「 人目の子どもに飴を 個あげる」場合
を表している。
この漸化式に従って DP を実装すると、dp[i + 1][j]
を計算するのに要する計算量が であることから、全体の計算量は となる。
このままでは TLE となるので、高速化する必要がある。
累積和で DP 高速化
DP の遷移式
dp[i + 1][j] = dp[i][j] + dp[i][j - 1] + ... + dp[i][j - min(j, a[i])]
の右辺をよく見ると、累積和をつかって高速化できそうな形になっていることがわかる。
具体的には、
dp[i][0]
, dp[i][1]
, dp[i][2]
, ...
という数列を考える。このとき、上式の右辺は、この数列の連続する要素の和となっている。そこで、この数列の累積和を考えよう。累積和を sum_dp
とおくと、
sum_dp[0] = 0
sum_dp[1] = dp[i][0]
sum_dp[2] = dp[i][0] + dp[i][1]
sum_dp[3] = dp[i][0] + dp[i][1] + dp[i][2]
- ...
sum_dp[K+1] = dp[i][0] + dp[i][1] + dp[i][2] + ... + dp[i][K]
というようになる。このとき、DP の遷移式は次式のように表せる!!
dp[i + 1][j] = sum_dp[j + 1] - sum_dp[j - min(j, a[i])]
こうすると、計算量が に改善される。より詳細には
- 各 に対して
dp[i]
の累積和sum_dp
を作る部分:計算量sum_dp
を用いて、各 に対してdp[i+1][j]
を求める部分:計算量
となるので、合計で となる。
解答例
ACL の modint を使った。
#include <bits/stdc++.h> using namespace std; #include "atcoder/modint.hpp" using mint = atcoder::modint1000000007; int main() { int N, K; cin >> N >> K; vector<int> a(N); for (int i = 0; i < N; ++i) cin >> a[i]; vector<vector<mint>> dp(N+1, vector<mint>(K+1, 0)); dp[0][0] = 1; for (int i = 0; i < N; ++i) { // dp[i] の累積和を求める vector<mint> sum_dp(K+2, 0); for (int j = 0; j <= K; ++j) { sum_dp[j+1] = sum_dp[j] + dp[i][j]; } // dp[i+1] を求める for (int j = 0; j <= K; ++j) { dp[i+1][j] = sum_dp[j+1] - sum_dp[j-min(j, a[i])]; } } cout << dp[N][K].val() << endl; }
別解:形式的冪級数を使う
「 個のものを分配する」という問題では、形式的冪級数が効くことが多いみたい。
つまり、 個ずつ配る方法の個数を として、多項式
を求めるのだ。これが求められれば (多項式 の 次の係数をこのように表記する) が答えとなる。
そして、子ども に 個以上 個以下の飴を配るという行為は、多項式を次のように更新することを表す。
←
つまり、この問題の答えは次の式で表せる。
これは FPS を用いて、 の計算量で計算できる。
さらに
もっと考察を進めよう。次のように式変形できる。
よって、求める値は次のように表せる。
ここで、 を掛けるという行為は、次のように分解できる。
をかける
これは実は、累積和をとる操作に相当する。実は、上述の DP において、dp[i]
の累積和 sum_dp
を求める操作に対応している。
をかける
これは、実は上述の DP において、
dp[i + 1][j] = sum_dp[j + 1] - sum_dp[j - min(j, a[i])]
という計算をしていたことに対応している。
以上から、形式的冪級数を用いても、上述の累積和を用いた DP と同じ解法が導出できることがわかった。