首页 > 代码库 > NYOJ 1091 超大01背包(折半枚举)

NYOJ 1091 超大01背包(折半枚举)

这道题乍一看是普通的01背包,最最基础的,但是仔细一看数据,发现普通的根本没法做,仔细观察数组发现n比较小,利用这个特点将它划分为前半部分和后半部分这样就好了,当时在网上找题解,找不到,后来在挑战程序设计上找到了这个题,就拿来引用一下

 

挑选物品的方法总从2^n中,直接枚举肯定不行,因为n最大为40,但是如果n为20就可以了,这时候就要用到折半枚举,先枚举前一半,在枚举后一半。先把前把部分的选取方法对应的重量和价值总和记为w1, v1,这样后半部分寻找w2 <= W - w1时 使v2最大的选取方法就好了。

因此,我们要思考从枚举得到的(w2, v2)的集合中高效寻找max{v2|w2<=W‘}的方法。首先,显然我们可以排除所有w2[i] <= w2[j]并且v2[i] >= v2[j] 的 j。 这一点可以按照w2,v2的字典序排列后做到。此后剩余的元素都满足w2[i] < w2[j] , v2[i] < v2[j], 要计算max{v2|w2<=W‘}的话,只要寻找满足w2[i]<=W‘的最大的i就可以了。这可以利用二分搜索完成。剩余的元素个数为M的话,一次搜索要用log(M)的时间,可以解决

代码如下:

方法一(折半枚举):

 1   2 #include <iostream> 3 #include <algorithm> 4 #include <cstdio> 5 #define Max(a,b) a>b?a:b 6 #define INF 10000000000000000 7 using namespace std; 8 typedef long long LL; 9 const int MAX = 40;10 LL weight[MAX], value[MAX];11 LL W;12 pair<LL, LL> ps[1 << (MAX / 2)];13 int n;14 void slove()15 {16     //枚举前半部分 17     int n2 = n / 2;18     for (int i = 0; i < 1 << n2; i++)//前半部分的枚举总数为 2^(n/2); 19     {20         LL sw = 0, sv = 0;21         //每种结果选取特定的价值和重量(i.e 一共2个东西,就一共四种情况,都不选,选第一个,选第二个,都选) 22         for (int j = 0; j < n2; j++)23         {24             if (i >> j & 1)25             {26                 sw += weight[j];27                 sv += value[j];28             }29         }30         ps[i] = make_pair(sw, sv);//加入到ps数组中 31     }32     //对ps排序 33     sort(ps, ps + (1 << n2));34     //ps 去重 35     int m = 1;36     for (int i = 1; i < 1 << n2; i++)37         if (ps[m - 1].second < ps[i].second)38             ps[m++] = ps[i];39     LL res = 0;//保存结果 40     //枚举后半部分, 并且找到最优解 41     for (int i = 0; i < 1 << (n - n2); i++)//同样枚举的总个数 42     {43         LL sw = 0, sv = 0;44         for (int j = 0; j < n - n2; j++)//和前半部分的一样 45         {46             if (i >> j & 1)47             {48                 sw += weight[n2 + j];49                 sv += value[n2 + j];50             }51         }52         if (sw <= W)//加个判断求解最大价值,只有小于背包容量的时候 53         {54             LL tv = (lower_bound(ps, ps + m, make_pair(W - sw, INF)) - 1)->second;//找到前半部分对应的value 55             res = Max(res, sv + tv); 56         }57     }58     printf("%lld\n", res);59 }60 61 int main()62 {63     while (~scanf("%d %lld", &n, &W))64     {65         for (int i = 0; i < n; i++)66             scanf("%lld %lld", &weight[i], &value[i]);67         slove();68     }69     return 0;70 }71         

这个题也可以用搜做来做,搜索反而来的更快,因为n比较小

方法二(搜索):

 1 #include <stdio.h> 2 #include <string.h> 3 #define Max(a, b) a > b ? a : b 4 const int MAX = 45; 5 long long weight[MAX], value[MAX], sw[MAX], sv[MAX]; 6 long long W, n, ans; 7 //i表示当前取到第n-i个,cnt 表示当前的总value, w当前背包剩余的空间  8 void dfs(int i, long long cnt, long long w) 9 {10     if (i == 0)//取到最后 11     {12         ans = Max(ans, cnt);13         return;14     }15     if (w == 0 || cnt + sv[i] < ans)//背包满或者当前总的加上这个前i个的总价值小于当前的总value,这步是剪枝 16         return ;17     if (w >= sw[i])//因为是从上往下找的,所以只要当前容量能装下前i个的和,所以这时一定是最大的 18     {19         cnt += sv[i];20         ans = Max(ans, cnt);21         w = 0;22         return ; 23     }24     if (w > weight[i])//深搜两种状态 25         dfs(i - 1, cnt + value[i], w - weight[i]);//相当于01背包中的两种状态 26     dfs(i - 1, cnt, w);27 } 28 int main()29 {30     while (~scanf("%d %lld", &n, &W))31     {32         memset(sw, 0, sizeof(weight));33         memset(sv, 0, sizeof(value));34         ans = 0;35         for (int i = 1; i <= n; i++)36         {37             scanf("%lld %lld", &weight[i], &value[i]);38             sw[i] = sw[i - 1] + weight[i];39             sv[i] = sv[i - 1] + value[i];40         }41         dfs(n, 0, W);42         printf("%lld\n", ans);43     }44     45     return 0;46 } 

 

NYOJ 1091 超大01背包(折半枚举)