枚举二进制数的子集

思想:

如1011的二进制子集有1011,1010,1001,1000,0011,0010,0001,0000

思想是每次对当前最小子元素-1与目标x取与运算。枚举到0为止。

  1. 成立原因:因为我们是由大到小,如果是每次减一不取与(&),显然枚举x次可以找到所有子集。
  2. 而与运算就是缩减了这部分时间,你每次减一后,如果当前数>下一个目标子元素,&x进行了一些删除操作(删除了当前数~下一个目标子元素中间的数)
  3. 考虑他们为什么会被删除吧,因为他们二进制上有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)

思路:

  1. 直枚举接计算i,j发现会重复计算多次相同的a,b(设a=i&j,b=i|j),所以我们考虑计算每组(a,b)然后算贡献。

  2. 我们先默认n>m(毕竟前后都一样),那么所有数len<log2n<14(即a,b都是二进制不超过14位的),如果枚举a,b二进制的组合,情况有3^len<3^14=4782969<5e6,暴力dfs可以接受。

  3. 显然,a是b的子集,那么b-a就是i与j不是一起出现的二进制上的1组成的数,所以b-a的二进制的子集就是i,j可以取到的所有组合情况,如gif.latex?b-a%3D%280101%29_%7B2%7D,那么可以是i=a+(0000),j=a+(0101)或者i=a+(0001),j=a+(0100),或者i=a+(0100),j=a+(0001)或者i=a+(0101),j=a+(0000)。

  4. 注意到有一个数(i,j谁都可以啦)要<=m,所以我们可以取b-a的子集中<=m-a的元素,这些元素的个数*gcd就是该(a,b)组合产生的贡献。

  5. 等等,还不行吧?注意到,可以取的子集也有下限,即你如果i取了x,那么j要取b-x,所以i=a+x,j=a+b-x,要同时满足1<=i,j<=n,所以a+b-x<=n,即下限是x>=b-n。

  6. 上面都说了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;
}

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值