思想:
如1011的二进制子集有1011,1010,1001,1000,0011,0010,0001,0000
思想是每次对当前最小子元素-1与目标x取与运算。枚举到0为止。
- 成立原因:因为我们是由大到小,如果是每次减一不取与(&),显然枚举x次可以找到所有子集。
- 而与运算就是缩减了这部分时间,你每次减一后,如果当前数>下一个目标子元素,&x进行了一些删除操作(删除了当前数~下一个目标子元素中间的数)
- 考虑他们为什么会被删除吧,因为他们二进制上有1而x却没1,只有这个原因,如果你有的1x都有,你是不会被删除的,所以删除的都是非子集。又因为是逐步-1直到0,所以可以快速枚举所有子集。
for(int j=i; ; j=(j-1)&i)//枚举二进制子集
{
cnt[i].push_back(j);
if(!j)break;
}
一道例题:Problem - 5648 (hdu.edu.cn)
思路:
-
直枚举接计算i,j发现会重复计算多次相同的a,b(设a=i&j,b=i|j),所以我们考虑计算每组(a,b)然后算贡献。
-
我们先默认n>m(毕竟前后都一样),那么所有数len<log2n<14(即a,b都是二进制不超过14位的),如果枚举a,b二进制的组合,情况有3^len<3^14=4782969<5e6,暴力dfs可以接受。
-
显然,a是b的子集,那么b-a就是i与j不是一起出现的二进制上的1组成的数,所以b-a的二进制的子集就是i,j可以取到的所有组合情况,如,那么可以是i=a+(0000),j=a+(0101)或者i=a+(0001),j=a+(0100),或者i=a+(0100),j=a+(0001)或者i=a+(0101),j=a+(0000)。
-
注意到有一个数(i,j谁都可以啦)要<=m,所以我们可以取b-a的子集中<=m-a的元素,这些元素的个数*gcd就是该(a,b)组合产生的贡献。
-
等等,还不行吧?注意到,可以取的子集也有下限,即你如果i取了x,那么j要取b-x,所以i=a+x,j=a+b-x,要同时满足1<=i,j<=n,所以a+b-x<=n,即下限是x>=b-n。
-
上面都说了i,j都要>=1,如果a=0,那么b取子集0000不就炸了吗(a+(0000)=0<1).所以特判以下,a=0,增添上下限,x至少>=1,至多<=b-1(至少留一个1给另一个)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl "\n"
#define int long long
const int N = 2e4 + 10;
int len,ans,n,m;;
vector<int>cnt[N];
int tocnt(int x,int val)//upper_bound返回所有小于等于val个数的子集
{
return upper_bound(cnt[x].begin(),cnt[x].end(),val)-cnt[x].begin();
}
void dfs(int pos,int a,int b)
{
if(pos==len)
{
int sum=b-a,l=b-n,r=m-a;
if(!a)l=max(1ll,l),r=min(r,b-1);
if(l>r)return;
ans+=__gcd(a,b)*(tocnt(sum,r)-tocnt(sum,l-1));//划分上下限求贡献
return;
}
dfs(pos+1,a,b);
dfs(pos+1,a,b|(1<<pos));
if((a|(1<<pos))<=m)dfs(pos+1,a|(1<<pos),b|(1<<pos));//dfs枚举a,b组合情况
}
void mysolve()
{
cin>>n>>m;
if(n<m)swap(n,m);
len=ans=0;
while((1<<len)<=n)len++;//取二进制最长长度
for(int i=0; i<(1<<len); ++i)
{
cnt[i].clear();
for(int j=i; ; j=(j-1)&i)//枚举二进制子集
{
cnt[i].push_back(j);
if(!j)break;
}
sort(cnt[i].begin(),cnt[i].end());//排序使后面可以二分
}
dfs(0,0,0);
cout<<ans<<endl;
}
int32_t main()
{
std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
ll t=1;
cin >> t;
while (t--)
{
mysolve();
}
system("pause");
return 0;
}