容斥原理描述如下:
说大白话就是求几个集合的并集,要计算几个集合并集的大小,我们要先将所有单个集合的大小计算出来,然后减去所
有两个集合相交的部分,再加上所有三个集合相交的部分,再减去所有四个集合相交的部分...依此类推,一直计算到
所有集合相交的部分。
最简单的就是两个集合的并集:
所以数学公式就可以表示为 |A∪B|=|A|+|B|-|A∩B|。
对于三个集合,数学公式为|A∪B∪C|=|A|+|B|+|C|-|A∩B|-|A∩C|-|B∩C|+|A∩B∩C|。
常用方法有两种:递归法和二进制枚举法。
递归法是利用dfs的思想进行搜索,检索每一种方案进行容斥。
二进制枚举的方法最大的好处是能够枚举出所有元素组合的不同集合。假设一个集合的元素有m个,则对于m长的二进
制数来说就有m个1或0的位置,对于每一个1就对应一个元素。
整个二进制枚举完就是所有子集,从0到2^m就行。[0, 2^m)
看下模板题HDU-1796
题意:给定一个数n,数列m个数,求这小于n的数中,有多少个数满足能被这m个数中任意一个数整除。
思路:1~n之间有多少个能被x整除的数,公式为n/x,题目中要求小于n,所以(n-1)/x。
可以递归法求,需要保存中间态重叠x次的最小公倍数lcm,符合题意的数有(n-1)/lcm个,根据k表示重叠的次数进
行或者加上,或者减去。
也可以用二进制枚举法,将符合条件的m个数,看作m位,每位是0或者是1,那么一共有2^m种状态,只要判断一下每
一个状态有多少个1,也就是有多少个数(重叠多少次),记为k,每一个1代表哪几个具体的数,求这几个数的最小
公倍数,然后(n-1)/lcm, 利用k的值来判断应该减去还是加上即可。
Code1:
#include <bits/stdc++.h>
using namespace std;
int n, m, num[15], ans, tot, x;
int gcd(int a, int b)
{
if(a%b == 0) return b;
return gcd(b, a%b);
}
void dfs(int pos, int pre_lcm, int k)
{
for(int i = pos+1; i < tot; ++i)
{
int lcm = pre_lcm/gcd(num[i], pre_lcm)*num[i];
if(k&1) ans += (n-1)/lcm;
else ans -= (n-1)/lcm;
dfs(i, lcm, k+1);
}
}
int main()
{
while(~scanf("%d %d", &n, &m))
{
ans = 0; tot = 1;
for(int i = 1; i <= m; ++i)
{
scanf("%d", &x);
if(x > 0 && x < n) num[tot++] = x;
}
dfs(0, 1, 1);
printf("%d\n", ans);
}
return 0;
}
Code2:
#include <bits/stdc++.h>
using namespace std;
int n, m, x, k, tot, up, t, pos, lcm, ans, num[15];
int gcd(int a, int b)
{
if(a%b == 0) return b;
return gcd(b, a%b);
}
int main()
{
while(~scanf("%d %d", &n, &m))
{
tot = 1; ans = 0;
for(int i = 1; i <= m; ++i)
{
scanf("%d", &x);
if(x > 0 && x < n) num[tot++] = x;
}
up = (1<<(tot-1));
for(int i = 1; i < up; ++i)
{
t = i, k = 0, pos = 1; lcm = 1;
while(t)
{
if(t&1)
{
lcm = num[pos]/gcd(lcm, num[pos])*lcm;
++k;
}
t >>= 1; ++pos;
}
if(k&1) ans += (n-1)/lcm;
else ans -= (n-1)/lcm;
}
printf("%d\n", ans);
}
return 0;
}
图片来自百度图片,参考博文: