容斥原理
Part1:知识点
Part2:例题
【模板题】区间整除数
题意
给出一个数组
解题思路
-
总体来说,我们可先求出区间
中能被a数组整除的数,再求出 中能被a数组整除的数,两者相减即是答案 -
那么对于区间
,我们可先求出区间中能被a数组中任意一个数整除的数,那么总量就等于 。但这样很明显会算多,例如 ,对 ,我们求出总量就等于 。但其实,真正的答案应为 。这是因为 这个数它既是 的倍数,又是 的倍数 -
所以,我们考虑容斥原理。那么举个例子,对于区间
, ,答案就等于 ,(奇加偶减)。其中 表示最小公倍数 -
利用容斥原理,我们就可以很轻松地求出区间
和 的答案
代码
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int N=20;
int G,n;
LL l,r,ans,a[N];
LL gcd(LL a,LL b)
{
if(b==0)
return a;
return gcd(b,a%b);
}
LL lcm(LL a,LL b)
{
return a*b/gcd(a,b);
}
void dfs(int level,int k,LL multi,LL m) //level表示枚举到的a数组的下标,
{ //k表示选了多少数,multi表示最小公倍数,m表示区间右端点
if(level==n+1)
return;
dfs(level+1,k,multi,m);
if(lcm(multi,a[level])<=m)
{
k++;
multi=lcm(multi,a[level]);
if(k&1) //奇数加偶数减
ans+=m/multi;
else
ans-=m/multi;
dfs(level+1,k,multi,m);
}
}
int main()
{
cin>>G;
while(G--)
{
memset(a,0,sizeof(a));
cin>>n>>l>>r;
for(int i=1; i<=n; i++)
cin>>a[i];
LL sa,sb;
dfs(1,0,1,l-1);
sa=ans; ans=0;
dfs(1,0,1,r);
sb=ans; ans=0;
cout<<sb-sa<<endl;
}
return 0;
}
【练习题】Cowpatibility G
(题目传送门)
题目大意
研究证明,有一个因素在两头奶牛能否作为朋友和谐共处这方面比其他任何因素都来得重要——她们是不是喜欢同一种口味的冰激凌!
Farmer John 的
请求出不能和谐共处的奶牛的对数。
解题思路
-
我们可以反向考虑,设
表示前 头奶牛中有多少头奶牛和第 头奶牛有至少一种共同口味。那么,不能和谐共处的奶牛对数就是 -
现在我们来考虑如何求出
,我们考虑容斥原理,分别地求出有1种口味相同的,2种口味相同的……5种口味相同的,再利用奇加偶减的方法求出答案 -
那为了达到这个效果,我们可以考虑用二进制来枚举第
头奶牛所有口味的选择情况。对于每一种情况,我们可以将选择的口味用字符串连接起来。我们事先建立一个 储存前 头奶牛每一种选择的情况的个数,这样就可以轻松地求出 -
枚举完每一种情况后,我们还需要将它加进去
里,方便之后奶牛的操作
注意事项
-
在读入第
头奶牛的5种口味后,我们需要将它们排个序,否则就会出现两头奶牛口味分别是"2,3,5"和"5,3,2",却把他们当作不一样的情况 -
用字符串连接选择情况时要注意中间加间隔符
代码
#include <bits/stdc++.h>
using namespace std;
long long n,ans; //ans是对所有f(i)的求和
string a[10];
map <string,long long> f; //这里的f并不是真正意义上的f(i),而是前i-1头奶牛中每种口味选择情况的个数
int main()
{
cin>>n;
for(int i=1; i<=n; i++)
{
for(int j=1; j<=5; j++)
cin>>a[j];
sort(a+1,a+1+5); //排序
for(int j=1; j<(1<<5); j++) //枚举第i头奶牛选择情况
{
int cnt=0;
string s="";
for(int k=1; k<=5; k++)
{
if(j&(1<<(k-1)))
{
cnt++;
s+=a[k]+','; //用带间隔符的字符串连接
}
}
if(cnt&1) //奇加偶减
ans+=f[s];
else
ans-=f[s];
f[s]++; //更新map
}
}
cout<<n*(n-1)/2-ans;
return 0;
}
【练习题】完全平方数
(题目传送门)
题目大意
询问第k个不是完全平方数或完全平方数整数倍的整数。
1不视作完全平方数。
解题思路
-
首先看到这么大的数据,我们考虑二分答案,求出区间
里有多少个不是完全平方数或完全平方数的倍数。注意,区间上界最大为 ,读者可自行思考原因 -
但我们很难直接求出答案,于是我们反向考虑,求出“是”的,再用”总“的减去“是”的
-
因为任意一个完全平方数
都可以表示成若干个质数的平方的乘积,即 ,我们就可以考虑容斥原理,通过对于区间 里的质数的选择再根据每个 所对应的 的奇偶性求出答案。即“是”的= -
但是,由于质数的数量非常多,利用dfs或者二进制枚举肯定会超时,所以我们要换一个角度去考虑。我们不难发现,有些质数的乘积的平方是会超过
的范围的,即 ,这种情况可以舍去,而且我们计算时考虑的是 和 的质因子个数,所以我们其实可以从 枚举到 ,对于其中的每个数进行质因数分解,如果它存在某个质因子的指数大于 ,则舍去,否则就将它加入我们待操作的数里面,这样便可以有效的解决问题
代码
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int N=50010;
LL t,k;
LL tot,a[N];
bool check(LL mid)
{
LL sum=0;
for(int i=1; i<=tot && a[i]*a[i]<=mid; i++)
sum+=mid/(a[i]*abs(a[i]));
return mid-sum>=k; //"总"的减去"是"的
}
int find() //二分答案
{
LL lo=0,hi=k<<1,mid;
while(lo+1<hi)
{
mid=(lo+hi)>>1;
if(check(mid))
hi=mid;
else
lo=mid;
}
return hi;
}
void fir_make() //对1~根号k里的数进行质因数分解
{
for(int i=2; i<=44721; i++) //44721是不大于根号2*10^9的最大整数
{
bool flag=1;
int cnt=0,ii=i;
for(int j=2; j*j<=i; j++)
{
if(!(ii%(j*j))) //如果某个质因子的指数大于2则舍去
{
flag=0;
break;
}
if(!(ii%j))
cnt++,ii/=j;
}
if(ii>1)
cnt++;
if(flag)
{
if(cnt&1) //奇加偶减
a[++tot]=i;
else
a[++tot]=-i;
}
}
}
int main()
{
fir_make(); //预处理
cin>>t;
while(t--)
{
cin>>k;
cout<<find()<<endl;
}
return 0;
}
【扩展题】扮演方案
题目大意
- FJ有
头奶牛,第i头奶牛的体重是 。现在FJ要把这 头奶牛分成两堆,扮演警察和小偷的游戏。 - 每头奶牛要么扮演警察要么扮演小偷。扮演警察的至少要有一头奶牛,扮演小偷的奶牛也至少要有一头。
- 扮演警察的所有奶牛聚在一起,会产生一个“合作指数”,
“合作指数”的值是所有扮演警察的奶牛的体重按照二进制数展开后,再进行二进制 操作的结果。 - 同样,扮演小偷的奶牛们也有“合作指数”,也是同样的计算方法。
- 为了使得游戏好玩,FJ决定,扮演警察的奶牛的“合作指数”和扮演小偷的奶牛的“合作指数”要相等。
- 问题是:有多少种“不同的扮演方案”?
下面定义不同的“扮演方案”,假设A、B是两种扮演方案,如果至少存在一头奶牛(不妨假设是奶牛X),奶牛X在方案A中扮演的角色和在方案B中扮演的角色不同,那么方案A和方案B就是不同的方案。
解题思路
-
我们可以将问题转化成求奶牛与警察“合作指数”不相等的方案,再用总方案数减去它得到答案
-
但是求指数不相等的方案仍十分困难。我们发现
有 即 这么多,因此我们先考虑只有 位的情况。、 -
只有
位时,我们可将数分为 和 两种。很明显,要想让两个指数不相等,则所有的 都要放在一起,否则两边的指数都为 。这时,我们就对剩下的 进行分析,对于任意一个 ,它放在哪边都对结果没有影响,所以令 的个数为 ,则 的方法数有 ,但由于不能把所有 和 都放在一起,所以方法数为 。又由于 也有两种方式,所以总方案数为 -
我们既然可以求出有
位不相等的方案,那我们也可以求出 不相等的方案, 位不相等的方案……。那么根据容斥原理,我们对奇数位不相等的方案要加上,偶数位不相等的方案要减去 -
那么问题是,我们怎么求出同时满足好几位指数不相等的方案数呢?先来考虑只有两位同时不相等的情况,假设共有
个数,他们展开二进制后是: 。第一位(右往左数)为 的是第一、四个,第二位为 的是第一、二、三个。我们之前已经说过,同一位的 必须全部在同一个集合里面。所以第一、四个必须在同一个集合里,第一、二、三个也必须在同一个集合里面,也就是这四个元素都必须在同一个集合里面。所以我们完全可以把这四个元素看成一个“大元素”。因此这个六个数实际上是 这三个元素。方案数也很简单: (所有2是减去数都在同一个集合的两种情况) -
这时候,我们就需要利用并查集来帮助我们判断几个元素是否在同一集合内。将同一位里面的是
的元素连边,然后找一下一共有多少个不同的集合,设集合数为 ,则方案数为
注意事项
-
当同一位之中没有
,全是 时,显然按位与之后两个集合内这一位都是 ,不满足“同时满足每一位按位并后不相等“”的情况,可直接pass掉。当所有的元素的某一位中全部都是 ,那么合并之后肯定只剩下一个集合,也可以pass掉。对于这两种情况,我们可以给 加上 (感性理解一下即可) -
在二进制枚举之前预处理每一位中有哪些元素是
,到时候枚举时就可以很快速地连边 -
注意开long long!
代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=30,N=60;
int G,n,w[N];
int zero[MAXN][N],len[MAXN],fa[N]; //zero[i][j]表示第i位为0的是第几个,len[i]表示第i位是0的有几个
long long ans; //不相等的方案数
int lowbit(int x)
{
return x&(-x);
}
int get(int x)
{
if(fa[x]==x)
return x;
return fa[x]=get(fa[x]);
}
void merge(int x,int y)
{
fa[get(x)]=get(y);
}
int main()
{
cin>>G; //多组测试数据
while(G--)
{
memset(w,0,sizeof(w));
memset(len,0,sizeof(len));
memset(zero,0,sizeof(zero));
ans=0;
cin>>n;
for(int i=1; i<=n; i++)
cin>>w[i];
for(int i=0; i<20; i++)
for(int j=1; j<=n; j++)
if(!(w[j]&(1<<i)))
zero[i][++len[i]]=j; //预处理每一位为0的是哪几个数
for(int i=1; i<=1048575; i++) //二进制枚举所有选择情况
{
int tot=0,cnt=0;
bool flag=1;
for(int j=1; j<=n; j++)
fa[j]=j; //并查集初始化
for(int j=0; j<20; j++) //枚举选择的位
{
if(i&(1<<j)) //选了第j位
{
tot++; //计数器++
if(!len[j]) //这一位没有0
{
flag=0; //pass掉
break;
}
for(int k=1; k<len[j]; k++) //并查集连边
merge(zero[j][k],zero[j][k+1]);
}
}
for(int j=1; j<=n; j++) //查询集合个数
if(fa[j]==j)
cnt++;
if(!flag || cnt==1) //感性理解
{
i=i+lowbit(i)-1;
continue;
}
if(tot&1) //奇加偶减
ans+=(1LL<<cnt)-2;
else
ans-=(1LL<<cnt)-2;
}
cout<<((1LL<<n)-2-ans)<<endl; //总的减去不相等的
}
return 0;
}
【扩展题】 跳跃
题目大意
- 有一只青蛙在点
处,它要跳 步跳到点 。青蛙每一步是这样跳的:假设青蛙现在所在的点坐标是 ,那么他一步可以跳到点 ,但是 和 要满足如下两个条件: - 1、
, ,且点 和点 不是同一个点。
2、输入数据会给出一个数组 , 对于所有的 , 和 必须是不同的点。 - 输入数据保证
一定是 的整数倍。
解题思路
(感谢wzr同学的帮助)
-
首先将问题转化成求不合法的方案数,再用总的减去不合法的求出合法的
-
假设不考虑
的限制,考虑用最简单的 算法:设 表示跳 步后到达 的方案数。则有:
- 但这样会超时,我们可以把
个状态 和 拆出来,形成 和 的 个动态规划数组。则有:
-
我们再接着考虑
的限制。在计算的时候,要保证 和 是不同的 个点,这时想到,可以直接令 -
再来考虑
的限制,我们知道:正确答案 = 不考虑限制的答案 - 不合法的答案。考虑跳跃的过程,可以得到等式:
其中
只要有一对
- 设
表示至少有 对 不合法,其不合法的每个 的和为 时的方案数。则有:
- 设
表示至少有 对 不合法的方案数。则有:
- 根据容斥原理,不合法总方案数为:
代码
#include<bits/stdc++.h>
using namespace std;
const int N=60,M=900,R=1700,MOD=10007;
int n,tx,ty,mx,my,r,bad[N];
int fx[R][M],fy[R][M],sum[M],f[R][R];
int dp[R][M],illgeal[R],ans;
void make_triangle()
{
for(int i=0; i<=r; i++)
f[i][0]=f[i][i]=1;
for(int i=1; i<=r; i++)
for(int j=1; j<i; j++)
f[i][j]=(f[i-1][j]+f[i-1][j-1])%MOD;
}
void fir_DP()
{
fx[0][0]=fy[0][0]=1;
for(int i=1; i<=r; i++)
{
memset(sum,0,sizeof(sum));
for(int j=0; j<=tx; j++)
{
if(j) //前缀和优化
sum[j]=sum[j-1];
fx[i][j]=sum[j]=(sum[j]+fx[i-1][j])%MOD;
if(j>mx)
fx[i][j]=(fx[i][j]-sum[j-mx-1]+MOD)%MOD;
}
memset(sum,0,sizeof(sum));
for(int j=0; j<=ty; j++)
{
if(j)
sum[j]=sum[j-1];
fy[i][j]=sum[j]=(sum[j]+fy[i-1][j])%MOD;
if(j>my)
fy[i][j]=(fy[i][j]-sum[j-my-1]+MOD)%MOD;
}
}
}
void sec_DP()
{
bad[++n]=0;
dp[0][0]=1;
for(int i=1; i<=r; i++)
{
for(int j=0; j<=min(tx,ty); j+=10)
{
for(int k=1; k<=n; k++)
{
if(j>=bad[k])
dp[i][j]=(dp[i][j]+dp[i-1][j-bad[k]])%MOD;
}
}
}
}
void make_ans()
{
ans=(fx[r][tx]*fy[r][ty])%MOD;
for(int i=1; i<=r; i++)
{
for(int j=0; j<=min(tx,ty); j+=10)
illgeal[i]=(illgeal[i]+dp[i][j]%MOD*fx[r-i][tx-j]%MOD*fy[r-i][ty-j]%MOD+MOD)%MOD;
illgeal[i]=(illgeal[i]*f[r][i])%MOD;
if(i&1) //容斥原理
ans=(ans-illgeal[i]+MOD)%MOD;
else
ans=(ans+illgeal[i])%MOD;
}
}
int main()
{
cin>>tx>>ty>>mx>>my>>r>>n;
for(int i=1; i<=n; i++)
cin>>bad[i];
make_triangle(); //预处理求出组合数
fir_DP();
sec_DP();
make_ans();
cout<<ans;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?