暑假训练,算是正式步入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
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 ?
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.
1 5 10 1 2 3 4 5 5 4 3 2 1
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后面那个"?"是啥意思,有知道的嘛?哎呀强迫症犯了)
后面的m行,每行都有两个数据ai(整型),bi(实型)分别表示第i个学校的申请费用和可能拿到offer的概率。
输入的最后有两个0。
10 3 4 0.1 4 0.2 5 0.3 0 0
44.0%HintYou should use printf("%%") to print a '%'.
先写上我的代码:
<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>
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.
Bank j contains Mj millions, and the probability of getting caught from robbing it is Pj .
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.
3 0.04 3 1 0.02 2 0.03 3 0.05 0.06 3 2 0.03 2 0.03 3 0.05 0.10 3 1 0.03 2 0.02 3 0.05
2 4 6
大体解释下题意:一个人想抢银行,抢银行的测试样例有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
某天,食堂中有n种菜出售,每种菜可购买一次。已知每种菜的价格以及卡上的余额,问最少可使卡上的余额为多少。
第一行为正整数n,表示菜的数量。n<=1000。
第二行包括n个正整数,表示每种菜的价格。价格不超过50。
第三行包括一个正整数m,表示卡上的余额。m<=1000。
n=0表示数据结束。
1 50 5 10 1 2 3 2 1 1 2 3 2 1 50 0
-45 32
这道题关键让人伤脑筋的是那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
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?
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.
2 10 10 15 10 5 10 5 3 10 5 10 5 3 5 6 2 7 3
5 11
先贴上代码:
#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
题外话:像我一样的初学者,希望能认真反思一下这几种经典的限制条件,便于灵活变通,举一反三。
m Type_1:price_1 Type_2:price_2 ... Type_m:price_m
其中正整数 m 是这张发票上所开物品的件数,Type_i 和 price_i 是第 i 项物品的种类和价值。物品种类用一个大写英文字母表示。当N为0时,全部输入结束,相应的结果不要输出。
200.00 3 2 A:23.50 B:100.00 1 C:650.00 3 A:59.99 A:120.00 X:10.00 1200.00 2 2 B:600.00 A:400.00 1 C:200.50 1200.50 3 2 B:600.00 A:400.00 1 C:200.50 1 A:100.00 100.00 0
123.50 1000.00 1200.50
<span style="font-size:24px;">#include<stdio.h>
#include<algorithm>
#include<string.h>
int N;
float Q;
int p;//保存扩大100倍后的Q
struct 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];
}
else
flag = 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
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).
A test case starting with a negative integer terminates input and this test case is not to be processed.
2 10 1 20 1 3 10 1 20 2 30 1 -1
20 10 40 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背包的入门训练,希望对初学者们能有一丝的帮助。