容斥原理应用
样例解释
n = 10, p1=2,p2=3, 求1-10中能满足能整除p1或p2的个数, 即
2,3,4,6,8,9,10
,共7个
解题思路
记Si为1-n 中能整除pi的集合,那么根据容斥原理, 所有数的个数为各个集合的并集,计算公式如下m⋃i=1Si=S1+S2+…+Sm−(S1⋂S2+S1⋂S3+…+Sm−1⋂Sm)+(S1⋂S2⋂S3+…+Sm−2⋂Sm−1⋂Sm)+…+(−1)m−1(m⋂i=1S)
以题目样例为例
S1={2,4,6,8,10},S2={3,6,9},S1⋂S2={6},故S1⋃S2={2,3,4,6,8,9,10}
实现思路
-
每个集合实际上并不需要知道具体元素是什么,只要知道这个集合的大小,大小为|Si|=n/pi, 比如题目中|S1|=10/2=5,|S2|=10/3=3
-
交集的大小如何确定?因为pi均为质数,这些质数的乘积就是他们的最小公倍数,n除这个最小公倍数就是交集的大小,故|S1⋂S2|=n/(p1\*p2)=10/(2\*3)=1
-
如何用代码表示每个集合的状态?这里使用的二进制,以m = 4为例,所以需要4个二进制位来表示每一个集合选中与不选的状态,m=4⏞1101,这里表示选中集合S1,S2,S4,故这个集合中元素的个数为 n/(p1\*p2\*p4), 因为集合个数是3个,根据公式,前面的系数为(−1)3−1=1。所以到当前这个状态时,应该是res+=n/(p1\*p2\*p4) 。这样就可以表示的范围从0000到1111的每一个状态
用二进制表示状态的小技巧非常常用,后面的状态压缩DP也用到了这个技巧,因此一定要掌握
AC代码
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 20;
int p[N], n, m;
int main() {
cin >> n >> m;
for(int i = 0; i < m; i++) cin >> p[i];
int res = 0;
//枚举从1 到 1111...(m个1)的每一个集合状态, (至少选中一个集合)
for(int i = 1; i < 1 << m; i++) {
int t = 1; //选中集合对应质数的乘积
int s = 0; //选中的集合数量
//枚举当前状态的每一位
for(int j = 0; j < m; j++){
//选中一个集合
if(i >> j & 1){
//乘积大于n, 则n/t = 0, 跳出这轮循环
if((LL)t * p[j] > n){
t = -1;
break;
}
s++; //有一个1,集合数量+1
t *= p[j];
}
}
if(t == -1) continue;
if(s & 1) res += n / t; //选中奇数个集合, 则系数应该是1, n/t为当前这种状态的集合数量
else res -= n / t; //反之则为 -1
}
cout << res << endl;
return 0;
}
1101表示的不应该是s1,s3,s4的集合吗?
同问
你是对的,但作者讲的思路是对的
同志,是从左到右遍历的,所以1101是s1,s2,s4的集合
顺序
写的很好!!!
这里我觉得为了避免最小公倍数溢出
long long
还是过于复杂了,在已知质数的最小公倍数是直接乘的情况下可以直接每次多除p[j]
作为等价产物来解决。for (int i = 1; i < (1 << m); i++ ) { // cm 记录本情况下子集的值 num记录本情况下选了多少个质数 int cm = n, num = 0; for (int j = 0; j < m; j++ ) { // 判断i的0~m-1位(第j位)是否为1 如果为1说明子集当中选择了p[j] if (i >> j & 1) { num++; cm = cm / p[j]; } // 否则不选p[j]直接看下一个 } if (num % 2 == 0) res -= cm; else res += cm; }
好棒的题解!感谢!
原来判断大于n是有可能的,我漏考虑了
🐂🍺
s++这一行为什么放在 if 的后面呢,我看y总放在前面的,不过我发现不影响结果,但就是有点疑问,break不会影响吗
放前放后都一样,都是计算用到的集合个数,如果发现不满足情况,break后,此时的t为-1,直接进行下一次循环,重新计算下一种状态。不会考虑这一次cnt的个数
大佬想问一下这个t是干啥用的,题目没让求这个啊
为了使 t * p [ j ]不超过n;因为是说求1~n的里面的数能被p [ j ]整除的数的个数
t表示的就是当前要求的那个被除的数,比如这个题目当中的2,3,6。
枚举每一位的状态其实就是在看当前要除的是哪个数,t会在一次循环中不断更新。
整个评论区凑不出一张动图
我i真的ikun啊
泥最好食
小黑子树枝666
别在这理发店
我现在在上概率论好舒服呀,mua
你干嘛哎哟喂,哦衣呀,啊哈,mua
我是真ikun啊,你们都是小黑子树枝6666
鸡你太美
大佬,这个j循环为什么不能小于等于i,一定要小于m,没有枚举的位不都是0么
第二层j循环时确定i的二进制每一位代表的集合有没有选中,它的每一位不一定全是0,同时也i的二进制也不是有i位的。
没看懂
i的二进制不一定是有i位,当i的二进制位数>m时候p数组可能会越界
是被整除不是整除!
//乘积大于n, 则n/t = 0, 跳出这轮循环 if((LL)t * p[j] > n){ t = -1; break; }
为什么一定要退出,当分母大于分子的时候结果是0,不会影响结果,我删了以后有些数据过vu了为什么中间判断t 和 n的大小时候 把判断放在t*p[j]之后 1000000000 16
9091 3793 4201 7901 3557 6473 6761 3491 5381 6883 9643 9491 6869 4787 1129 8443 这样的数据过不了
这个时间复杂度为什么是2^m
枚举每个集合选或不选的状态O(2^m), 每次枚举每一位O(m), 所以时间复杂度是O(2^m·m),数据范围m≤16,计算量大概是一百万