1. 程式人生 > 實用技巧 >CF1228E Another Filling the Grid

CF1228E Another Filling the Grid

因為要保證每行都必須填一個 \(1\),於是我們需要逐層考慮並在這層填若干個 \(1\),為了能描述當前的狀態,我們可以令 \(dp_{i, j}\) 表示當前已經填完前 \(i\) 行,有 \(j\) 列已經填有 \(1\) 的方案。轉移的話可以列舉當前有多少個位置是剛剛新增 \(1\),其餘的位置隨意:

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

但是這個轉移在 \(k = 0\) 時是失效的,因此 \(k = 0\)

的情況我們需要特殊轉移,有 \(dp_{i, j} = (m ^ j \times (m - 1) ^ {n - j} - (m - 1) ^ n) dp_{i - 1, j}\)\(j\) 個位置中至少有一個 \(1\) 的方案,容斥一下即可。

#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, P1[N], P2[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 Inc(int a, int b){
    return (a += b) >= Mod ? a - Mod : a;
}
int Dec(int a, int b){
    return (a -= b) < 0 ? a + Mod : a;
}
int Mul(int a, int b){
    return 1ll * a * b % Mod;
}
int Qpow(int a, int b){
    int ans = 1;
    while(b){
        if(b & 1) ans = Mul(ans, a);
        a = Mul(a, a), b >>= 1;
    }
    return ans;
}
int main(){
    n = read(), m = read();
    P1[0] = P2[0] = 1;
    rep(i, 1, n) P1[i] = Mul(P1[i - 1], m), P2[i] = Mul(P2[i - 1], m - 1);
    rep(i, 0, n) C[i][0] = 1;
    rep(i, 1, n) rep(j, 1, i) C[i][j] = Inc(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] = Inc(dp[i][j], Mul(dp[i - 1][k], Mul(C[n - k][j - k], Mul(P1[k], P2[n - j]))));
        dp[i][j] = Inc(dp[i][j], Mul(dp[i - 1][j], Mul(Dec(P1[j], P2[j]), P2[n - j])));
    }
    printf("%d", dp[n][n]);
    return 0;
}

上面這個 \(dp\) 能做到 \(O(n ^ 3)\),雖然能通過本題,但有沒有什麼更好的辦法呢?可以發現,本題中唯一難限制的點就是每一行每一列都必須存在至少一個 \(1\),那麼我們可以考慮一下二項式反演欽定某些行某些列有 \(1\) 其他行隨意的方案,發現這不好算,因為行和列之間會有重複部分,這樣放 \(1\) 的位置會影響每行每列的情況。那麼,我們再反過來思考,能否欽定一些行一些列沒有放 \(1\) 呢?可以發現這是非常好算的,令 \(f_{i, j}\) 表示欽定有 \(i\) 行沒有填 \(1\),有 \(j\) 列沒有填 \(1\) 其他位置隨意的方案。那麼有:

\[f_{i, j} = \dbinom{n}{i} \times \dbinom{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\) 的方案,那麼就有 \(f_{x, y} = \sum\limits_{i = x} ^ n \sum\limits_{j = y} ^ n \dbinom{i}{x} \dbinom{j}{y} g_{i, j}\),運用高維二項式反演有 \(g_{x, y} = \sum\limits_{i = x} ^ n \sum\limits_{j = y} ^ n (-1) ^ {i + j - x - y} \dbinom{i}{x} \dbinom{j}{y} f_{i, j}\) 那麼就會有:

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

從這之後往下推,我嘗試過很多方法,主要的難點在於如何將 \((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\) 其中有相同的因式,於是我們可以考慮將 \((m - 1) ^ {(n - j)i + nj}\) 拆開:

\[g_{0, 0} = \sum\limits_{i = 0} ^ n \sum\limits_{j = 0} ^ n (-1) ^ {i + j} \dbinom{n}{i} \times \dbinom{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} \dbinom{n}{i} \times \dbinom{n}{j} \times ((m - 1) ^ i \times m ^ {(n - i)}) ^ {n - j} \times (m - 1) ^ {nj} \]

然後我們驚奇地發現 \(n - j + j = n\) 配合前面的 \(\dbinom{n}{j}\) 是一個二項式定理的形式,於是可以有如下推導:

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

於是只需預處出 \(P1_i = m ^ i, P2_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, P1[N], P2[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 Inc(int a, int b){
    return (a += b) >= Mod ? a - Mod : a;
}
int Dec(int a, int b){
    return (a -= b) < 0 ? a + Mod : a;
}
int Mul(int a, int b){
    return 1ll * a * b % Mod;
}
int Qpow(int a, int b){
    int ans = 1;
    while(b){
        if(b & 1) ans = Mul(ans, a);
        a = Mul(a, a), b >>= 1;
    }
    return ans;
}
int C(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] = P1[0] = P2[0] = 1;
    rep(i, 1, n){
        fac[i] = Mul(fac[i - 1], i), inv[i] = Qpow(fac[i], Mod - 2);
        P1[i] = Mul(P1[i - 1], m), P2[i] = Mul(P2[i - 1], m - 1);
    } 
    rep(i, 0, n){
        if(i & 1) ans = Dec(ans, Mul(C(n, i), Qpow(Dec(Mul(P1[n - i], P2[i]), P2[n]), n)));
        else ans = Inc(ans, Mul(C(n, i), Qpow(Dec(Mul(P1[n - i], P2[i]), P2[n]), n)));
    }
    printf("%d", ans);
    return 0;
}

雙倍經驗 [JSOI2015]染色問題