すごく NTT したくなる
問題概要
正の整数 が与えられる。
をそれぞれ
個以上
個以下とってくる方法のうち、平均が
となるものの個数を素数
で割ったあまりを、各
に対して求めよ。
制約
考えたこと
まず、平均制約を次のように言い換える。これをすることで「平均」に関する制約が「総和」に関する制約となって扱いやすくなる!
- 整数
の平均が
である
- 整数
の総和が
である
これを踏まえて考えて行く。たとえば で
のとき
- -2 を
個以下
- -1 を
個以下
- 0 を
個以下
- 1 を
個以下
- 2 を
個以下
- 3 を
個以下
ずつ選ぶ方法であって、総和が 0 になるものを数え上げる問題となる。これはさらに次のように考えることができる。
- 0 は何個選んでもいいので
通り
- マイナス部分から選んだ総和、プラス部分から選んだ総和が等しくなることが必要十分
よって、
- dp[ v ][ s ] := 1, 2, ..., v を K 個以下ずつ選んでできる総和が s になる場合の数
というのを前処理しておけば、各 x に対する答えは次のように求められる。なお、s として考えるべき値のオーダーは となる。
に対して
res += dp[ x - 1 ][ s ] × dp[ N - x ][ s ]
として、答えは res × (K + 1) - 1 となる
前処理がちゃんとできていれば、各 x に対して答えを求める作業は でできる。
よって残りは dp[ v ][ s ] を求める問題へと帰着された。
解法 (1):DP 高速化 (いもす法 or 累積和)
DP 遷移式はナイーブには次のように考えられる。
dp[ v ][ s + v × i ] += dp[ v - 1 ][ s ] (i = 0, 1, ..., K)
しかしこれでは、DP テーブルの状態量が あって、遷移が
だけあるので、全体で
の計算量となって間に合わない。
しかし上の更新式を見ると、次のことがわかる。
dp[ v - 1 ][ i ] から dp[ v ][ j ] への遷移は、i と j を v で割ったあまりが等しい部分でしか発生しない
このことから、配列 dp の第二添字を v でわったあまりごとに独立に考えてよい。そのとき、
dp[ v ][ s + v × i ] += dp[ v - 1 ][ s ] (i = 0, 1, ..., K)
という更新は「連続する区間に一律に値 dp[ v -1 ][ s ] を足している」というものとなっている。よっていもす法によって高速化できる (集める DP で書けば累積和で高速化できる)。
この高速化を行うことで前処理・クエリ処理の計算量はともに となって間に合う。
#include <bits/stdc++.h> using namespace std; // modint vector<int> MODS = { 1000000007 }; // 実行時に決まる template<int IND = 0> struct Fp { long long val; int MOD = MODS[IND]; constexpr Fp(long long v = 0) noexcept : val(v % MODS[IND]) { if (val < 0) val += MOD; } constexpr int getmod() { return MOD; } constexpr Fp operator - () const noexcept { return val ? MOD - val : 0; } constexpr Fp operator + (const Fp& r) const noexcept { return Fp(*this) += r; } constexpr Fp operator - (const Fp& r) const noexcept { return Fp(*this) -= r; } constexpr Fp operator * (const Fp& r) const noexcept { return Fp(*this) *= r; } constexpr Fp operator / (const Fp& r) const noexcept { return Fp(*this) /= r; } constexpr Fp& operator += (const Fp& r) noexcept { val += r.val; if (val >= MOD) val -= MOD; return *this; } constexpr Fp& operator -= (const Fp& r) noexcept { val -= r.val; if (val < 0) val += MOD; return *this; } constexpr Fp& operator *= (const Fp& r) noexcept { val = val * r.val % MOD; return *this; } constexpr Fp& operator /= (const Fp& r) noexcept { long long a = r.val, b = MOD, u = 1, v = 0; while (b) { long long t = a / b; a -= t * b; swap(a, b); u -= t * v; swap(u, v); } val = val * u % MOD; if (val < 0) val += MOD; return *this; } constexpr bool operator == (const Fp& r) const noexcept { return this->val == r.val; } constexpr bool operator != (const Fp& r) const noexcept { return this->val != r.val; } friend constexpr ostream& operator << (ostream &os, const Fp<IND>& x) noexcept { return os << x.val; } friend constexpr istream& operator >> (istream &is, Fp<IND>& x) noexcept { return is >> x.val; } friend constexpr Fp<IND> modpow(const Fp<IND> &a, long long n) noexcept { if (n == 0) return 1; auto t = modpow(a, n / 2); t = t * t; if (n & 1) t = t * a; return t; } }; using mint = Fp<>; void solve(int N, int K) { int S = 0; for (int i = 1; i <= N; ++i) S += i * K; vector<vector<mint>> dp(N+1, vector<mint>(S+1, 0)); dp[0][0] = 1; for (int v = 1; v <= N; ++v) { for (int s = 0; s <= S; ++s) { dp[v][s] += dp[v-1][s]; if (s+v*(K+1) <= S) dp[v][s+v*(K+1)] -= dp[v-1][s]; } for (int s = 0; s+v <= S; ++s) dp[v][s+v] += dp[v][s]; } for (int k = 1; k <= N; ++k) { mint res = 0; for (int s = 0; s <= S; ++s) res += dp[k-1][s] * dp[N-k][s]; res *= (K+1); cout << res-1 << endl; } } int main() { int N, K; cin >> N >> K >> MODS[0]; solve(N, K); }
解法 (2):DP 高速化パートを形式的冪級数で導出
上では DP 高速化部分をいもす法でやった。しかしこのような DP 遷移を見ていると、次のような NTT をどうしてもやりたくなってしまう。
配列 dp[ v ] は、これを最初の v 個まで掛け合わせたときの各係数になる。よって NTT を用いれば でできるのだが、僕のライブラリでは TLE した。ものすごく速い NTT なら間に合うのかもしれない。
しかし形式的冪級数を少し式変形すると十分扱えるものになる。
よって、一般に形式的冪級数に対して
を掛ける
で割る
という操作が高速にできるなら良さそう。
1 - xa を掛ける
形式的冪級数に を掛けるのは、DP 遷移 (配列 dp から配列 ndp への遷移) の言葉で翻訳すると次のような遷移になる
ndp[ i ] = dp[ i ] - dp[ i - a ]
この遷移は でできる (
)。また、実装上は in-place に実装できる (i を降順に更新)。
1 - xa で割る
上の遷移の逆変換をとればよいので、次のようになる
dp[ i ] = ndp[ i ] + dp[ i - a ]
この遷移も でできる。これも実装上は in-place に実装できる (i を昇順に更新)。
まとめ
以上から、DP パートはまとめて でできるので間に合う。そして上の DP は実は「いもす法による高速化」と完全に一致する。
解法 (3):前処理なしで、形式的冪級数で殴る
解法 (2) の形式的冪級数を用いた議論を突き詰めると、以下のような問題を直接殴ることもできる。
が
個以下
が
個以下
- ...
が
個以下
選ぶ方法のうち、総和が 0 になる方法を直接数え上げる。まずは の場合の解を求めてあげる。そして
を増やしていくとき、次のような更新を行えばよい。
- 配列 dp に対して、「
を
個以下利用できる」という操作を削除する更新を行う (戻す DP)
- 配列 dp に対して、「
を
個以下利用できる」という操作を挿入する更新を行う
これを形式的冪級数の言葉で整理すると、次のようになる。
として、
で割る
として、
をかける
として、
をかける
として、
で割る
ただしこのままでは v < 0 になりうるので、次のように言い換える
として、
で割る
として、
をかける
として、
をかける
として、
で割る
- 最後に
をかける
そして実装上は をかけるのを省略して、代わりに dp 値を取得すべき添字が Kv だけ右にずれると考えれば OK。以上より、各差分更新を
で実施できるので、全体の計算量は
となる。
#include <bits/stdc++.h> using namespace std; // modint vector<int> MODS = { 1000000007 }; // 実行時に決まる template<int IND = 0> struct Fp { long long val; int MOD = MODS[IND]; constexpr Fp(long long v = 0) noexcept : val(v % MODS[IND]) { if (val < 0) val += MOD; } constexpr int getmod() { return MOD; } constexpr Fp operator - () const noexcept { return val ? MOD - val : 0; } constexpr Fp operator + (const Fp& r) const noexcept { return Fp(*this) += r; } constexpr Fp operator - (const Fp& r) const noexcept { return Fp(*this) -= r; } constexpr Fp operator * (const Fp& r) const noexcept { return Fp(*this) *= r; } constexpr Fp operator / (const Fp& r) const noexcept { return Fp(*this) /= r; } constexpr Fp& operator += (const Fp& r) noexcept { val += r.val; if (val >= MOD) val -= MOD; return *this; } constexpr Fp& operator -= (const Fp& r) noexcept { val -= r.val; if (val < 0) val += MOD; return *this; } constexpr Fp& operator *= (const Fp& r) noexcept { val = val * r.val % MOD; return *this; } constexpr Fp& operator /= (const Fp& r) noexcept { long long a = r.val, b = MOD, u = 1, v = 0; while (b) { long long t = a / b; a -= t * b; swap(a, b); u -= t * v; swap(u, v); } val = val * u % MOD; if (val < 0) val += MOD; return *this; } constexpr bool operator == (const Fp& r) const noexcept { return this->val == r.val; } constexpr bool operator != (const Fp& r) const noexcept { return this->val != r.val; } friend constexpr ostream& operator << (ostream &os, const Fp<IND>& x) noexcept { return os << x.val; } friend constexpr istream& operator >> (istream &is, Fp<IND>& x) noexcept { return is >> x.val; } friend constexpr Fp<IND> modpow(const Fp<IND> &a, long long n) noexcept { if (n == 0) return 1; auto t = modpow(a, n / 2); t = t * t; if (n & 1) t = t * a; return t; } }; using mint = Fp<>; void mul(vector<mint> &dp, int a) { for (int i = (int)dp.size()-1; i >= a; --i) dp[i] -= dp[i-a]; } void div(vector<mint> &dp, int a) { for (int i = a; i < dp.size(); ++i) dp[i] += dp[i-a]; } void solve(int N, int K) { int S = 0; for (int i = 1; i <= N; ++i) S += i * K; vector<mint> dp(S+1, 0); dp[0] = 1; for (int v = 1; v < N; ++v) { mul(dp, (K+1)*v); div(dp, v); } cout << dp[0]*(K+1)-1 << endl; int D = 0; for (int x = 2; x <= N; ++x) { div(dp, (K+1)*(N-x+1)); mul(dp, N-x+1); mul(dp, (K+1)*(x-1)); div(dp, x-1); D += (x-1)*K; cout << dp[D]*(K+1)-1 << endl; } } int main() { int N, K; cin >> N >> K >> MODS[0]; solve(N, K); }