1. 程式人生 > >【專題總結】容斥原理(持續更新)

【專題總結】容斥原理(持續更新)

從動機的角度出發。在用“做減法”的思想解決計數類問題時,可能會遇到“多減去符合條件的數目”,試圖加回來的時候又會遇到“多加上不符合條件的數目”的情況,這時候也許需要用容斥原理來設計計數演算法。

從實現的角度出發。在對事件集合的“並事件”計數遇到困難時,可通過容斥原理轉化成對事件子集的“交事件”的計數。即“大並化小交”)

這裡寫圖片描述

UVAOJ 11806

大意

k 個棋子放在 n×m 的棋盤上。我們將連續的緊貼棋盤邊界的放置棋子位置稱為邊緣。問上下左右四個邊緣上都至少放置一個棋子的棋子放置方案共有多少種。

思路

根據“做減法”的思想,我們能夠起草一種貌似正確的演算法:將無任何限制的擺放棋子的方法數 s

um 減去沒有棋子放在上邊緣的方法數 sumup 減去沒有棋子放在下邊緣的方法數 sumdown 以此類推,再減去 sumleftsumright 就能得出結果 ans 。但是這樣做會導致重複減掉一些東西,我們要將這些東西( sumleftAndUp,sumrightAndDown )加回來。我們可以用容斥原理來解決這個問題。實現起來是將邊緣是否有棋子用 01 來表示,於是上下左右四個邊緣是否有棋子的情況一共需要 16 個狀態來表示。對每個狀態計算有多少個邊緣被編碼為 1 ,根據“奇加偶減”的容斥原理計算公式便可以知道該狀態的擺放方案數是應該加到答案中還是應該被從答案中減去。(每種狀態的擺放方案數可以用組合數來計算。)

程式碼

#include <bits/stdc++.h>
using namespace std;

const int mod = 1e6 + 7, maxk = 500;
int t, n, m, k, r, c, b, ans, C[maxk+5][maxk+5];

// 預處理出組合數
void makeComb(int n) {
    memset(C, 0, sizeof(C));
    for(int i = 0; i <= n; i++) {
        C[i][0] = C[i][i] = 1;
        for(int j = 1; j < i ; j++) {
            C[i][j] = (C[i-1
][j] + C[i-1][j-1]) % mod; } } } int main() { makeComb(maxk); scanf("%d", &t); for(int kase = 1; kase <= t; kase++) { scanf("%d%d%d", &n, &m, &k); ans = 0; // 列舉壓縮後的狀態 for(int s = 0; s < 16; s++) { r = n; c = m; b = 0; if(s & 1) { r--; b++; } if(s & 2) { r--; b++; } if(s & 4) { c--; b++; } if(s & 8) { c--; b++; } // 奇加偶減 if(b & 1) { ans = (ans - C[r*c][k] + mod) % mod; } else { ans = (ans + C[r*c][k]) % mod; } } printf("Case %d: %d\n", kase, ans); } return 0; }

HDU 5120

大意

求平面直角座標系上的兩個圓環的相交部分的面積。

思路

一個圓環由一個大圓和一個小圓構成。將兩個相交圓環在紙上畫出來後,兩個圓環相交部分的面積實際上等於兩個大圓的相交面積減去兩個“大圓與小圓”的相交面積,再加回兩個小圓的相交面積就是最後的結果。這是容斥原理的一種特殊情況,因此可以直接得出簡短的公式並將數值代入進行計算。

程式碼

#include <bits/stdc++.h>
using namespace std;

const double eps = 1e-10, pi = 4 * atan(1.0);

int dcmp(double x) {
    if(fabs(x) < eps) return 0;
    return x < 0 ? -1 : 1;
}

struct Point {
    double x, y;
    Point() {}
    Point(double x, double y): x(x), y(y) {}
    double distance(Point p) {
        return hypot(x - p.x, y - p.y);
    }
};

struct Circle {
    double r;
    Point o;
    Circle() {}
    Circle(double r, Point o): r(r), o(o) {}
    double area(Circle c) {
        double d = o.distance(c.o);
        if(dcmp(d - r - c.r) >= 0) {
            return 0;
        }
        if(dcmp(d - fabs(r - c.r)) <= 0) {
            double R = dcmp(r - c.r) < 0 ? r : c.r;
            return pi * R * R;
        }
        double x = (r * r + d * d - c.r * c.r) / (2 * d);
        double a1 = acos(x / r);
        double a2 = acos((d - x) / c.r);
        double s1 = a1 * r * r + a2 * c.r * c.r;
        double s2 = r * d * sin(a1);
        return s1 - s2;
    }
};

int main() {
    int t;
    scanf("%d", &t);
    double r, R, x1, y1, x2, y2;
    for(int kase = 1; kase <= t; kase++) {
        scanf("%lf%lf%lf%lf%lf%lf", &r, &R, &x1, &y1, &x2, &y2);
        Point o1(x1, y1), o2(x2, y2);
        Circle c1(r, o1), C1(R, o1);
        Circle c2(r, o2), C2(R, o2);
        double s1 = C1.area(C2);
        double s2 = C1.area(c2);
        double s3 = c1.area(c2);
        printf("Case #%d: %.6f\n", kase, s1 - 2 * s2 + s3);
    }
    return 0;
}

HDU 4135

大意

問區間 [A,B] 中與 n 互質的數有多少個。

思路

由於 A,B 的規模都比較大,因此無法列舉 [A,B] 中的所有數同時檢查是否與 n 互質。所以將目光投向“互質”。“互質”是否能為我們帶來可用的性質,使得題目的計數變為可能呢?很遺憾,這應該是比較麻煩的。“互質”這個條件太強了。
於是嘗試換一個方向思考,能否求出“不互質”的情況,再讓總情況減去“不互質”的情況呢?嘗試思考“不互質”的本質。因為“整數的結構是由質因數決定的”,因此“不互質”的本質就是“有公因數”。所以,我們將問題轉化為“問區間 [A,B] 中與 n 有公因數的數有多少個”。我們還可以進一步轉化問題,使得問題更加簡化——“問區間 [1,x] 中與 n 有公因數的數有多少個”並將這個問題的答案記作 ans[x] ,那麼原問題就等價於求 Bans[B]((A1)ans[A1])
現在當務之急是找出 ans[x] 的解法。也就是求解區間 [1,x] 中有與 n 有公因數的數有多少個。遺憾的是,這也是很難求的。但是我們可以求區間 [1,x] 中含有 n 的因子 f 的數有多少個,將其命名為 subAns[x,f] ,其答案就是 xf 。到此,可行的解法終於浮出水面。我們可以用容斥原理將所有的 subAns[x,f] 求出來——其中 f 可以是 n 的因子,也可以是 n 的因子的乘積——根據“奇加偶減”的計算方法將這些“小交集”的計數結果“拼接”成“大並集”的計數結果。這題的答案也就水到渠成地被解出來了。

程式碼

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
int t, n, m, cnt, full;
ll a, b, pro, ans;
vector <int> f;

ll solve(ll x) {
    ll res = 0;
    for(int i = 1; i <= full; i++) {
        int pro = 1;
        int cnt = 0;
        for(int j = 0; j < m; j++) {
            if((i >> j) & 1) {
                pro *= f[j];
                cnt++;
            }
        }
        if(cnt & 1) {
            res += x / pro;
        }
        else {
            res -= x / pro;
        }
    }
    return x - res;
}

int main() {
    scanf("%d", &t);
    for(int kase = 1; kase <= t; kase++) {
        scanf("%I64d%I64d%d", &a, &b, &n);
        f.clear();
        for(int i = 2; i * i <= n; i++) {
            if(n % i == 0) {
                f.push_back(i);
                while(n % i == 0) {
                    n /= i;
                }
            }
        }
        if(n != 1) {
            f.push_back(n);
        }
        m = f.size();
        full = (1 << m) - 1;
        printf("Case #%d: %I64d\n", kase, solve(b) - solve(a - 1));
    }
    return 0;
}

HDU 5768

大意

給定一個區間 [x,y]n 個數對 (pi,ai) ,問在區間中的能被 7 整除的,並且對所有 i ,被 pi 除不餘 ai 的數共有多少個。

思路

本題實際上要我們求對於一個給定的 r ,在區間 (0,r] 中,被 7 整除的數的個數 seven(r) 與“被 7 整除且存在某個 pi