01背包问题

来源:互联网 发布:网络推广方案 编辑:程序博客网 时间:2024/06/06 16:46

暑假训练,算是正式步入ACM的世界,有形形色色太多的问题等着我去面对,去学习,第一个要解决的板块, 就是01背包问题。 这一段真的学的迷迷糊糊的,我只能先把自己掌握的部分列举一下。日后理解深刻,再做补充。 

01背包,离不开一段最核心的代码:```  

 for( i = 1; i <= n; i++){                        

<span style="font-size:24px;">            for( j = m; j >= w[i]; j--){                      dp[j] = max( dp[j], dp[j-w[i]] + v[i] );                                         }                                            }</span>

i代表的是物件的数量,我对外层这个循环的理解是考虑第i个物品该怎么处理,是放,还是不放。  

j代表的是我们要考虑的空间```,很多人印象里这里应该这样写:  

<span style="font-size:24px;">                           for( i = 1; i <= n; i++)                                                                                         for( j = 0; j <= m; j++){                                                                    if( j < w[i]){                                                                                            dp[i][j] = dp[i-1][j];                                                                     }                                                  else{                                                   dp[i][j] = max( dp[i-1][j], dp[i-1][j-w[i]] );                                           }                  </span>
<span style="font-size:24px;">                                         }                </span>

当然还有的是 i从n开始减的,这样就把每处i-1改成i+1,这个原理可理解为回溯,也就是我们在当下的情形无法直接做出判断,所以我们从之前的分析结果里来提取。而二维数组这种写法,比较麻烦,其实我们完全可以用一维数组来代替,我们可以从两个方面来接纳理解这种写法的好处: 

1、从for(j = m; j >= w[i]; j--)这里,我们可以发现,这一步其实就包含了 j与w[i]的大小判断,我们只判断j>=w[i]的情形,就可以省去if判断。  

2、省去写[i],关键就在于我们的j是从m开始递减这样来讨论,我理解的不够深刻,想详细了解,参考这里:> http://blog.csdn.net/tr990511/article/details/7595854 总之一定要养成用一维数组的习惯,十分重要。  

 

下面就是我做题训练的过程了: 

一、最最简单,最最基础,最01背包的题——杭电2602+

传送门:http://acm.hdu.edu.cn/showproblem.php?pid=2602 

Problem Description
Many years ago , in Teddy’s hometown there was a man who was called “Bone Collector”. This man like to collect varies of bones , such as dog’s , cow’s , also he went to the grave …
The bone collector had a big bag with a volume of V ,and along his trip of collecting there are a lot of bones , obviously , different bone has different value and different volume, now given the each bone’s value along his trip , can you calculate out the maximum of the total value the bone collector can get ?

 

Input
The first line contain a integer T , the number of cases.
Followed by T cases , each case three lines , the first line contain two integer N , V, (N <= 1000 , V <= 1000 )representing the number of bones and the volume of his bag. And the second line contain N integers representing the value of each bone. The third line contain N integers representing the volume of each bone.
 

Output
One integer per line representing the maximum of the total value (this number will be less than 231).
 

Sample Input
15 101 2 3 4 55 4 3 2 1
 

Sample Output
14

好吧,纵然尽管即使的确我说这道题很简单,我还是做错了,傻boy,唉,先写正确的代码,再给出我这个小学渣出错的代码,有则改之,无则加勉。

<span style="font-size:24px;"></pre><pre name="code" class="cpp"><span style="font-size:18px;"><strong>#include<stdio.h> #include<algorithm>using namespace std;int T, N, V;int dp[1111];int w[1111], v[1111];int max( int a, int b ){    if( a > b )     return a;    else     return b;}int main(){    int i, j;    scanf("%d",&T);    while(T--){     memset(dp,0,sizeof(dp));     memset(w,0,sizeof(w));     memset(v,0,sizeof(v));     scanf("%d%d",&N,&V);     for( i = 1; i <= N; i++ ){           scanf("%d",&v[i]);      }     for( i = 1; i <= N; i++ ){           scanf("%d",&w[i]);      }     for( i = 1; i <= N; i++ ){      for( j = V; j >= w[i]; j-- ){dp[j] = max(dp[j], dp[j-w[i]] + v[i]);                                }                }        printf("%d\n",dp[V]);      }      return 0;}</strong></span></span>
的确核心的那一部分代码只要掌握了,这道题就基本没问题了,但还是有细节细节!也就是每一个测试样例开始输入前,一定要memset 0一下,道理大家都懂,但我当时出错确实是因为这个,现在知错了。  

二、稍微提高了一些难度和精度掌握的一道题——杭电1203+

传送门:http://acm.hdu.edu.cn/showproblem.php?pid=1203(题外话:我一直不明白这个网址上php后面那个"?"是啥意思,有知道的嘛?哎呀强迫症犯了)


Problem Description
Speakless很早就想出国,现在他已经考完了所有需要的考试,准备了所有要准备的材料,于是,便需要去申请学校了。要申请国外的任何大学,你都要交纳一定的申请费用,这可是很惊人的。Speakless没有多少钱,总共只攒了n万美元。他将在m个学校中选择若干的(当然要在他的经济承受范围内)。每个学校都有不同的申请费用a(万美元),并且Speakless估计了他得到这个学校offer的可能性b。不同学校之间是否得到offer不会互相影响。“I NEED A OFFER”,他大叫一声。帮帮这个可怜的人吧,帮助他计算一下,他可以收到至少一份offer的最大概率。(如果Speakless选择了多个学校,得到任意一个学校的offer都可以)。
 

Input
输入有若干组数据,每组数据的第一行有两个正整数n,m(0<=n<=10000,0<=m<=10000)
后面的m行,每行都有两个数据ai(整型),bi(实型)分别表示第i个学校的申请费用和可能拿到offer的概率。
输入的最后有两个0。
 

Output
每组数据都对应一个输出,表示Speakless可能得到至少一份offer的最大概率。用百分数表示,精确到小数点后一位。
 

Sample Input
10 34 0.14 0.25 0.30 0
 

Sample Output
44.0%
Hint
You should use printf("%%") to print a '%'.
这道题关键要想清楚怎么快速计算这个录取的概率,也就相当于是"1-不被录取的概率"

先写上我的代码:

<span style="font-size:24px;"><span style="color:#333333;">#include<stdio.h>#include<algorithm>using namespace std;int n, m;int money[11111];float rate[11111];float dp[11111];int main(){int i, j;while(scanf("%d%d",&n,&m) && ( n != 0 || m != 0 )){memset(money,0,sizeof(money));memset(rate,0,sizeof(rate));for( i = 0; i <= n; i++ )dp[i] = 1;if( m == 0 ){printf("0.0%%\n");continue;}for( i = 1; i <= m; i++ ){scanf("%d%f",&money[i],&rate[i]);    rate[i] = 1 - rate[i];}for( i = 1; i <= m; i++ ){for( j = n; j >= money[i]; j-- ){dp[j] = min(dp[j], dp[j-money[i]] * rate[i]);}}printf("%.1f%%\n",(1 - dp[n]) * 100);}return 0;}</span>
<span style="font-size:24px;">很多细节需要注意,我老粗枝大叶的,暴露了很多问题:</span>
<span style="font-size:24px; font-family: Arial, Helvetica, sans-serif;">1、如m==0这种情况,想当然地写了个continue就完事了,忘记这种情况也要输出0.0%</span>
<span style="font-size:24px;">2、对dp的初始化,我想当然地用了memset(dp,1,sizeof(dp),以至于很长时间没明白,顺带补充一下memset的用法:</span>
<span style="font-size:24px;">memset是以字节为单位,初始化内存块。<span style="font-family: Arial, Helvetica, sans-serif;">所以初始化char类型的数组元素时,没有任何毛病,因为每个元素都占1个字节。</span>
<span style="font-size:24px;">memset针对数组,只能用-1,0赋值,用1会出现16843009这个奇葩的数字。</span>
<span style="font-size:24px;">还有关于指针呀,虚函数的一些注意,想再了解的参考这里:</span>
<span style="font-size:24px;"><a target=_blank href="http://http://blog.csdn.net/my_business/article/details/40537653">http://blog.csdn.net/my_business/article/details/40537653</a></span>
<span style="font-size:24px;">所以我只好认怂,用for循环一个个地初始化。</span>
<span style="font-size:24px;">3、一开始我很头疼不知道怎么表示概率这个东西,因为之前都是累加的形式,脑子都僵化了,所以其实就是加号变个乘号,原理不变</span>
<span style="font-size:24px;">三、杭电2955  又是一道注意精度的题和上一道题对比着来吧</span>
<span style="font-size:24px;">传送门:<a target=_blank href="http://http://acm.hdu.edu.cn/showproblem.php?pid=2955">http://acm.hdu.edu.cn/showproblem.php?pid=2955</a></span>
Problem Description
The aspiring Roy the Robber has seen a lot of American movies, and knows that the bad guys usually gets caught in the end, often because they become too greedy. He has decided to work in the lucrative business of bank robbery only for a short while, before retiring to a comfortable job at a university.


For a few months now, Roy has been assessing the security of various banks and the amount of cash they hold. He wants to make a calculated risk, and grab as much money as possible.


His mother, Ola, has decided upon a tolerable probability of getting caught. She feels that he is safe enough if the banks he robs together give a probability less than this.
 

Input
The first line of input gives T, the number of cases. For each scenario, the first line of input gives a floating point number P, the probability Roy needs to be below, and an integer N, the number of banks he has plans for. Then follow N lines, where line j gives an integer Mj and a floating point number Pj .
Bank j contains Mj millions, and the probability of getting caught from robbing it is Pj .
 

Output
For each test case, output a line with the maximum number of millions he can expect to get while the probability of getting caught is less than the limit set.

Notes and Constraints
0 < T <= 100
0.0 <= P <= 1.0
0 < N <= 100
0 < Mj <= 100
0.0 <= Pj <= 1.0
A bank goes bankrupt if it is robbed, and you may assume that all probabilities are independent as the police have very low funds.
 

Sample Input
30.04 31 0.022 0.033 0.050.06 32 0.032 0.033 0.050.10 31 0.032 0.023 0.05
 

Sample Output
246


大体解释下题意:一个人想抢银行,抢银行的测试样例有T个,这是我们一开始要输入的,然后要输入P和N,P是一个安全预期值,代表着失败几率,只要抢银行的总失败概率不大于这个,就是可以抢的,另外N代表有几个抢银行的方案,后面的N行就是每一个方案中要抢的银行能抢到的钱数和失败概率。我们要计算在成功的情况下抢到的钱最多可以是多少。

思路还是01背包的思路,我们仍要记得转化成“1-失败的概率”

下面贴上代码:

#include<stdio.h>
#include<algorithm>
using namespace std;
int T, N;
float P;
int M[1111];
float p[1111];
float f[11111];
int sum;
int main(){
int i, j;
scanf("%d",&T);
while(T--){
sum = 0;
f[0] = 1.0;
scanf("%f%d",&P,&N);
P = 1 - P;
for( i = 1; i <= N; i++ ){
scanf("%d%f",&M[i],&p[i]);
   p[i] = 1 - p[i];
sum += M[i];
}
for( i = 1; i <= N; i++ ){
for( j = sum; j >= M[i]; j-- ){
f[j] = max(f[j], f[j-M[i]] * p[i]);
}
}
for( i = sum; i >= 0; i-- ){
if( f[i] - P > 0.00000001 ){
printf("%d\n",i);
break;
}
}
}
return 0;
}

就在写这篇博客之前,我又做了一遍,出了点小插曲,半天AC不了,不得其解,对比了原来的代码才发现,最后一个for循环中,我将 "i >=0;"写成了"i>0",想了一下明白了,因为有一种情况就是每家银行的失败率都高于承受值,所以一家都抢不了,这时就应该输出0,而我如果不写“=”,就会什么都不输出。


四、这是一道有一定限制条件的01背包,很经典的题型:杭电2546

传送门:http://acm.hdu.edu.cn/showproblem.php?pid=2546

Problem Description
电子科大本部食堂的饭卡有一种很诡异的设计,即在购买之前判断余额。如果购买一个商品之前,卡上的剩余金额大于或等于5元,就一定可以购买成功(即使购买后卡上余额为负),否则无法购买(即使金额足够)。所以大家都希望尽量使卡上的余额最少。
某天,食堂中有n种菜出售,每种菜可购买一次。已知每种菜的价格以及卡上的余额,问最少可使卡上的余额为多少。
 

Input
多组数据。对于每组数据:
第一行为正整数n,表示菜的数量。n<=1000。
第二行包括n个正整数,表示每种菜的价格。价格不超过50。
第三行包括一个正整数m,表示卡上的余额。m<=1000。

n=0表示数据结束。
 

Output
对于每组输入,输出一行,包含一个整数,表示卡上可能的最小余额。
 

Sample Input
1505101 2 3 2 1 1 2 3 2 1500
 

Sample Output
-4532

这道题关键让人伤脑筋的是那5块钱怎么处理,我们这么想:让那5块钱的价值体现最大化,就能最大程度地让余额变小,所以我们先设定:用这5块买最贵的菜,然后把这5块和最贵的菜都先不看,剩下的钱和菜进行01背包即可

下面是代码:

<span style="font-size:24px;">#include<stdio.h>#include<algorithm>using namespace std;int n;int p[1111];int m;int f[55555];int main(){int i, j;int MAX;while(scanf("%d",&n) && n){memset(f,0,sizeof(f));MAX = 0;for( i = 1; i <= n; i++ ){scanf("%d",&p[i]);}sort( p + 1, p + 1 + n );MAX = p[n];scanf("%d",&m);if( m < 5 ){printf("%d\n",m);continue;}else{m -= 5;for( i = 1; i < n; i++ ){for( j = m; j >= p[i]; j-- ){f[j] = max(f[j], f[j-p[i]] + p[i]);}}printf("%d\n",m + 5 - MAX - f[m]);}}return 0;}</span>


这个题第一次也没有AC,唉,因为只顾着让m-=5,就开始进行后面的讨论,必然忽略了m本身是否是可以-5的,所以要先判断m是不是小于5,小于5的话直接输出m。


五、这道题也是有限制条件的背包问题——杭电3466 因为一个问题,我真的蒙逼了很久很久

传送门:http://acm.hdu.edu.cn/showproblem.php?pid=3466

Problem Description
Recently, iSea went to an ancient country. For such a long time, it was the most wealthy and powerful kingdom in the world. As a result, the people in this country are still very proud even if their nation hasn’t been so wealthy any more.
The merchants were the most typical, each of them only sold exactly one item, the price was Pi, but they would refuse to make a trade with you if your money were less than Qi, and iSea evaluated every item a value Vi.
If he had M units of money, what’s the maximum value iSea could get?

 

Input
There are several test cases in the input.

Each test case begin with two integers N, M (1 ≤ N ≤ 500, 1 ≤ M ≤ 5000), indicating the items’ number and the initial money.
Then N lines follow, each line contains three numbers Pi, Qi and Vi (1 ≤ Pi ≤ Qi ≤ 100, 1 ≤ Vi ≤ 1000), their meaning is in the description.

The input terminates by end of file marker.

 

Output
For each test case, output one integer, indicating maximum value iSea could get.

 

Sample Input
2 1010 15 105 10 53 105 10 53 5 62 7 3
 

Sample Output
511

先贴上代码:

#include<stdio.h>
#include<algorithm>
using namespace std;
int N, M;
struct node{
  int P, Q, V;
}a[555];

int cmp(node a, node b){
return a.Q - a.P < b.Q - b.P;
}//这个是最最关键的,但是看了网上很多的代码感觉都没有解释得很清楚,理所当然地就说这样是对的,唉,让我这心是空落落的,我想了一个半个下午都没想得很透彻,不过现在想明白了,这是一个折中的理解办法,也算相当于逆推了,我们假设,我们要先买物品1,再买物品2,对于物品1计算的时候必然有这个式子:

                        for( j = M; j >= a[1].Q; j-- ){
f[j] = max(f[j],f[j-a[1].P] + a[1].V);
}

所以我们可以认为,[a[1].Q,M]这个区间的值,都是已经有状态,也就是已经有值了的区间,而为了让计算的最终结果合理,我们应该做的就是让每一次的状态更新的区间,都要比前一次小,或者说,比前一次的区间左端点,更接近M一些,这样我们才能保证每一次更新的值都有据可依,才能保证每一次状态更新都有前一次更新的值和状态作为前提,所以,在买物品2的时候,首先有代码:

                     for( j = M; j >= a[2].Q; j-- ){
f[j] = max(f[j],f[j-a[2].P] + a[2].V);
}

所以我们可以知道,在买物品2时,状态更新时所依据的状态区间,实际上是[j(min)-a[2].P,j(max)-a[2].P],在这里j(min)就是a[2].Q,j(max)就是M,所以也就是[a[2].Q-a[2].P,M-a[2].P],而买物品1更新的区间是[a[1].Q,M],所以为了充分利用这一区间,我们应该保证a[2].Q-a[2].P > a[1].Q,到这里式子已经和原式有点相像了,我们继续思考,我们认为a[1].Q就是左极限了,为什么a[1].Q - a[1].P才是物品2状态更新依据的“最左极限”呢,因为物品1购买的时候,其实就是用的[a[1].Q-a[1].P,M-a[1].P]这一区间来更新自己的状态的,而[a[1].Q-a[1].P,a[1].Q]这一区间,其实本来就没有更新变化过,(物品1更新的区间:[a[1].Q,M])所以我们的物品2的左极限,是可以延伸到a[1].Q-a[1].P的,这只是证明了充分性,而必要性,则是因为,我们应该让此前涉及到的区间,都派上用场罢
int f[5555];
int main(){
int i, j;
while(scanf("%d%d",&N,&M)!=EOF){
memset(f,0,sizeof(f));
for( i = 1; i <= N; i++ )
scanf("%d%d%d",&a[i].P,&a[i].Q,&a[i].V);
   sort(a+1, a+1+N, cmp);
for( i = 1; i <= N; i++ ){
for( j = M; j >= a[i].Q; j-- ){
f[j] = max(f[j],f[j-a[i].P] + a[i].V);
}
}
printf("%d\n",f[M]);
}
return 0;
}


六、还是一个有特殊限制性条件的01背包问题,杭电1864

传送门:http://acm.hdu.edu.cn/showproblem.php?pid=1864

题外话:像我一样的初学者,希望能认真反思一下这几种经典的限制条件,便于灵活变通,举一反三。

Problem Description
现有一笔经费可以报销一定额度的发票。允许报销的发票类型包括买图书(A类)、文具(B类)、差旅(C类),要求每张发票的总额不得超过1000元,每张发票上,单项物品的价值不得超过600元。现请你编写程序,在给出的一堆发票中找出可以报销的、不超过给定额度的最大报销额。
 

Input
测试输入包含若干测试用例。每个测试用例的第1行包含两个正数 Q 和 N,其中 Q 是给定的报销额度,N(<=30)是发票张数。随后是 N 行输入,每行的格式为:
m Type_1:price_1 Type_2:price_2 ... Type_m:price_m
其中正整数 m 是这张发票上所开物品的件数,Type_i 和 price_i 是第 i 项物品的种类和价值。物品种类用一个大写英文字母表示。当N为0时,全部输入结束,相应的结果不要输出。
 

Output
对每个测试用例输出1行,即可以报销的最大数额,精确到小数点后2位。
 

Sample Input
200.00 32 A:23.50 B:100.001 C:650.003 A:59.99 A:120.00 X:10.001200.00 22 B:600.00 A:400.001 C:200.501200.50 32 B:600.00 A:400.001 C:200.501 A:100.00100.00 0
 

Sample Output
123.501000.001200.50

<span style="font-size:24px;">#include<stdio.h>#include<algorithm>#include<string.h>int N;float Q;int p;//保存扩大100倍后的Qstruct node{float value[1111];char type[1111];int m;}a[33];float f[3333333];node output[33];using namespace std;int main(){int i, j, k;float A, B, C;int flag;float sum;int sum1[33];while (scanf("%f%d", &Q, &N) && N){k = 0;p = Q * 100;memset(f, 0, sizeof(f));for (i = 1; i <= N; i++){scanf("%d", &a[i].m);for (j = 1; j <= a[i].m; j++){scanf(" %c:%f", &a[i].type[j], &a[i].value[j]);}}for (i = 1; i <= N; i++){flag = 1;A = B = C = 0;for (j = 1; j <= a[i].m; j++){if (a[i].type[j] == 'A'){A += a[i].value[j];}else if (a[i].type[j] == 'B'){B += a[i].value[j];}else if (a[i].type[j] == 'C'){C += a[i].value[j];}elseflag = 0;  }sum = A + B + C;if (A <= 600 && B <= 600 && C <= 600 && flag && sum <= 1000){output[++k] = a[i];sum1[k] = sum * 100;}}for (i = 1; i <= k; i++){for (j = p; j >= sum1[i]; j--){f[j] = max(f[j], f[j - sum1[i]] + sum1[i]);}}printf("%.2f\n", f[p] / 100);}return 0;}</span>

注意:1、题目中所说的单项物品价格不能超过600元,实际是说A,B,C每类物品的价格总和不能超过600,而不是每一件物品的价格都不超过600就可以了

    2、一直在改的一个小错误,就是把dp数组开得太小,应该认真去估算一下每次报销的最大可能值才好。

七、这是一个分东西的问题,后面还有一道类似的,先写一下这道01背包的:杭电1171

传送门:http://acm.hdu.edu.cn/showproblem.php?pid=1171

Problem Description
Nowadays, we all know that Computer College is the biggest department in HDU. But, maybe you don't know that Computer College had ever been split into Computer College and Software College in 2002.
The splitting is absolutely a big event in HDU! At the same time, it is a trouble thing too. All facilities must go halves. First, all facilities are assessed, and two facilities are thought to be same if they have the same value. It is assumed that there is N (0<N<1000) kinds of facilities (different value, different kinds).
 

Input
Input contains multiple test cases. Each test case starts with a number N (0 < N <= 50 -- the total number of different facilities). The next N lines contain an integer V (0<V<=50 --value of facility) and an integer M (0<M<=100 --corresponding number of the facilities) each. You can assume that all V are different.
A test case starting with a negative integer terminates input and this test case is not to be processed.
 

Output
For each case, print one line containing two integers A and B which denote the value of Computer College and Software College will get respectively. A and B should be as equal as possible. At the same time, you should guarantee that A is not less than B.
 

Sample Input
210 120 1310 1 20 230 1-1
 

Sample Output
20 1040 40

     这道题的难点应该就在于A和B分到的物品的价值量不一样,还要尽量均等,其实,我们仍可以先算出总价值量,然后除以2,如果是奇数,也会使得结果小于实际的价值,我们对其中的一份进行01背包,就可以求出一个小于等于sum/2的一个结果,同时我们也就成功地划分好了。

<span style="font-size:24px;">#include<stdio.h>#include<algorithm>using namespace std;int N;int V[5555];int f[255555];int v;int main(){int sum, A, B;int temp;int M[5555];int i, j, l;while (scanf("%d", &N) && N > 0){sum = 0;l = 0;memset(f, 0, sizeof(f));memset(V, 0, sizeof(V));for (i = 1; i <= N; i++){scanf("%d%d",&v, &M[i]);sum += v * M[i];while (M[i]--){V[++l] = v;}}for (i = 1; i <= l; i++){for (j = sum/2; j >= V[i]; j--){f[j] = max(f[j], f[j - V[i]] + V[i]);}}printf("%d %d\n", sum - f[sum/2], f[sum/2]);}return 0;}</span>

以上就是我学习01背包的入门训练,希望对初学者们能有一丝的帮助。

0 0
原创粉丝点击