背包系列专题讲义之0-1背包问题

背包系列专题讲义

 

专题一:0-1背包问题

/*

       Name:0-1背包问题

       Author:巧若拙

       Description:0-1背包问题:在n种物品中选取若干件(同一种物品最多选一次)放在容量为c的背包里,分别用P[i]和W[i]存储第i种物品的价值和重量。

求解怎么装物品可使背包里物品总价值最大。

样例输入

4 12

2 3

5 7

6 8

10 12

样例输出

15

*/

#include<iostream>

#include<cstring>

using namespace std;

 

const int MAXC = 10000; //背包最大容量

const int MAXN = 2000; //物品的最大个数

int W[MAXN+1];//物品的重量

int P[MAXN+1];//物品的价值

int B2[MAXN+1][MAXC+1]; //备忘录,记录给定n个物品装入容量为c的背包的最大价值

int B3[MAXN+1][MAXC+1]; //动态规划中记录给定i个物品装入容量为j的背包的最大价值

int pre[MAXC+1]; //记录上一行元素值

int cur[MAXC+1]; //记录当前行元素值

int F[MAXC+1]; //动态规划中记录给定n个物品装入容量为j的背包的最大价值

int cw, cp;//在回溯算法中分别记录给定t个物品时,已装物品的总重量和总价值

int bestPrice; //在回溯算法中记录目前已知的最大总价值,初始化为0

 

int Sum(int n, int t); //累计第t+1到n个物品的总价值

void ZeroOnePack_1(int n, int c, int t);//回溯算法求0-1背包问题

int ZeroOnePack_2(int n, int c);//记忆化搜索(备忘录算法)求0-1背包问题

int ZeroOnePack_3(int n, int c);//动态规划:二维数组存储记录

int ZeroOnePack_4(int n, int c);//优化的动态规划算法,使用2个一维数组代替二维数组

int ZeroOnePack_5(int n, int c);//优化的动态规划算法,一维数组存储记录

 

int main()

{

       intn, c;

       cin>> n >> c;

      

       for(int i=1; i<=n; i++)//不计下标为0的元素

       {

              cin>> W[i] >> P[i];

       }

      

       //算法1:回溯算法

       ZeroOnePack_1(n,c, 1); //不计下标为0的元素

       cout<< bestPrice << endl;

      

       //算法2:记忆化搜索(备忘录算法)

       memset(B2,-1, sizeof(B2)); //先初始化B2的值全为-1

       cout<< ZeroOnePack_2(n, c) << endl;

      

       //算法3:动态规划:二维数组存储记录

       cout<< ZeroOnePack_3(n, c) << endl;

      

       //算法4:动态规划:使用2个一维数组存储记录

       cout<< ZeroOnePack_4(n, c) << endl;

      

       //算法5:优化的动态规划算法,一维数组存储记录

       cout<< ZeroOnePack_5(n, c) << endl;

      

       return0;

}

 

算法1:回溯算法,需要用到全局变量cw, cp, bestPrice,W[], P[]。

void ZeroOnePack_1(int n, int c, int t)//回溯算法求0-1背包问题

{

    if(t == n+1) //语句1

    {

       bestPrice = max(cp, bestPrice);

    }

   else

   { 

             if (cw+W[t] <= c) //语句2

             {

                     cw+= W[t]; cp += P[t];

                     ZeroOnePack_1(n,c, t+1);

                     cw-= W[t]; cp -= P[t];

           }    

              if(cp+Sum(n, t) > bestPrice) //语句3

                     ZeroOnePack_1(n,c, t+1);

    }

}

 

int Sum(int n, int t) //累计第t+1到n个物品的总价值

{

      int s = 0;

   for (int i=t+1; i<=n; i++)

       {

             s +=P[i];

       }

   return s;

}

 

问题1:能否把语句1改为if (t == n)?为什么?

问题2:语句2和语句3哪个语句是处理装入第t个物品的情形的?

问题3:语句3实现剪枝功能,请问它减掉了哪些不满足条件的情形?

问题4:是否有必要把语句2改为:if (cw+W[t] <= c &&cp+Sum(n, t-1) > bestPrice)?

为什么?

 

参考答案:

问题1:不能修改,因为递归出口是t == n+1,此时所有物品处理完毕。而t == n时,表示正在处理第n个物品。

问题2:语句2和语句3分别表示装和不装第t个物品。

问题3:语句3剪掉了在不装第t个物品的前提下,把第t+1到n个物品全部装入背包时,仍不能得到更优解的情形。

问题4:没有必要。因为如果不满足(cp+Sum(n, t-1) >bestPrice)条件的话,在上一层就被剪枝了;既然能进入这一层,说明肯定有可能得到更优解,我们只需要判断装入第t个物品时是否超载就行了。

 

算法2:记忆化搜索(备忘录算法),需要用到全局变量W[], P[],另有B2[n][c]初始化为-1。

int ZeroOnePack_2(int n, int c)//记忆化搜索(备忘录算法)求0-1背包问题

{

      if (B2[n][c] != -1)  //语句1

              return B2[n][c];

     

       intbestP = 0;

       if(n == 1)

       {

              bestP= (c >= W[n]) ? P[n] : 0;

       }

       else//语句2

       {

              if(c < W[n])//若装不下,则不装第n个物品

                     bestP= ZeroOnePack_2(n-1, c);

              else//如果装得下,从装和不装两者中取最大值

                  bestP = max(ZeroOnePack_2(n-1, c),ZeroOnePack_2(n-1, c-W[n]) + P[n]);

       }

      

   return B2[n][c] = bestP;//做备忘录

}

 

问题1:能否将B2[n][c]初始化为0,并将语句1改为if (B2[n][c]!= 0)?为什么?

问题2:能否将语句2之后的语句块改写为:

{

              bestP= ZeroOnePack_2 (n-1, c); //先计算不装第n个物品的情形

              if(c >= W[n])//如果装得下,从装和不装两者中去最大值

              {

                  bestP = max(bestP, ZeroOnePack_2 (n-1,c-W[n])+P[n]);

              }

       }

你更喜欢哪种写法?为什么?

 

参考答案:

问题1:不能。题目的做法虽然也能得到正确结果,但是降低了效率,因为(B2[n][c]== 0)也是解之一,不把这些解记录下来,会造成重复的递归运算。

问题2:可以。两者是等效的,个人感觉算法2中的写法逻辑更清晰。

 

算法3:动态规划:二维数组存储记录,需要用到全局变量W[], P[], 另有B3[MAXN+1][]默认初始化为0。

int ZeroOnePack_3(int n, int c)//动态规划:二维数组存储记录,B3[i][j]初始化为0

{

       //记录前i(1<=i<n)个物品装入容量为1-c的背包的最大价值

      for (int i=1; i<n; i++)//语句1

       {

              for(int j=1; j<W[i]; j++)//语句2

                     B3[i][j]= B3[i-1][j];

              for(int j=W[i]; j<=c; j++)//语句3

                     B3[i][j]= max(B3[i-1][j], B3[i-1][j-W[i]] + P[i]);

       }

       //语句块4:单独处理第n个物品,直接计算B[n][c]

       if(c < W[n])

              B3[n][c]= B3[n-1][c];

       else

              B3[n][c]= max(B3[n-1][c], B3[n-1][c-W[n]]+P[n]);

      

       returnB3[n][c];

}

 

问题1:语句2和语句3把j的值分成了两个区间来处理,这样做有什么好处?如果想把它合成为一个for循环,循环体内的代码该如何编写?

问题2:语句块4,单独处理了第n个物品,直接计算B[n][c],这样做有什么好处?能否去掉语句块4,而把语句1改为:for (int i=1; i<=n; i++)?

你更喜欢哪种写法?为什么?

 

参考答案:

问题1:这样分区间处理,逻辑更清晰,而且无需重复判断j的范围,提高了效率。

若合成为一个for循环,可以这样写:

       for(int j=1; j<=c; j++)

       {

              B3[i][j]= B3[i-1][j]; //先分析不装第i个物品的情形

              if(j >= W[i] && B3[i-1][j] < B3[i-1][j-W[i]] + P[i])

                     B3[i][j]= B3[i-1][j-W[i]] + P[i];

       }

问题2:因为每个物品最多只能装一次,故背包不一定能装满,则对于给定的n个物品来说,B[n][c]==B[n][j],其中W[n]<=j<=c,所以对第n个物品来说,只需直接计算B[n][c],而不用考虑其他的容量j,这样可以减少计算量。

       两者写法各有千秋,后者代码简洁,前者效率更高。

 

算法4:动态规划:使用2个一维数组存储记录,需要用到全局变量W[], P[], 另有pre[]和cur[]均初始化为0。

int ZeroOnePack_4(int n, int c)//优化的动态规划算法,使用2个一维数组代替二维数组

{

      for (int i=1; i<=n; i++) //语句1

       {

              for(int j=1; j<=c; j++) //语句2

              {

                     if(j < W[i] || pre[j] > pre[j-W[i]] + P[i])

                            cur[j]= pre[j];

                     else

                            cur[j]= pre[j-W[i]] + P[i];

              }

              for(int j=1; j<=c; j++) //语句3

              {

                     pre[j]= cur[j];

              }

       }

 

       returncur[c];

}

 

问题1:语句1能否改为:for (int i=n; i>0; i--) ?为什么?

问题2:语句2和语句3能否改为:for (intj=c; j>0; j--) ?为什么?

问题3:和算法3作比较,ZeroOnePack_4()中的pre[j]和cur[j]分别相当于ZeroOnePack_3()中的哪两个变量?

问题4:为减少计算量,我们可以模仿算法3中的做法,单独处理第n个物品,如果这样做该如何修改ZeroOnePack_4()?

 

参考答案:

问题1:不能。因为这是自底而上的动态规划算法,需要先求出给定物品较少的情形,再逐渐增加给定物品的数量,并利用前面计算出的结果来求出后面的值。

问题2:可以。因为0-1背包问题是利用上一行的结果pre[]来求当前行的值cur[],而且在计算cur[j]的时候pre[]没有被修改,所以列坐标j递增或递减均可。

问题3:pre[j]相当于ZeroOnePack_3()中的B3[i-1][j],cur[j]相当于B3[i][j]。

问题4:先把语句1改为:for (int i=1; i<n; i++)。

       然后在return cur[c];语句前增加如下代码:

    //第n个物品只需考虑容量为c的一种情况

       cur[c]= pre[c]; //先默认为不装第n个物品

       if(c >= W[n] && pre[c] < pre[c-W[n]] + P[n]) //如果容量足够,且能获得更优解

              cur[c]= pre[c-W[n]] + P[n];

 

算法5:优化的动态规划算法,一维数组存储记录,需要用到全局变量W[],P[], 另有F[]初始化为0。

int ZeroOnePack_5(int n, int c)//优化的动态规划算法,一维数组存储记录,F[j]初始化为0 

{

      for (int i=1; i<=n; i++)  //语句1

       {

              for(int j=c; j>=W[i]; j--)  //语句2

              {//当(j < W[i] || F[j] > F[j-W[i]]+ P[i])时,F[j]的值不变

                     if(F[j] < F[j-W[i]] + P[i])

                            F[j]= F[j-W[i]] + P[i];

              }

       }

      

       returnF[c];

}

 

问题1:语句1能否改为:for (int i=n; i>0; i--) ?为什么?

问题2: 语句2只考虑了j>=W[i]的情形,好像有遗漏,是否需要更正为:for (int j=c;j>0; j--)?

问题3:语句2能否改为:for (int j=W[i]; j<=c; j++)?为什么?

 

参考答案:

问题1:不能。因为这是自底而上的动态规划算法,需要先求出给定物品较少的情形,再逐渐增加给定物品的数量,并利用前面计算出的结果来求出后面的值。

问题2:没必要。因为当(j < W[i] || F[j] >F[j-W[i]] + P[i])时,F[j]的值不变。

问题3:不能。我们与用二维数组记录结果的算法做比较:

在二维数组算法中B[i][j] = max(B[i-1][j], B[i-1][j-W[i]] + P[i]),即第i行第j列的元素,由第i-1行的元素决定,且列坐标j大的元素由j小的元素决定。由于我们记录了B[i-1][]的值,故在求B[i][j]时,列坐标j递增或递减均可。

若我们用一维数组F[j]代替B[i][j],则只记录了列坐标j,未记录行坐标i,在同一行中,必须先求出列坐标j较大的元素,再求j较小的元素。这样先改变的是下标j较大的元素,且其不会影响j小的元素。故在内层循环中,应该让循环变量j的值从大到小递减。

 

课后练习:

练习1:1775_采药

描述:辰辰是个很有潜能、天资聪颖的孩子,他的梦想是称为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。

医师为了判断他的资质,给他出了一个难题。医师把他带到个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是辰辰,你能完成这个任务吗?

输入

输入的第一行有两个整数T(1 <= T<= 1000)和M(1 <= M <= 100),T代表总共能够用来采药的时间,M代表山洞里的草药的数目。

接下来的M行每行包括两个在1到100之间(包括1和100)的的整数,分别表示采摘某株草药的时间和这株草药的价值。

输出

输出只包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。

样例输入

70 3

71 100

69 1

1 2

样例输出

3

 

练习2:8785_装箱问题

描述:有一个箱子容量为V(正整数,0<=V<=20000),同时有n个物品(0<n<=30),每个物品有一个体积(正整数)。

要求n个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。

输入

一个整数v,表示箱子容量

一个整数n,表示有n个物品

接下来n个整数,分别表示这n 个物品的各自体积

输出

一个整数,表示箱子剩余空间。

样例输入

24

6

8 3 12 7 9 7

样例输出

0

 

练习3:2985_数字组合

描述有n个正整数,找出其中和为t(t也是正整数)的可能的组合方式。

如:n=5,5个数分别为1,2,3,4,5,t=5;

那么可能的组合有5=1+4和5=2+3和5=5三种组合方式。

输入

输入的第一行是两个正整数n和t,用空格隔开,其中1<=n<=20,表示正整数的个数,t为要求的和(1<=t<=1000)

接下来的一行是n个正整数,用空格隔开。

输出

和为t的不同的组合方式的数目。

样例输入

5 5

1 2 3 4 5

样例输出

3

 

提示:这是一个典型的0-1背包问题,只不过不是求最优解,而是求所有可能的组合,故需要累计所有的组合。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值