常用小技巧之折半枚举(超大背包实例)

折半枚举

有时候,问题的规模比较大,外面无法枚举所有元素的组合,但能枚举一半或者一部分的组合。此时,将问题拆分成两半或几部分后分别枚举,再合并他们,这一方法往往非常有效。

举个例子
例题: 给定各有n个整数的四个数列A,B,C,D。要从每个数列中个取出1个数,使四个数的和为0,求出这样组合的个数。当一个组合中有多个相同的数字时,把他们当不同的数字看待。(poj 2785)

思路: 从四个数列中选择的话共有n4种情况,所以全部枚举一遍肯定不行,不过将他们对半分成AB和CD再考虑,就可以快速的解决了。从两个数列中选择的话就只有n2种组合,可以进行枚举。从A、B中选出a,b后,为使总和为0,所以应使c+d = -a-b。因此枚举C、D中取数字,然后计算所有情况的和,将其排个序,这样就可以二分搜索了,时间复杂度是O(n2logn)。

代码模板:

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 4010;

long long n,a[N],b[N],c[N],d[N],sum[N*N],ans;	//sum是c和d数列所有和的数组

void solve()
{
	for(int i = 0;i < n;i++)
		for(int j = 0;j < n;j++)
			sum[i*n+j] = c[i] + d[j];	//计算c和d数列的和
	sort(sum,sum+n*n);			//排序
	for(int i = 0;i < n;i++)
	{
		for(int j = 0;j < n;j++)
		{
			long long cd = -(a[i]+b[j]);	//先求出此时c和d的和应该是多少,记为cd
			ans += upper_bound(sum,sum+n*n,cd) - lower_bound(sum,sum+n*n,cd);	//然后找c和d的和中有几个cd,二分找到大于的地址减去大于等于的地址就是等于cd的个数
		}
	}
	cout << ans << endl;
}

int main()
{
	cin >> n;
	for(int i = 0;i < n;i++)
		cin >> a[i] >> b[i] >> c[i] >> d[i];

	solve();

	return 0;
}

lower_bound和upper_bound函数具体可以参考此文章


超大背包问题

我们现在有体积和价值分别为v和w的n个物品。现在从这些物品中选出体积不超过m的物品放进背包中,求所有挑选方案中价值总和的最大值。
(1 <= n <= 40,1 <= wi,vi <= 1015,1 <= m <= 1015

思路: 这就是一个很基础的01背包问题,但我们知道,01背包的时间复杂度是O(nm),n是物品数,m是背包体积,因为我们用了两层循环一层循环物品,一层循环体积。但此处的背包体积非常大,达到了1015,若再用nm的复杂度肯定会超时,所以针对这个体积超大的背包,我们应该利用n比较小这一特点,去考虑问题。

最简单的想法是枚举所有选物品的情况,但挑选物品的方法共有2n种,所以不能直接枚举,但我们可以拆成两部分,分别枚举,220的复杂度还是可以接受的。
当我们把前半部分所有情况枚举出来后,如何判断后面一部分呢?记前半部分一种选取方法对应的体积和价值总和分别为v1和w1。这样在后半部分寻找总体积v2 <= m-v1时使v2达到最大就好了。我们要思考从所有枚举的(v2,w2)的集合中高效的找到max{w2|v2 <= m-v1},因此我们可以排除所有的v2[i] < v2[j]且w1[i] > w1[j]的 j的情况,这一点可以按照v2,w1排序筛出来。得到的所有元素都满足v2[i] < v2[j]且w2[i] < w2[j](如下图),要计算max{w2|v2 <= m-v1}只需要找到满足v2 <= m-v1的最大i就可得到,这里可以用二分完成。所以总的时间复杂度是O(2(n/2)n)。

如下图,若某种选法的总体积大于前一个但总价值还小于前一个(红圈内的情况),那我们肯定不选择这种选法,所以我们直接删去这种选法来简化计算。
eg
代码详解:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long ll;
const int N = 50;
const ll INF = 0x3f3f3f3f;

ll n,m,v[N],w[N];
vector<pair<ll,ll> > p;		//p存第一次枚举出的所有的v和w

void solve()
{	
	int nn = n / 2;		//分成两半
	for(int i = 0;i < 1 << nn;i++)		//用二进制的每一位的0和1表示该件物品选与不选,如第五位若是1表示选第五件物品,若为0表示不选第五件物品,1左移nn位可以表示nn件物品的所有选取情况
	{	
		ll v1 = 0,w1 = 0;
		for(int j = 0;j < nn;j++)	//循环nn件物品
		{
			if(i >> j & 1)		//判断第i位是否为1
			{
				v1 += v[j];	//若为1表示选了第j件物品,加上体积和价值
				w1 += w[j];
			}
		}
		p.push_back({v1,w1});	//存入p数组
	}

	sort(p.begin(),p.end());	//对p排序
	
	//筛除必不可能选的元素
	int num = 1;	//num来存下所有可以选的物品
	for(int i = 1;i < 1 << nn;i++)	//循环所有物品(此时已经按体积从小到大排好序了)
		if(p[num-1].second < p[i].second)	//如果当前的价值大于刚才存的最后一个的价值,就把这个存进去
			p[num++] = p[i];	//存进去

	//枚举后半部分
	ll ans = 0;
	for(int i = 0;i < 1 << (n-nn);i++)	//循环后半部分的所有情况
	{
		ll v2 = 0,w2 = 0;
		for(int j = 0;j < n-nn;j++)	//循环后半部分所有物品
		{
			if(i >> j & 1)		//若选取第nn+j件物品
			{
				v2 += v[nn+j];	//加上第nn+j件物品的体积和价值
				w2 += w[nn+j];
			}
		}
		if(v2 <= m)	//如果只选取后半部分的体积已经超过了m就不用再判断了
		{
			ll tw = (lower_bound(p.begin(),p.begin()+num,make_pair(m-v2,INF))-1)->second;	//在第一部分的情况中查找体积,找到第一个大于m-v2的情况,再往回找一个,就是最大的v1了
			ans = max(ans,w2 + tw);	//答案就是此时的w1+w2,并找到最大值
		}
	}
	cout << ans << endl;	//输出答案即可
}

int main()
{
	cin >> n >> m;
	for(int i = 0;i < n;i++)
		cin >> v[i] >> w[i];
	
	solve();

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值