鸽笼原理&容斥原理

鸽笼原理

又叫抽屉原理
最基本的描述:如果有n种n+1个物品,那么至少有一种有两个物品
这种基本概念也没什么太多好阐释的,主要是鸽笼定理在思维上可能会造成一些奇怪的突破口
直接上题吧

例题1 POJ2356&POJ3370&HDU1808&UVA 11237 Halloween treats

四倍经验题了解一下
题意:给你两个整数C和N,再给你N个正数,从中找到若干数(不要求连续),使得其和刚好是 C的倍数。c<=n<=100000

题解

我们先让这些数对C取模,然后求前缀和
那么总共最多会有C种前缀和,但是实际上我们只需要考虑C-1中,因为前缀和等于0是本身就是答案
然后根据抽屉原理,由于 C1<N C − 1 < N ,所以一定有两个前缀和相同
所以呢尽管题目说了不要求连续,但是我们可以保证存在连续的满足答案

//其实POJ2356略有不同,不过差别不大
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e5+5;
int n,m;
int a[N];
int s[N];
int vis[N];
int main()
{
    while(~scanf("%d%d",&m,&n)&&m){
        memset(vis,0,sizeof vis);
        for(int i=1;i<=n;i++){
            scanf("%d",&a[i]);
            a[i]%=m;
            s[i]=(a[i]+s[i-1])%m;
        }
        for(int i=1;i<=n;i++){
            if(s[i]==0){
                for(int j=1;j<=i;j++)
                    printf("%d ",j);
                puts("");
                break;
            }
            if(vis[s[i]]){
                for(int j=vis[s[i]];j<=i;j++)
                    printf("%d ",j);
                puts("");
                break;
            }
            vis[s[i]]=i+1;
        }
    }
}

变式

POJ 3844
题意基本一样,给你两个整数M和N,再给你N个正数,从中找到连续的若干数,使得其和刚好是 M的倍数的方案数


额…本质上也是一样的,比如%M=i出现了cnti次,那么答案加上C(cnti,2)

例题2 HDU 5762 Teacher Bo

题意:多组数据(<=50) 有N个点,横纵坐标都在[0,M]中,然后问你能否找到两对点使得这两对点的曼哈顿距离相同 N,M<=1e5

题解

由于总共就只有2e5种曼哈顿距离的取值,那么如果我们遍历了2e5+1个点对,就一定会出现重复,所以我们直接暴力

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=2e5+5;
int n,m;
bool vis[N];
int x[N],y[N];
void Solve(){
    memset(vis,0,sizeof vis);
    for(int i=1;i<=n;i++)
        for(int j=i+1;j<=n;j++){
            int d=abs(x[i]-x[j])+abs(y[i]-y[j]);
            if(vis[d]){
                puts("YES");
                return ;
            }
            vis[d]=1;
        }
    puts("NO");
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)
            scanf("%d%d",&x[i],&y[i]);
        Solve();
    }
}

习题1 ZOJ2955 Interesting Dart Game

题意:一个飞镖盘分为n个区域(n<=100),你投中一个区域会得到wi的分(1<=wi<=100),然后问你使得总分恰好为m(1<=m<=1e9)最少要投多少次飞镖(无解输出-1)

题解

我们先把w数组排序
我们可以把它们每次得到的分作为一串数字,那么类似于例题中我们所用的方法,我们可以证明,当这串数字的长度大于等于wn时,一定存在一个子串的和是wn的倍数。那么也就是说价值小于wn的我们最多只会使用wn次(更准确的说是wn-1次)
然后我们可以先用剩下的n-1个进行完全背包,背包容量是wn*wn,然后对于每一个状态计算若要填满m,wn的使用次数即可
更简便一点的做法是我们把这n个都进行完全背包,然后我们求出最大的与m的差是wn的倍数的小于wn*wn的位置即可

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=10005;
int n;
int d[N];
int w[N];
int main()
{
    int t;
    scanf("%d",&t);
    while(t--){
        int n,m;
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)
            scanf("%d",&w[i]);
        sort(w+1,w+n+1);
        int mx=w[n]*w[n];
        memset(d,-1,sizeof d);
        d[0]=0;
        for(int i=1;i<=n;i++)
            for(int j=w[i];j<=mx;j++)
                if(d[j-w[i]]!=-1&&(d[j]==-1||d[j]>d[j-w[i]]+1))
                    d[j]=d[j-w[i]]+1;
        int tot=m,cnt=0;
        if(m>mx){
            tot=m%mx;
            cnt=(m-tot)/w[n];
        }
        int ans;
        if(d[tot]==-1)
            ans=-1;
        else
            ans=cnt+d[tot];
        printf("%d\n",ans);
    }
}

习题2 LightOJ 1100 Again Array Queries

题意:有N(n<=1e5)个数整(1<=ai<=1000),q组询问(q<=10000),每次询问[l,r]这个区间内所有的数对中最小的差值是多少

题解

乍一看是不是什么数据结构神题
由于ai<=1000,那么也就是说

if(r-l+1>1000)
    puts("0");

然后直接1000*q的大暴力即可

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=100005;
int a[N];
int z[N];
int main()
{
    int T;
    scanf("%d",&T);
    for(int t=1;t<=T;t++){
        int n,m;
        scanf("%d%d",&n,&m);
        for(int i=0;i<n;i++)
            scanf("%d",&a[i]);
        printf("Case %d:\n",t);
        for(int i=1;i<=m;i++){
            int l,r;
            scanf("%d%d",&l,&r);
            if(r-l+1>1000)
                puts("0");
            else{
                for(int i=l;i<=r;i++)
                    z[a[i]]++;
                int ans=1000;
                int last=-1;
                for(int i=1;i<=1000;i++){
                    if(z[i]>=2)
                        ans=0;
                    if(z[i]>=1){
                        if(last!=-1)
                            ans=min(ans,i-last);
                        last=i;
                    }
                }
                printf("%d\n",ans);
                for(int i=l;i<=r;i++)
                    z[a[i]]--;
            }
        }
    }
}

好了,鸽笼定理我们暂时告一段落


容斥原理

简单地说,如果说一些元素具有一些性质,那么这些的并集=包含至少1个性质-包含2个+包含3个…
解题的关键一般是这所谓的性质是什么性质,还有快速计算满足这一条件的集合大小的方法。而且经常使用求补集的计算技巧
(推荐一篇博客
容斥原理在OI中的实现一般有三种(都是2^n的数量级)
1.dfs,简洁明了吧,枚举01状态计算
2.队列数组,其实类似于bfs之于dfs,就是用类似广搜的方法进行拓展恰好n层
3.位运算,强烈推荐lowbit
算了单纯的知识内容我们就跳过吧,我们直接上例题吧

例题3 HDU 1796 How many integers can you find

题意:给出M个非负整数,问[1,n)中,有多少个数能至少被那M个数中至少一个数整除。 0<N<231,0<M<=10 0 < N < 2 31 , 0 < M <= 10

题解

em
版题啊
我就分别用dfs和队列数组实现(位运算实现见例题4)

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=15,M=1<<12;
int gcd(int x,int y){
    if(!y)
        return x;
    return gcd(y,x%y);
}
int lcm(int x,int y){
    return x/gcd(x,y)*y;
}
ll n;
int m;
int z[N],k;
ll ans;
void dfs(int x,int f,ll sum){
    if(x==k){
        ans+=n/sum*f;
        return ;
    }
    dfs(x+1,f,sum);
    dfs(x+1,-f,lcm(sum,z[x]));
}
int q[M];
int bfs(){
    int t=0;
    q[0]=-1;
    ll ans=0;
    for(int i=0;i<k;i++){
        int cnt=t;
        for(int j=0;j<=cnt;j++)
            q[++t]=q[j]/gcd(abs(q[j]),z[i])*z[i]*(-1);
    }
    for(int i=1;i<=t;i++)
        ans+=n/q[i];
    return ans;
}
int main()
{
    while(~scanf("%I64d%d",&n,&m)){
        n--;//因为是闭区间
        k=0;
        for(int i=1;i<=m;i++){
            int x;
            scanf("%d",&x);
            if(x)//注意是给出非负整数
                z[k++]=x;
        }
        ans=n;
        dfs(0,-1,1);
        if(bfs()!=ans)
            puts("What the ****! ");
        printf("%I64d\n",ans);
    }
}

例题4 HDU 4135 Co-prime

题意:求[a,b]区间中与n互质的数的个数
0<=a<=b<=1e15,1<=n<=1e9

题解

我们先考虑求[1,m]内与n互质的数
所以我们先质因数分解,然后就转化成了例题3,然后容斥原理解决此题

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=10005,M=1<<11;
int p[N+5],pcnt;
bool np[N+5];
int z[N+5],zcnt;
int bits[M+5];
void Init(){
    for(int i=0;i<11;i++)
        bits[1<<i]=i;
    for(int i=2;i<=N;i++){
        if(!np[i]){
            p[++pcnt]=i;
        }
        for(int j=1;p[j]*i<=N;j++){
            np[p[j]*i]=1;
            if(i%p[j]==0)
                break;
        }
    }
}
inline int lowbit(int x){
    return x&(-x);
}
ll get(ll x){
    ll ans=0;
    for(int i=0;i<(1<<zcnt);i++){
        int sign=1;
        ll sum=1;
        int S=i,Z;
        while(S){
            Z=lowbit(S);
            S^=Z;
            Z=bits[Z];
            sum*=z[Z];
            sign*=-1;
        }
        ans+=x/sum*sign;
    }
    return ans;
}
int main()
{
    Init();
    int T;
    scanf("%d",&T);
    for(int t=1;t<=T;t++){
        ll a,b;
        int n;
        scanf("%I64d%I64d%d",&a,&b,&n);
        zcnt=0;
        for(int i=1;i<=pcnt&&p[i]<=n;i++)
            if(n%p[i]==0){
                z[zcnt++]=p[i];
                while(n%p[i]==0)
                    n/=p[i];
            }
        if(n!=1){
            z[zcnt++]=n;
        }
        ll ans=get(b)-get(a-1);
        printf("Case #%d: %I64d\n",t,ans);
    }
}

例题5 BZOJ 1042 [HAOI2008] 硬币购物

硬币购物一共有4种硬币。面值分别为c1,c2,c3,c4。某人去商店买东西,去了tot次。每次带di枚ci硬币,买si的价值的东西。请问每次有多少种付款方法。
di,si<=100000 tot<=1000

题解

假如我们不考虑硬币数量限制,那么答案可以通过一个简单的类似完全背包的dp求出f[i]数组表示凑出i的方案数
然后呢,这些方案和什么性质有关呢?
和不满足多少种限制有关
所以呢,答案就是总方案-至少超过1种+至少超过2种-至少超过3种+至少超过4种
那么怎么快速求出方案呢
如果我们要不满足限制,我们先要令这个硬币使用di+1枚,然后剩下的我们就任意取——也就是说,f[tot-(di+1)*si]

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
using namespace std;
typedef long long ll;
const int N=100005,M=1000000;
const int mod=1e9+7;
ll d[N];
int v[5];
int num[5];
int n;
ll ans;
void dp(){
    d[0]=1;
    for(int i=1;i<=4;i++)
        for(int j=v[i];j<=N-5;j++)
            d[j]+=d[j-v[i]];
}
void dfs(int x,int f,int sum){
    if(sum<0)
        return ;
    if(x==5){
        ans+=1ll*f*d[sum];
        return ;
    }
    dfs(x+1,f,sum);
    dfs(x+1,-f,sum-(num[x]+1)*v[x]);
}
int main()
{
    for(int i=1;i<=4;i++)
        scanf("%d",&v[i]);
    dp();
    int m,sum;
    scanf("%d",&m);
    while(m--){
        ans=0;
        for(int i=1;i<=4;i++)
            scanf("%d",&num[i]);
        scanf("%d",&sum);
        dfs(1,1,sum);
        printf("%lld\n",ans);
    }
}

还有一道题Codeforces 451E Devu and Flowers
有N种颜色的花N<=20,每种花有ai种,问你选择s朵花的方案数

其实和这道题本质上是一样的

例题6 POJ 3904 Sky Code

给一串数字,求解满足四个数字gcd=1的四元组的个数

题解

我们考虑一下这四个数的公因数的质因子个数
我们奇数个素因子-,偶数个素因子+
是不是有点像 μ μ ?嗯不过不是…
但是这道题确实有莫比乌斯反演的做法
这样我们容斥一下就可以得到答案了

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=10005;
int p[N+5],pcnt;
bool np[N+5];
int sign[N+5],cnt[N+5];
int z[N+5],zcnt;
int bits[N+5];
int lowbit(int x){return x&-x;}
void Init(){
    for(int i=1;i<=10;i++)
        bits[1<<i]=i;
    for(int i=2;i<=N;i++){
        if(!np[i]){
            p[++pcnt]=i;
        }
        for(int j=1;p[j]*i<=N;j++){
            np[p[j]*i]=1;
            if(i%p[j]==0)
                break;
        }
    }
}
void Solve(int n){
    zcnt=0;
    for(int i=1;i<=pcnt&&p[i]<=n;i++)
        if(n%p[i]==0){
            z[zcnt++]=p[i];
            while(n%p[i]==0)
                n/=p[i];
        }
    if(n>1){
        z[zcnt++]=n;
    }
    for(int S=0;S<(1<<zcnt);S++){
        int t=1;
        int f=1;
        int T=S;
        while(T){
            int U=lowbit(T);
            T^=U;
            int u=bits[U];
            f=-f;
            t*=z[u];
        }
        cnt[t]++;
        sign[t]=f;
    }
}
ll calc(int x){
    return 1ll*x*(x-1)*(x-2)*(x-3)/24;
}
int main()
{
    int n,m;
    Init();
    while(~scanf("%d",&n)){
        memset(cnt,0,sizeof cnt);
        for(int i=1;i<=n;i++){
            scanf("%d",&m);
            Solve(m);
        }
        ll ans=0;
        for(int i=0;i<=N;i++)
            if(cnt[i])
                ans+=calc(cnt[i])*sign[i];
        printf("%lld\n",ans);
    }
}

习题3 BZOJ 4762: 最小集合

题意:我们知道,有N个元素的集合有 2N 2 N 个子集(含空集),现在我们从中取出若干个子集(至少一个),使得它们的交集的元素个数为K,求取法的方案数.答案对1000000007取模

题解

定义F(i)为交集至少为i个的选择方案
先C(n,i)从中选出i个元素,那么不含这i个元素的集合共有 2ni 2 n − i
然后每一个选和不选,就有 22ni 2 2 n − i 种方案,但是我们不能一个都不选,所以最终
F(i)=C(n,i)(22ni1) F ( i ) = C ( n , i ) ( 2 2 n − i − 1 )
然后容斥即可

ans=i=0nk(1)iC(nk,i)(22nki1) a n s = ∑ i = 0 n − k ( − 1 ) i C ( n − k , i ) ( 2 2 n − k − i − 1 )

那么怎么计算 22ni 2 2 n − i 呢?
我们令t(i)= 22ni 2 2 n − i ,则显然有t(i-1)=t(i)*t(i)
所以我们从后往前枚举i即可
以上。

 #include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const ll mod=1e9+7;
const int M=1000010;
ll ksm(ll x,ll y){
    ll res=1;
    while(y){
        if(y&1)
            res=(res*x)%mod;
        x=(x*x)%mod;
        y>>=1;
    }
    return res;
}
ll fact[M+5];
ll inv[M+5];
void pre(){
    fact[0]=1;
    for(int i=1;i<=M;i++)
        fact[i]=(fact[i-1]*i)%mod;
    inv[M]=ksm(fact[M],mod-2);
    for(int i=M-1;i>=0;i--)
        inv[i]=(inv[i+1]*(i+1))%mod;
}
inline ll C(ll n,ll m){
    if(n<m)
        return 0;
    return fact[n]*inv[m]%mod*inv[n-m]%mod;
}
ll solve(ll n){
    ll temp=2,ans=0;
    for(int i=n;i>=0;i--){
        if(i&1)
            ans=(ans-(C(n,i)*(temp-1))%mod+mod)%mod;
        else
            ans=(ans+(C(n,i)*(temp-1))%mod)%mod;
        temp=(temp*temp)%mod;
    }
    return ans;
}
int main()
{
    //freopen("set.in","r",stdin);
    //freopen("set.out","w",stdout);
    pre();
    ll n,m;
    scanf("%lld%lld",&n,&m);
    ll ans=(solve(n-m)+mod)%mod;
    ans=ans*C(n,m)%mod;
    printf("%lld\n",ans);
}

习题4 51nod 1407 与与与与

题意:有n个整数,问从他们中取出若干个数字相与之后结果是0的有多少组。
答案可能比较大,输出对于 1e9+7取模后的结果。
ai<220 a i < 2 20

题意

我们先考虑这样一个问题,令f(x)=有多少个i满足a[i]&x=x
那么,答案就是

x=0220(1)cnt(x)(2f(x)1) ∑ x = 0 2 20 ( − 1 ) c n t ( x ) · ( 2 f ( x ) − 1 )

(其中cnt[i]=__builtin_popcount(i))
所以现在的问题只剩下了如何求出f(x)
我们可以用dp的办法
我们令 d(i,j)ika[k] d ( i , j ) 表 示 只 考 虑 二 进 制 的 最 后 i 位 , 有 多 少 个 k 能 满 足 a [ k ] & j==j j == j
那么 f(j)=d(19,j) f ( j ) = d ( 19 , j )
若j的第k位为1,则
d(k,x)=d(k1,x) d ( k , x ) = d ( k − 1 , x )

否则
d(k,x)=d(k1,x)+d(k1,x+2k) d ( k , x ) = d ( k − 1 , x ) + d ( k − 1 , x + 2 k )

这样这道题就可以结束了

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=25,M=1<<20;
const int mod=1e9+7;
int n;
int d[N][M];
int pow2[M];
int main()
{
    pow2[0]=1;
    for(int i=1;i<M;i++)
        pow2[i]=(1ll*pow2[i-1]*2)%mod;
    while(~scanf("%d",&n)){
        memset(d,0,sizeof d);
        for(int i=1;i<=n;i++){
            int x;
            scanf("%d",&x);
            if(x&1)
                d[0][x]++,d[0][x^1]++;
            else
                d[0][x]++;
        }
        for(int i=1;i<20;i++)
            for(int j=0;j<1<<20;j++)
                if(j&(1<<i))
                    d[i][j]=d[i-1][j];
                else
                    d[i][j]=d[i-1][j]+d[i-1][j^(1<<i)];
        int ans=0;
        for(int i=0;i<1<<20;i++){
            if(__builtin_popcount(i)&1)
                ans-=pow2[d[19][i]]-1;
            else
                ans+=pow2[d[19][i]]-1;
            ans=(ans%mod+mod)%mod;
        }
        printf("%d\n",ans);
    }
}

以下题目感谢yhn的博客

习题5 AtCoder Regular Contest 093 F Dark Horse

2N 2 N 个选手参与一场比赛,比赛规则是:相邻的两个人比赛一次,败者淘汰掉,胜者继续进行,直到只剩一个人为止。
现在给出1号选手会败给哪些选手
并且已知其他选手之间均满足:两个选手比赛,编号小的一定会胜利。
现在可以安排每个选手初始的位置,要钦定1号选手吃鸡最后获胜,求能满足条件的初始位置的方案数。

题解

就放个链接了吧,下同
https://blog.csdn.net/qq_34454069/article/details/79734314

习题6 Atcoder ARC101 Ribbons on Tree

给定一棵点数为偶数的树,要求有多少种将点两两配对(配成n/2对)的方案使得每一条边至少被一对匹配点之间的路径覆盖。

题解

https://blog.csdn.net/qq_34454069/article/details/82086374

习题7 Atcoder ARC102 Stop. Otherwise…

有N个K个面的骰子,当i=2,3,4……2*k时, 求所有骰子的点数中,没有任何两个之和为i的方案数。这N个骰子不互相区分

题解

https://blog.csdn.net/qq_34454069/article/details/82292107

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值