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))种。这样得到:

d p [ i ] [ j ] = d p [ i 1 ] [ j ] + d p [ 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

小伙伴们可以先尝试解决一下这个问题。

猜你喜欢

转载自blog.csdn.net/xielinrui123/article/details/80385601