[SMOJ2161]棋盤

這道題目是容斥原理的典型例題,主要體現了一種逆向思維。

如果我們試圖直接考慮在某種情況下,每一行和每一列都“不完全爲空”,會發現相對比較難以計算。此時可以反其道而行之,先把所有的情況算出來,然後再減去不合法的情況,會更好辦些,這裏就體現了容斥原理的思想。

使用容斥原理解決本題,大體上可以分爲兩種解決思路:

一、直接對整個棋盤進行容斥。

不考慮任何限制,每個格子就有黑和白兩種狀態,因此整個棋盤共有 2RC 種可能的狀態。

接下來,我們考慮一個 3*3 的棋盤,不妨把第一行完全爲空的狀態集合記爲 A ,則顯然還剩下 6 個格子,對它們沒有什麼限制,每個都有黑與白兩種可能,因此有 |A|=2(R1)C 。不難理解,第二行完全爲白(B )、第三行完全爲白(C )的方案數都和 A 是一樣的。類似地,第一列完全爲白(D )、第二列完全爲白(E )和第三列完全爲白(F )的方案數都爲 2R(C1) 。上面提到,合法方案數=總方案數-非法方案數,即

ans=2RC(|ABCDEF|)

注意到後面括號的部分很顯然就是直接套用容斥原理,只需解決如何計算若干集合的並集即可。

在本題中,如果一個 RC 列的棋盤中有 nm 列完全爲白,則剩餘未被限制的格子個數爲 (Rn)(Cm)=RCnCRm+nm ,這種情況的狀態數爲 2 。(自己畫一下草圖很好理解,而且細心的話可以發現其實這個小式子裏也有點容斥原理的味道)

但是直接這樣分開枚舉 AB 之類的方法肯定是有問題的,時間複雜度達到 O(2RC) 級別,只能通過 30% 的測試數據。

不難發現發現,對於都有 n 行和 m 列同時爲空的交集 S1S2 ,它們的元素個數其實是一樣的(如 |AD|=|AE| ),而 nm 列爲白的情況顯然有 CnRCmC 種,也就是對於一定的 nm ,需要加上或減去的是

2RCnCRm+nmCnRCmC

更具體一點,當 n+m 爲奇數時要減,爲偶數時要加(結合具體例子並不難理解)。這樣只需要枚舉 nm 就可以計算了,時間複雜度直接降到了 O(RC)

cdc 老師的 std:

#include <cstdio>
#include <cstring>

typedef long long LL;
#define Mod 1000000007

LL C[2050][2050], pow2[4000050];

int main()
{
    int N, M;
    scanf("%d%d", &N, &M);

    C[0][0] = 1;
    for (int i = 1; i <= N || i <= M; ++ i)
    {
        C[i][0] = 1;
        for (int j = 1; j <= i; ++ j) C[i][j] = (C[i-1][j-1] + C[i-1][j]) % Mod;
    }

    pow2[0] = 1;
    for (int i = 1; i <= N*M; ++ i) pow2[i] = pow2[i-1] * 2 % Mod;

    LL Ans = 0;
    for (int i = 0; i <= N; ++ i) 
        for (int j = 0; j <= M; ++ j)
            Ans = (Ans + (((i+j)&1)*(-2)+1) * pow2[(N-i)*(M-j)] % Mod * C[N][i] % Mod * C[M][j] % Mod) % Mod;

    printf("%d\n", int( (Ans + Mod) % Mod ));

    return 0;
}


二、只對行進行容斥。

現在假設我們先忽略對列的限制。

在一行中,要求不完全爲空,則要任意選 k 個格子塗黑(其中 1kC ),總共有

k=1CCkC
種方案,注意這個結果其實等於 2C1

對於 R 行,每一行都滿足上面的式子。因此總的方案數爲 (2C1)R ,這些是已經滿足了行的限制,而沒有考慮列的限制的情況。

接下來要做的就是再減去列的不合法情況。類似地,在每一行都不全爲白色的前提下, i 列全爲白的方案數爲 (2Ci1)R ,再乘上 i 列全爲白的可能情況 CiC 種,並跟上面同理,根據 i 的奇偶性,奇數減去,偶數加上即可。

雖然只需要枚舉 i ,看上去是 O(C) 的,但不要忘了組合數的預計算都需要 O(RC) ,因此實際上總的時間複雜度和第一種方法是一樣的。

參考代碼(寫得這麼醜當然是我的啦):

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>

using namespace std;

#define sign(x) (x&1?-1:1)

const long long MOD = 1000000007LL;

long long C[3000][3000];

/*
long long C(int a, int b) {
    if (a < b) return 0LL;
    if (a - b < b) b = a - b;
    long long ret = 1LL;
    for (int i = a; i + b > a; i--) (ret *= i) %= MOD;
    return ret;
}
*/

long long pow(int k, int p) {
    if (k == 0) return 1LL;
    else if (k == 1) return p;
    else {
        long long t = pow(k >> 1, p);
        if (k & 1) return t * t % MOD * p % MOD; else return t * t % MOD;
    }
}

long long g(int n, int m) {
    return pow(n, pow(m, 2) - 1);
}

long long f(int n, int m) {
    long long ret = g(n, m);
    for (int i = 1; i <= m; i++) {
//      printf("%lld %lld %lld\n", );
        (ret += C[m][i] * g(n, m - i) % MOD * sign(i) + MOD) %= MOD;
    }
    return ret;
}

int main(void) {
    freopen("2161.in", "r", stdin);
    freopen("2161.out", "w", stdout);
    int n, m; scanf("%d%d", &n, &m);
    C[0][0] = 1;
    for (int i = 1; i <= max(n, m); i++) {
        C[i][0] = C[i][i] = 1LL;
        for (int j = 1; j < i; j++) C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % MOD;
    }
    printf("%I64d\n", f(n, m));
    return 0;
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章