从一道题开始。
hdu 1796 Howmany integers can you find
题目的大意是说:给你一个数N,和一个集合M,要你找出小于N的,且只能被集合M中数除的数的个数,比如N=12,M={2,3},那么答案就是7,即2,3,4,6,8,9,10。这里的数都能被2或3整队。这个问题的关键就是避免重复计算,当然可以用一个vis数组来记录哪个数已经被访问过,如果你没有看到它的数据范围是 0<N<2^31,0<M<=10。
在比赛遇到相类似的题目,结束后,听学长讲,要用容斥原理做,去百度了下容斥原理。
在百度百科里,这样定义容斥原理:
在计数时,必须注意无一重复,无一遗漏。为了使重叠部分不重复计算,人们研究出一种新的计数方法,这种计数方法的基本思想是:先不考虑重叠的情况,把包含于某内容中的所有对象数目先计算出来,然后再把计数时重复的数目排斥出去,使得计算的结果即无遗漏又无重复,这种计数方法称为容斥原理。
用一个图来说明的话,我们如果要计算图示中图形内的数字个数的话,我们可以先把编号为1,3,7的圆内的数字加起来,注意到我们这时候编号为2,4,6区域内的数字个数加两次,把编号为5的区域内的数字个数加3次,那么我们就要把编号为2,4,6的区域内的数字个数分别减去1次,然后再把编号为5的区域内的个数加上一次,就可以得出答案。
简而言之,就是对于重叠次数只有奇数次的,我们加上,重叠次数为偶数次的,我们要减去。
知道了上面的知识,我们就可以快速的解决hud1796,问题的关键就变成如果得到集合M中所有的数字的组合。
代码如下:
#include<iostream>
#include<stdio.h>
using namespacestd;
int gcd(inta,int b)
{
int temp;
while(b>0)
{
temp=a%b;
a=b;
b=temp;
}
return a;
}
int main()
{
int n,k,cnt,a[20];
while(scanf("%d%d",&n,&k)!=EOF)
{
n--;cnt=0;
int temp,ans=0;
for(int i=0;i<k;i++)
{
scanf("%d",&temp);
if(temp>0&&temp<n)a[cnt++]=temp;
}
for(int i=1;i<(1<<cnt);i++)
{
int sum=0,lcm=1;
for(int j=0;j<cnt;j++)
{
if(i&(1<<j))
{
sum++;
lcm=a[j]/gcd(a[j],lcm)*lcm;
}
}
if(sum&1) ans+=n/lcm;
else ans-=n/lcm;
}
printf("%d\n",ans);
}
return 0;
}
也可以用递归来实现这个过程:
代码如下:
#include<iostream>
#include<stdio.h>
using namespacestd;
intn,k,a[20],cnt,sum;
intgcd(int,int);
voiddfs(int,int,int);
int main()
{
while(scanf("%d%d",&n,&k)!=EOF)
{
n--;cnt=0;sum=0;
for(int i=0;i<k;i++)
{
int temp;
scanf("%d",&temp);
if(temp>0&&temp<=n)a[cnt++]=temp;
}
for(int i=0;i<cnt;i++)
{
dfs(i,1,1);
}
printf("%d\n",sum);
}
return 0;
}
int gcd(inta,int b)
{
int temp;
while(b>0)
{
temp=a%b;
a=b;
b=temp;
}
return a;
}
void dfs(intnow,int lcm,int num)
{
lcm=a[now]/gcd(lcm,a[now])*lcm;
if(num&1) sum+=n/lcm;
else sum-=n/lcm;
for(int i=now+1;i<cnt;i++)
{
dfs(i,lcm,num+1);
}
}
升级版是 UVALive 4683,问题变成给你由K个数字组成的集合M,要你找出一个第N大的数,这个数满足,只能被M集合中的一个数整除。比如还是集合{2,3}要你找出第四大的数,就是(2,3,4,8,9),第四个数,即8。
容斥原理的升级版,一个整体的思路时,因为答案是小于10^15,所以我们可以二分去计算出mid下有多少个满足条件的数。如果刚好有N个数,那么就是找到了答案。跟上面的一题不同的是,我们计算的不是小于一个上限的能被集合中一个或多个数整除的数的个数,即关键的是去重。我们要找的是只能被一个数整除,如果能被两个数整除,那么这个数就不满足题意,不能计入,这也是为什么在这题里答案是8,而不是6的原因。
如果还是这个图,这个题目要我们求的是,编号为1,3,7的区域里个数,即删除所有有重叠的部分。
简而言之,就是对于重叠次数只有奇数次的,我们加上个数乘以重叠次数,重叠次数为偶数次的,我们要减去次数乘以重叠次数。
#include<iostream>
#include<stdio.h>
#define LL longlong
using namespacestd;
const LLINF=1000000000000000LL;
LLk,n,cnt,a[20];
struct NODE
{
LL sum,mul;
void get(LL a,LL b){sum=a;mul=b;}
}node[10000];
LL gcd(LL,LL);
LL cal(LL);
LL find(LL);
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
cnt=0;
scanf("%lld%lld",&k,&n);
for(int i=0;i<k;i++)
{
scanf("%lld",&a[i]);
}
for(int i=1;i<(1<<k);i++)
{
LL sum=0,lcm=1;
for(int j=0;j<k;j++)
{
if(i&(1<<j))
{
sum++;
lcm=a[j]/gcd(a[j],lcm)*lcm;
if(lcm>INF) break;
}
}
if(lcm<INF)
{
node[cnt++].get(sum,lcm);
}
}
// for(int i=0;i<cnt;i++)
// printf("%lld %lld\n",node[i].sum,node[i].mul);
printf("%lld\n",find(n));
}
return 0;
}
LL gcd(LL a,LLb)
{
LL temp;
while(b>0)
{
temp=a%b;
a=b;
b=temp;
}
return a;
}
LL find(LL x)
{
LL l=1,r=INF,mid;
while(l+1<r)
{
mid=l+(r-l)/2;
LL temp=cal(mid);
// printf("temp=%lld mid=%lld\n",temp,mid);
if(temp>=x) r=mid;
else l=mid;
}
return r;
}
LL cal(LL mid)
{
LL ans=0;
for(int i=0;i<cnt;i++)
{
if(node[i].sum&1)ans+=(node[i].sum*(mid/node[i].mul));
elseans-=(node[i].sum*(mid/node[i].mul));
}
return ans;
}