超大背包问题(二进制枚举 + 二分)

超大背包问题


第一次看到这一题好像是在某一场比赛,就是给你一个炸空间和时间的背包,让你选最大的价值,看似是01背包
然鹅今天在挑战程序设计这本书上看到了这题,看到了作者的做法,感觉豁然开朗,直接暴搜也会炸,但是我们
可以把这些物品拆分成两堆,然后我们用二进制枚举两个堆的物品,时间复杂度为\(O(n*2^{(n/2)})\)
在枚举第二堆的时候我们就可以在第一个堆里通过二分搜索找到满足条件物品堆,然后选择最优的结果就行
每次二分查询时间log(m),m表示的是第一堆物品有效个数,具体请看代码讲解

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int N = 45;
typedef long long ll;
ll w[N],v[N],W;
int n;

struct Node {
	ll w,v;
}ps[1 << (N/2)];//表示的是第一个堆枚举的可能的情况总数

bool cmp(Node a, Node b) {//排序函数,因为待会我们搜索的时候是按照W的大小进行二分
	if(a.w != b.w)
		return a.w < b.w;
	return a.v > b.v;
}

int erfen(int l,int r,ll k) {//二分搜索
	while(l + 1 < r) {
		int mid = l + r >> 1;
		if(ps[mid].w <= k) {
			l = mid;
		}
		else {
			r = mid;
		}
	}
//此时的r表示的是大于k的第一个位置
	return r - 1;
}

void slove() {
	int len1 = n / 2;
	for(int i = 0, len = 1 << len1; i < len; ++i) {//二进制枚举第一个堆可能的情况
		int tempw = 0, tempv = 0;
		for(int j = 0;j < len1; ++j) {
			if(i >> j & 1) {//是否选取
				tempw += w[j];
				tempv += v[j];
			}
		}
		ps[i] = (Node){tempw,tempv};
	}
	sort(ps,ps+(1<<len1),cmp);//排序
	
	int m = 1;
	for(int i = 1, len = 1 << len1; i < len; ++i) {//这里筛出无用的选取组合,
		if(ps[m - 1].v < ps[i].v) {//这样能让ps从0到m-1的元素都是w和v升序的
			ps[m++] = ps[i];   //也就是让后一个元素的v一定是大于前一个元素的v
		}
	}
	
	ll ans = 0;
	for(int i = 0,len = (1 << (n - len1)); i < len; ++i) {//二进制枚举第二个堆的选取情况
		int tempw = 0, tempv = 0;
		for(int j = 0; j < n - len1; ++j) {
			if(i >> j & 1) {
				tempw += w[len1 + j];
				tempv += v[len1 + j];
			}
		}
		if(tempw <= W) {
			int loc = erfen(-1,m,W-tempw);
                        ll key = 0;
			if(loc != -1)//可能第一个堆什么也不选就满了
			      key = ps[loc].v;
			ans = max(ans,key + tempv);//选取最优情况
		}
	}
	printf("%lld\n",ans);
}

int main()
{
	scanf("%d",&n);
	for(int i = 0; i < n; ++i) {
		scanf("%lld",&w[i]);
	}
	for(int i = 0; i < n; ++i) {
		scanf("%lld",&v[i]);
	}
	scanf("%lld",&W);
	slove();
	return 0;
}
posted @ 2020-11-30 20:47  MangataTS  阅读(106)  评论(0编辑  收藏  举报