CF1228E Another Filling the Grid の解法

すべての行に少なくとも 1 つの 1 を配置する必要があるため、行ごとに処理し、その行にいくつかの 1 を追加することを考えます。現在の状態を表すために、dp[i][j] を「最初の i 行を埋めた時点で、1 がすでに存在する列が j 列ある場合の埋め方の総数」と定義します。遷移では、現在の行で新たに 1 を追加する位置の数を k とし、残りの位置には任意の値を入れます。

\[dp_{i, j} = \sum\limits_{k = 0} ^ j \binom{n - j + k}{k} \times m ^ {j - k} \times (m - 1) ^ {n - j} dp_{i - 1, j - k} \]

しかし、この遷移は k = 0 の場合に破綻します。なぜなら、この式は行内に 1 が少なくとも 1 つあることを保証できないからです。そのため、k = 0 の場合は特別に扱います。この場合、j 個の位置の中に少なくとも 1 つ 1 がある場合の数を包除原理で計算し、dp[i][j] = (m ^ j \times (m - 1) ^ {n - j} - (m - 1) ^ n) dp[i - 1][j] とします。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for(int i = l; i <= r; ++i)
const int N = 250 + 5;
const int Mod = 1000000000 + 7;
int n, m, powM[N], powM1[N], C[N][N], dp[N][N];

int read() {
    char c; int x = 0, f = 1;
    c = getchar();
    while (c > '9' || c < '0') { if (c == '-') f = -1; c = getchar(); }
    while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}
int add(int a, int b) { return (a += b) >= Mod ? a - Mod : a; }
int sub(int a, int b) { return (a -= b) < 0 ? a + Mod : a; }
int mul(int a, int b) { return (long long)a * b % Mod; }
int powMod(int a, int b) {
    int res = 1;
    while (b) {
        if (b & 1) res = mul(res, a);
        a = mul(a, a);
        b >>= 1;
    }
    return res;
}
int main() {
    n = read(); m = read();
    powM[0] = powM1[0] = 1;
    rep(i, 1, n) {
        powM[i] = mul(powM[i - 1], m);
        powM1[i] = mul(powM1[i - 1], m - 1);
    }
    rep(i, 0, n) C[i][0] = 1;
    rep(i, 1, n) rep(j, 1, i) C[i][j] = add(C[i - 1][j], C[i - 1][j - 1]);
    dp[0][0] = 1;
    rep(i, 1, n) rep(j, 0, n) {
        rep(k, 0, j - 1) {
            dp[i][j] = add(dp[i][j], mul(dp[i - 1][k], mul(C[n - k][j - k], mul(powM[k], powM1[n - j]))));
        }
        dp[i][j] = add(dp[i][j], mul(dp[i - 1][j], mul(sub(powM[j], powM1[j]), powM1[n - j])));
    }
    printf("%d\n", dp[n][n]);
    return 0;
}

上記の DP は O(n^3) で動作し、この問題を解くには十分ですが、より効率的な方法はないでしょうか?制約の難しい点は、すべての行と列に少なくとも 1 つの 1 が存在しなければならないことです。これを別の視点から考えます。特定の行や列に 1 を配置しないことを「強制」するのはどうでしょうか?この方が計算が容易です。f[i][j] を「正確に i 行と j 列に 1 を配置しないことが強制され、残りのセルは任意に埋める場合の総数」と定義します。すると次の式が成り立ちます。

\[f_{i, j} = \binom{n}{i} \times \binom{n}{j} \times (m - 1) ^ {n(i + j) - i \times j} \times m ^ {n ^ 2 - n(i + j) + i \times j} \]

次に g[i][j] を「正確に i 行と j 列に 1 が存在しない場合の総数」とします。すると fg の間には次の関係があります:

\[f_{x, y} = \sum\limits_{i = x} ^ n \sum\limits_{j = y} ^ n \binom{i}{x} \binom{j}{y} g_{i, j} \]

高次元の二項係数包除原理を用いると:

\[g_{x, y} = \sum\limits_{i = x} ^ n \sum\limits_{j = y} ^ n (-1) ^ {i + j - x - y} \binom{i}{x} \binom{j}{y} f_{i, j} \]

したがって、求めたい g[0][0] は次のようになります。

\[g_{0, 0} = \sum\limits_{i = 0} ^ n \sum\limits_{j = 0} ^ n (-1) ^ {i + j} \binom{n}{i} \times \binom{n}{j} \times (m - 1) ^ {n(i + j) - i \times j} \times m ^ {n ^ 2 - n(i + j) + i \times j} \]

ここで式を整理します。n^2 - n(i + j) + i \times j = (n - i)(n - j) および n(i + j) - i \times j = (n - j)i + nj という因数分解を利用します。

\[g_{0, 0} = \sum\limits_{i = 0} ^ n \sum\limits_{j = 0} ^ n (-1) ^ {i + j} \binom{n}{i} \times \binom{n}{j} \times (m - 1) ^ {(n - j)i} \times m ^ {(n - i)(n - j)} \times (m - 1) ^ {nj} \]

共通の因数でまとめます:

\[g_{0, 0} = \sum\limits_{i = 0} ^ n \sum\limits_{j = 0} ^ n (-1) ^ {i + j} \binom{n}{i} \times \binom{n}{j} \times \left( (m - 1) ^ i \times m ^ {n - i} \right) ^ {n - j} \times ((m - 1) ^ n) ^ j \]

n - jj の指数の和が n であり、\binom{n}{j} が現れることから、これは二項定理の形になっています。したがって:

\[ \begin{aligned} g_{0, 0} &= \sum\limits_{i = 0} ^ n (-1) ^ i \binom{n}{i} \sum\limits_{j = 0} ^ n (-1) ^ j \binom{n}{j} \left( (m - 1) ^ i \times m ^ {n - i} \right) ^ {n - j} \times ((m - 1) ^ n) ^ j \\ &= \sum\limits_{i = 0} ^ n (-1) ^ i \binom{n}{i} \left( (m - 1) ^ i \times m ^ {n - i} - (m - 1) ^ n \right) ^ n \end{aligned} \]

最後に powM[i] = m^i および powM1[i] = (m-1)^i を事前計算しておけば、O(n log n) で解を求めることができます。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for(int i = l; i <= r; ++i)
const int N = 250 + 5;
const int Mod = 1000000000 + 7;
int n, m, ans, powM[N], powM1[N], fac[N], inv[N];

int read() {
    char c; int x = 0, f = 1;
    c = getchar();
    while (c > '9' || c < '0') { if (c == '-') f = -1; c = getchar(); }
    while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}
int add(int a, int b) { return (a += b) >= Mod ? a - Mod : a; }
int sub(int a, int b) { return (a -= b) < 0 ? a + Mod : a; }
int mul(int a, int b) { return (long long)a * b % Mod; }
int powMod(int a, int b) {
    int res = 1;
    while (b) {
        if (b & 1) res = mul(res, a);
        a = mul(a, a);
        b >>= 1;
    }
    return res;
}
int comb(int n, int m) {
    if (m > n) return 0;
    return mul(fac[n], mul(inv[m], inv[n - m]));
}
int main() {
    n = read(); m = read();
    fac[0] = inv[0] = powM[0] = powM1[0] = 1;
    rep(i, 1, n) {
        fac[i] = mul(fac[i - 1], i);
        inv[i] = powMod(fac[i], Mod - 2);
        powM[i] = mul(powM[i - 1], m);
        powM1[i] = mul(powM1[i - 1], m - 1);
    }
    rep(i, 0, n) {
        int term = mul(comb(n, i), powMod(sub(mul(powM[n - i], powM1[i]), powM1[n]), n));
        if (i & 1) ans = sub(ans, term);
        else ans = add(ans, term);
    }
    printf("%d\n", ans);
    return 0;
}

この問題は [JSOI2015] 染色問題 と同様の技法で解くことができます。

タグ: DP 包除原理 二項係数 組合せ論 数学的最適化

5月23日 01:27 投稿