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
根據這個錯排公式就可以根據遞推的方法進行解決了。
例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))種。這樣得到:
這個式子就是問題的解。
示例程式碼:
#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
小夥伴們可以先嚐試解決一下這個問題。