1. 程式人生 > >acm組合數學及其應用--容斥原理與鴿巢原理(一)

acm組合數學及其應用--容斥原理與鴿巢原理(一)

acm組合數學及其應用–容斥原理與鴿巢原理

追逐青春的夢想,懷著自信的心,永不放棄

1、容斥原理

定理一(德摩根定理)

若A和B是全集U的子集,
1、則A和B並集的補集等於A的補集與B的補集的交集
2、則A和B交集的補集等於A的補集與B的補集的並集

定理二

具有性質A或B的元素個數等於具有性質A的元素個數與具有性質B的元素個數的和,減去同時具有性質A和性質B的元素個數。

以此類推……

例1、給你一個數N和M個數,問你從1~N中,有多少個數字能被這M個數整除(只要能被其中一個整除即可)。

輸入:
一個數T,代表資料的組數,接下來一行是兩個正整數N,M,如上敘述,接下來一行,有M個數,代表了可以拿去除的M個數。N、M(1<=N<1e18,1<=M<1000)。
輸出:一個數字,代表結果。

樣例輸入:

1
600 3
2 3 5

樣例輸出:

440

分析:直接使用容斥原理的結論就行了。

2、容斥原理的應用

a、錯排問題
例2、若一個排列使得所有的元素都不在原來的位置上,則稱這個排列為錯排。任給一個n,求出1、2、……、n的錯排個數D,共有多少個。

輸入:一個整數T,代表資料組數。接下來一行一個數字n,代表排列的長度。
輸出:一個數字,代表錯排個數。

根據分析,如果直接根據容斥原理來做會產生階乘的問題,而階乘問題往往可以轉化為通項公式的形式,令Dn為n排列的錯排數,那麼可以得到如下公式:
Dn = (n-1)(Dn-1+Dn-2

),D1 = 0,D2 = 1
根據這個錯排公式就可以根據遞推的方法進行解決了。

例3、在新年晚會上,組織者舉行了一個別開生面,獎品豐厚的抽獎活動,這個活動的具體要求如下:首先,所有參加晚會的人員都將一張鞋油自己名字的字條放入抽獎箱中;然後等待所有字條放入完畢,沒人從相中娶一個字條;最後,如果取得的字條上寫的就是自己的名字,那麼“恭喜你,中獎了!”。當時的氣氛非常熱烈,不過,正如所有試圖設計的喜劇往往以悲劇結尾,這次抽獎活動最後竟然沒有一個人中獎,怎麼會這樣呢?不過,先不要悲傷,現在問題來了,你能計算一下仿生這種情況的概率嗎?

輸入:輸入資料的第一行是一個整數C,表示測試例項的個數,然後是C行資料,每行包括一個整數n(1 < n<=20),表示參加抽獎的人數。
輸出:對於每個測試例項,請輸出發生這種情況的百分比,每個例項的輸出佔一行,結果保留兩位小數(四捨五入),具體格式請參照輸出樣例。
輸入樣例:

3
2
4
6

輸出樣例:

50.00%
37.50%
36.81%

分析:直接根據上一題的結論算出每個n的錯排的數量,然後除以整個的情況就可以了。

b、布棋問題
例4、在m*n的昂各種任意指定C個格子構成一個棋盤,在任一個這樣的棋盤上放置棋子,要求任意兩個棋子不得位於同一行或者同一列上。在棋盤上放置k個棋子並滿足上述要求的一種方法稱為一個方案。如果這是個n*n的棋盤,那麼一共有多少種方案?

結論:Rk(C) = Rk-1(C(x))+Rk(C(e));R0(C) = 1。其中Rk-1(C(x))表示對某格放置了一個棋子後,剩下的k-1棋子布到C(x)棋盤上的方案數,Rk(C(e))表示對某格不布棋子,則k個棋子布到C(e)上的方案數。遞迴求解答案即可(R1,R2,……,Rp

c、有禁區的排列

對排列增加某些限制條件

結論:有禁區的排列數為:
n! - R1(n-1)! + R2(n-2)! - ……+- Rn
其中Ri是有Ri個棋子佈置到禁區的方案數。由於用到了階乘,所以只適用於n比較小的情況。

例5、在國際象棋的規定中,“象”只能從它所在的位置做對角線,如果兩隻象處於同一斜線上,它們將攻擊對方。現在,給出2個數字n和k,在一個n*n的棋盤上放k個互不攻擊的象有多少種方法。

輸入:含有多組測試例項,每個測試樣例佔一行,每行包括2個整數n(1<=n<=8)和k(9<=k<=n2)。多組測試例項以 0 0 為結束標誌,你不必處理這組資料。
輸出:對於每個測試例項,輸出一共有多少種方法。你可以確定最後的結果小於1015。
樣例輸入:

8 6
4 4
0 0

樣例輸出:

55998888
260

分析:我們知道國際象棋棋盤是黑白格相間排列,這是,不難發現放在白格中的象不會攻擊放在黑格中的象。所以,如果我們在白格中放i個象的方法有Ri(C(white))種,在黑格中放k-i個象有Rk-i(C(black))種,則放k個象在n*n的棋盤中,根據乘法原理,就有Ri(C(white))*Rk-i(C(black))種。但是怎麼才能得到C(white)棋盤呢?這裡有一個巧妙的方法:把黑格從棋盤中抽出,小方格和棋盤都順時針旋轉45度,再壓縮。
設dp[i][j]表示前i行放j個棋子的方法數,c[i]表示棋盤第i行的格子數,那麼第i行如果不放j棋子有dp[i-1][j]種方法,如果在第i行放j棋子有dp[i-1][j-1]*(c[i]-(j-1))種。這樣得到:

dp[i][j]=dp[i1][j]+dp[i1][j1](c[i](j1))
這個式子就是問題的解。

示例程式碼:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<string>
#include<map>
#include<stack>
#include<queue>
#include<set>
#include<sstream>
#include<ctime>
#include<algorithm>
#include<bits/stdc++.h>
#include<sstream>
#include<list>
#include<cmath>
#include<deque>
#include<cstdlib>
using namespace std;
const int maxn = 10086;
const int N = 8;
#define inf 0x3f3f3f3f
#define eps 1e-8
#define pi acos(-1.0)
typedef long long LL;
int b[N+1],w[N+1],R_b[N+1][65],R_w[N+1][65];
void init(int n){
    memset(b,0,sizeof b);
    memset(w,0,sizeof w);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++){
            if((i+j)&1)w[(i+j)>>1]++;
            else b[(i+j)>>1]++;
        }
}
void dfsans(int n,int k,int C[N+1],int R[N+1][65]){
    for(int i=0;i<=n;i++)R[i][0]=1;
    for(int i=0;i<=k;i++)R[0][k]=0;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=C[i];j++){
            R[i][j] = R[i-1][j]+R[i-1][j-1]*(C[i]-j+1);
        }
    } 
}
void anhangduru(){
     string line;
     while(getline(cin,line)){
        int sum=0;
        int x;
        stringstream ss(line);
        while(ss>>x){sum+=x;}//按空格讀入
     }
}//按行讀入
//加上符號過載

int main()
{
//  ios::sync_with_stdio(0);//輸入輸出掛
    int n,k,ans;
    while(~scanf("%d%d",&n,&k)){
        if(n==0&&k==0){
            break;
        }
        init(n);
        sort(b+1,b+n+1);
        sort(w+1,w+n);
        dfsans(n,k,b,R_b);
        dfsans(n-1,k,w,R_w);
        ans = 0;
        for(int i=0;i<=k;i++){
            ans+=R_b[n][i]*R_w[n-1][k-i];
        }
        printf("%d\n",ans);
    }
    return 0;
}

3、莫比烏斯反演定理

例6、給一個正整數n,其中n<=107,求使得gcd(x,y)為質數的(x,y)的個數,1<=x,y<=n)。

分析:gcd(x,y)為質數,所以只要列舉1~n的質數就行了。
程式碼:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<string>
#include<map>
#include<stack>
#include<queue>
#include<set>
#include<sstream>
#include<ctime>
#include<algorithm>
#include<bits/stdc++.h>
#include<sstream>
#include<list>
#include<cmath>
#include<deque>
#include<cstdlib>
using namespace std;
typedef long long LL;  
const int N = 10000010;  

bitset<N> prime;  
LL phi[N];  
LL f[N];  
int p[N];  
int k;  

void isprime()  
{  
    k = 0;  
    prime.set();  
    for(int i=2; i<N; i++)  
    {  
        if(prime[i])  
        {  
            p[k++] = i;  
            for(int j=i+i; j<N; j+=i)  
                prime[j] = false;  
        }  
    }  
}  

void Init()  
{  
    for(int i=1; i<N; i++)  phi[i] = i;  
    for(int i=2; i<N; i+=2) phi[i] >>= 1;  
    for(int i=3; i<N; i+=2)  
    {  
        if(phi[i] == i)  
        {  
            for(int j=i; j<N; j+=i)  
                phi[j] = phi[j] - phi[j] / i;  
        }  
    }  
    f[1] = 0;  
    for(int i=2;i<N;i++)  
        f[i] = f[i-1] + (phi[i]<<1);  
}  

LL Solve(int n)  
{  
    LL ans = 0;  
    for(int i=0; i<k&&p[i]<=n; i++)  
        ans += 1 + f[n/p[i]];  
    return ans;  
}  

int main()  
{  
    Init();  
    isprime();  
    int n;  
    scanf("%d",&n);  
    printf("%I64d\n",Solve(n));  
    return 0;  
}  

如果,x和y的範圍不一樣的話,使用上面的方法就不奏效。當1<=x<=n,1<=y<=m時,
程式碼示例如下

#include<iostream>
#include<cstring>
#include<cstdio>
#include<string>
#include<map>
#include<stack>
#include<queue>
#include<set>
#include<sstream>
#include<ctime>
#include<algorithm>
#include<bits/stdc++.h>
#include<sstream>
#include<list>
#include<cmath>
#include<deque>
#include<cstdlib>
using namespace std;
typedef long long LL;
const int N = 10000005;

bool vis[N];
int p[N];
int cnt;
int g[N],u[N],sum[N];

void Init()
{
    memset(vis,0,sizeof(vis));
    u[1] = 1;
    cnt = 0;
    for(int i=2;i<N;i++)
    {
        if(!vis[i])
        {
            p[cnt++] = i;
            u[i] = -1;
            g[i] = 1;
        }
        for(int j=0;j<cnt&&i*p[j]<N;j++)
        {
            vis[i*p[j]] = 1;
            if(i%p[j])
            {
                u[i*p[j]] = -u[i];
                g[i*p[j]] = u[i] - g[i];
            }
            else
            {
                u[i*p[j]] = 0;
                g[i*p[j]] = u[i];
                break;
            }
        }
    }
    sum[0] = 0;
    for(int i=1;i<N;i++)
        sum[i] = sum[i-1] + g[i];
}

int main()
{
    Init();
    int T;
    scanf("%d",&T);
    while(T--)
    {
        LL n,m;
        cin>>n>>m;
        if(n > m) swap(n,m);
        LL ans = 0;
        for(int i=1,last;i<=n;i=last+1)
        {
            last = min(n/(n/i),m/(m/i));
            ans += (n/i)*(m/i)*(sum[last]-sum[i-1]);
        }
        cout<<ans<<endl;
    }
    return 0;
}

4、鴿巢原理(抽屜原理)

基本原理:n+1只鴿子飛回n個鴿籠至少有一個鴿籠含有不少於2只的鴿子。

例7、在萬聖節這天,每個小孩都回去挨家挨戶要糖果,但是鄰居們指向給出一定數量的糖果。如果你去的比較晚很可能就要不到糖果,所以孩子們就決定把所有要來的糖果放在一起,然後平均分攤。以以往的經驗,孩子們知道每個住戶會給出多少糖果,由於他們更關心要來的糖果是否能平分,所以他們只選擇了去一部分住戶索要糖果,這樣糖果恰好可以被平分,又不會有糖果剩下。

輸入:含有多組測試例項,測試樣例的第一行包含2個整數c和n(1<=c<=n<=100000),c是小孩的個數,n是住戶的數量,接下來一行包含n個整數分別用空格分開a1,a2,……,an-1,an(1<=ai<=100 000),ai表示第i個住戶只想給出ai塊糖果。輸入樣例以0 0 為結束標誌。
輸出:對於每個測試例項,輸出孩子們選擇的住戶的編號。如果沒有一組住戶給的糖果總數可以被孩子們平分,則輸出“no sweets”。如果有多組解,只需輸出任意一組即可。
輸入樣例:

4 5
1 2 3 7 5
3 6
7 11 2 5 13 17
0 0

輸出樣例:

3 5
2 3 4

小夥伴們可以先嚐試解決一下這個問題。