けんちょんの競プロ精進記録

競プロの精進記録や小ネタを書いていきます

AtCoder ABC 178 D - Redistribution (緑色, 400 点)

総和が一定値になるような数列の数え上げ、最近よく見る!

問題へのリンク

問題概要

整数  S が与えられる。 すべての項が 3 以上の整数で、その総和が  S であるような数列の個数を 1000000007 で割ったあまりを求めよ。

制約

  •  1 \le S \le 2000

解法 (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 ]

となる。あとはこれに従って素直に求めていける。計算量は  O(S^{2}) となる (DP 状態量が  O(S) で、各遷移が  O(S) であるため)。

#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];
}

このようにすると、計算量は  O(S) に改善できる!!

#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 ]

とてもシンプルな式になったので、累積和を求めなくても  O(S) で求められる!!!

#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):行列累乗

今回の問題では明らかにオーバースペックだけど、実は行列累乗を使うと  O(\log S) でできる。つまり  S \le 10^{12} とかでも解ける。