総和が一定値になるような数列の数え上げ、最近よく見る!
問題概要
整数 が与えられる。 すべての項が 3 以上の整数で、その総和が
であるような数列の個数を 1000000007 で割ったあまりを求めよ。
制約
解法 (1):素直に DP
まずは素直な DP で解いてみる。
- dp[ v ] := 総和が v となるような数列の個数
このとき、次のように場合分けできる。
- 最後の項が 3 のとき、残りの総和を v - 3 にすればよいので dp[ v - 3 ] 通り
- 最後の項が 4 のとき、同様に dp[ v - 4 ] 通り
- ...
- 最後の項が v - 1 のとき、同様に dp[ 1 ] 通り
- 最後の項が v のとき、dp[ 0 ] 通り
よって、
dp[ v ] = dp[ v - 3 ] + dp[ v - 4 ] + ... + dp[ 0 ]
となる。あとはこれに従って素直に求めていける。計算量は となる (DP 状態量が
で、各遷移が
であるため)。
#include <bits/stdc++.h> using namespace std; // modint template<int MOD> struct Fp { long long val; constexpr Fp(long long v = 0) noexcept : val(v % MOD) { if (val < 0) val += MOD; } constexpr int getmod() const { 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 istream& operator >> (istream& is, Fp<MOD>& x) noexcept { is >> x.val; x.val %= MOD; if (x.val < 0) x.val += MOD; return is; } friend constexpr ostream& operator << (ostream& os, const Fp<MOD>& x) noexcept { return os << x.val; } friend constexpr Fp<MOD> modpow(const Fp<MOD>& r, long long n) noexcept { if (n == 0) return 1; auto t = modpow(r, n / 2); t = t * t; if (n & 1) t = t * r; return t; } friend constexpr Fp<MOD> modinv(const Fp<MOD>& 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); } return Fp<MOD>(u); } }; const int MOD = 1000000007; using mint = Fp<MOD>; int main() { int S; cin >> S; vector<mint> dp(S+1, 0); dp[0] = 1; for (int v = 1; v <= S; ++v) { for (int d = 0; d <= v - 3; ++d) dp[v] += dp[d]; } cout << dp[S] << endl; }
解法 (2):累積和で高速化
計算量をよくすることを考えてみよう!
dp[ v ] = dp[ v - 3 ] + dp[ v - 4 ] + ... + dp[ 0 ]
という式の右辺は「累積和」の形になっていることがわかる。具体的には、配列 dp の累積和を sdp (sdp[ i + 1 ] = sdp[ i ] + dp[ i ] で求められる配列)とおくと、
dp[ v ] = sdp[ v - 2 ]
と表せるのだ。実装上はこんな風に、dp[ v ] を求めるたびに、sdp[ v + 1 ] の値を更新していく。
for (int v = 0; v <= S; ++v) { dp[v] = sdp[v-2]; sdp[v+1] = sdp[v] + dp[v]; }
このようにすると、計算量は に改善できる!!
#include <bits/stdc++.h> using namespace std; // modint 省略 const int MOD = 1000000007; using mint = Fp<MOD>; int main() { int S; cin >> S; vector<mint> dp(S+1, 0), sdp(S+2, 0); dp[0] = 1; sdp[1] = 1; for (int v = 1; v <= S; ++v) { if (v-2 >= 0) dp[v] = sdp[v-2]; sdp[v+1] = sdp[v] + dp[v]; } cout << dp[S] << endl; }
解法 (3):式変形で高速化
解法 (2) では「累積和」を用いて殴ったけど、累積和を使わなくても式変形でなんとかできる!!!
dp[ v ] = dp[ v - 3 ] + dp[ v - 4 ] + ... + dp[ 0 ]
さて、この手の式を扱う定石は、dp[ v ] と dp[ v - 1 ] とを比べることにある。
- dp[ v ] = dp[ v - 3 ] + dp[ v - 4 ] + ... + dp[ 0 ]
- dp[ v - 1 ] = dp[ v - 4 ] + ... + dp[ 0 ]
これらを比べると、次のことがわかる (ただし v >= 3 に限る)!!
dp[ v ] = dp[ v - 3 ] + dp[ v - 1 ]
とてもシンプルな式になったので、累積和を求めなくても で求められる!!!
#include <bits/stdc++.h> using namespace std; // modint 省略 const int MOD = 1000000007; using mint = Fp<MOD>; int main() { int S; cin >> S; vector<mint> dp(S+1, 0); dp[0] = 1; for (int v = 3; v <= S; ++v) { dp[v] = dp[v-3] + dp[v-1]; } cout << dp[S] << endl; }
解法 (4):行列累乗
今回の問題では明らかにオーバースペックだけど、実は行列累乗を使うと でできる。つまり
とかでも解ける。