超大背包问题(折半枚举)
超大背包问题
有重量和价值分别为w(i),v(i)的n个物品,从这些物品中选出总重不超过W的物品,求所有挑选方案中价值总和的最大值
1<=n<=40
1<=w(i),v(i)<=1e15
1<=W<=1e15
输入
第一行为n,接着输入一行w(i)和一行v(i),最后输入W占单独一行
输出
单独一行即所有挑选方案中价值总和的最大值
样例
Input
4
2 1 3 2
3 2 4 2
5
Output
7
思路
这个问题和普通的01背包一样,只是w,v的数据范围太大了,没法用常规的dp解决。但我们注意到了n很小,而每种物品只有选择和不选两种状态,所以可以尝试枚举所有物品的选择情况,但是复杂度已经达到O(2^40),是行不通的。所以这里我们考虑用折半枚举的思想,先把前面一半的物品的选择情况全部枚举出来,计入ps数组中(first是重量,second是价值),并按重量升序排序以便接下来的二分搜索。这时我们可以去除一些不必要的选择情况,当ps[i].first<ps[j].first&& ps[i].second>ps[j].second时我们一定不会选择第j种方案了,所以可以去掉。之后我们就继续把剩余物品的选择情况枚举出来,每枚举出来一种方案,让该方案和ps中重量first不超过W-sw的方案结合便得到一组解,记录最优解即可。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn = 50;
const ll INF = 1e18;
int n;
ll w[maxn], v[maxn], W;
pair<ll, ll> ps[1 << (maxn / 2)];//重量,价值
void solve() {
int n2 = n / 2;
//枚举0到n2-1的物品选择情况
for (int i = 0; i < 1 << n2; i++) {
ll sw = 0, sv = 0;
for (int j = 0; j < n2; j++) {
if (i >> j & 1) {
sw += w[j];
sv += v[j];
}
}
ps[i] = make_pair(sw, sv);
}
//去除肯定不选择的情况(这部分代码要多看几遍)
int cnt = 1;
sort(ps, ps + (1 << n2));//按重量排序
for (int i = 1; i < 1 << n2; i++) {
if (ps[cnt - 1].second < ps[i].second) {
ps[cnt] = ps[i];
cnt++;
}
}
//枚举剩下物品的选择情况并更新结果
ll ans = 0;
for (int i = 0; i < 1 << (n - n2); i++) {
ll sw = 0, sv = 0;
for (int j = 0; j < n - n2; j++) {
if (i >> j & 1) {
sw += w[j + n2];
sv += w[j + n2];
}
}
if (sw < W) {
int pos = lower_bound(ps, ps + cnt, make_pair(W - sw, INF)) - ps;
/*这里pos是不会等于0的,因为在枚举前n2个物品的时候,
肯定有一种选择方案是所有物品都不选而这个方案排序之后就是ps[0],所以
一定有ps[0].first == 0 < W - sw,
所以lower_bound(ps, ps + cnt, make_pair(W - sw, INF)) - ps >= 1
*/
if (ps[pos].first > W - sw || cnt == pos) pos--;//注意细节
ans = max(ans, sv + ps[pos].second);
}
/*
lower_bound(ps, ps + cnt, make_pair(W - sw, INF)会寻找数组ps中first值
第一个大于等于W - sw的元素,自己测试了下这里的second对搜索结果没有影响。
*/
}
printf("%lld\n", ans);
}
int main() {
while (scanf("%d", &n) == 1) {
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);
solve();
}
return 0;
}