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、在mn的昂各种任意指定C个格子构成一个棋盘,在任一个这样的棋盘上放置棋子,要求任意两个棋子不得位于同一行或者同一列上。在棋盘上放置k个棋子并满足上述要求的一种方法称为一个方案。如果这是个nn的棋盘,那么一共有多少种方案?
结论:Rk© = Rk-1(C(x))+Rk(C(e));R0© = 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))种。这样得到:
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
+
d
p
[
i
−
1
]
[
j
−
1
]
∗
(
c
[
i
]
−
(
j
−
1
)
)
dp[i][j] = dp[i-1][j] + dp[i-1][j-1]*(c[i]-(j-1))
dp[i][j]=dp[i−1][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
小伙伴们可以先尝试解决一下这个问题。