個数制限付き部分和問題+条件を満たす選び方の数え上げ
先日のコンテストの問題における、最後の高速化について知りたいというリクエストがあったため、解説を書きます。より一般的な「個数制限付き部分和問題+条件を満たす選び方の数え上げ」という枠組みで説明します。
扱う問題
正整数 と、正整数からなる長さ の数列 が与えられる。
を 個以上 個以下、 を 個以上 個以下…と選ぶ方法のうち、その総和が であるような方法の個数を で割った余りを求めよ。
より形式的には、以下の条件を満たす非負整数列 の個数を で割った余りを求めよ。
- 全ての に対して
この問題を全体計算量 で解きます。
解法
普通の部分和問題と同様にDPをします。状態を以下のように定義します。
- : をそれぞれいくつ使うか決めて、それまでの総和が であるような場合の数
このDPテーブルのサイズは です。
をいくつ使うかを決める時の遷移を、貰うDPで考えます。 に遷移するのは、
- から、 を 個使うことにして遷移
- から、 を 個使うことにして遷移
- …
- から、 を 個使うことにして遷移
という 個の遷移元のうち、添字が非負であるものです。
これらを全て個別に遷移していては最悪ケースで 掛かってしまうので、以下のいずれかの手法で高速化します。
高速化手法1:累積和
先ほどの説明から、DPの遷移式は以下のようになります。
を が等しいグループに分けて、グループごとに の累積和を取っておきます。そうするとこの右辺は1つのグループ内での区間和になるので、累積和から で計算できます。これで全体計算量 を達成できます。
高速化手法2:差分更新
本質的には高速化手法1とほぼ同じですが、より実装が楽だと思います。
と の遷移元がほとんど同じで1つだけずれていることを利用すると、 を小さい方から求めていきながら以下の式で差分更新することができます。
添字が負である領域の値は全て とします。この計算は でできるので、同様に全体計算量 を達成できます。
実装例
高速化手法2を使っています。
#include <bits/stdc++.h> using namespace std; int64_t MOD = 1e9+7; void add(int64_t& a, int64_t b){ a = (a+b) % MOD; } void mul(int64_t& a, int64_t b){ a = a*b % MOD; } int main(){ int N, S; vector<int> A(N), B(N); vector<vector<int64_t>> dp(N+1, vector<int64_t>(S+1)); dp[0][0] = 1; for(int i=0; i<N; i++) for(int j=0; j<=S; j++) { dp[i+1][j] = dp[i][j]; if(j-A[i] >= 0) add(dp[i+1][j], dp[i+1][j-A[i]]); if(j-(B[i]+1)*A[i] >= 0) add(dp[i+1][j], MOD-dp[i][j-(B[i]+1)*A[i]]); } }
ACコード
冒頭にリンクを貼った問題のACコードリンクも貼っておきます。これが元のリクエストの回答になればと思います。