动态规划-背包问题(01背包、完全背包、多重背包)

Source


背包问题:有多个重量不同、价值不同的物品,以及一个容量有限的背包,选择一些物品装入背包,求最大总价值。
背包问题无法用贪心求最优解,是典型的动态规划问题。背包问题还可以分成3种:① 0-1背包、② 完全背包、③ 多重背包。

区别
0/1背包 每种物品是一件
完全背包 每种物品是无限件
多重背包 每种物品是有限件

0/1背包


0/1背包顾名思义就是0和1两种状态,即每个物品装入和不装入背包这两种状态,如果不懂dp的话,可能用dfs的做法深搜,但时间复杂度是2n,肯定是不能接受的,或者用贪心,但也并不能得到全局最优解。下面举例说明dp。

原理

假设4个物品,重量分别是w={2,3,6,5},价值分别是v={6,3,5,4},背包容量为9。
引进二维表dp[][],dp[i][j]表示考虑前i个物品装入容量为j的背包中获得的最大价值。可以把每个dp[i][j]都看成一个背包。

步骤一:只装第1个物品
因为物品1的重量是2,所以容量小于2的背包都放不进去(即dp[1][0]=dp[1][1]=0),在容量2时装入,其价值是物品1的价值(即dp[1][2]=6),容量大于2的背包,多余的容量也用不到(因为现在只考虑物品1),其价值也都是6,如下图

0 1 2 3 4 5 6 7 8 9
0 0 0 0 0 0 0 0 0 0 0
1 0 0 6 6 6 6 6 6 6 6

步骤二:只装前2个物品
首先,物品2重量是3,背包容量3的情况和只装物品1情况一样。下面从dp[2][3]开始填,此时我们可以选择装物品2,也可以不装。
如果装,那么相当于把dp[i-1][j-3]的最大价值加上物品2的价值3,即考虑只第一个物品的时候,腾出3容量放物品2,此时价值=3;
在这里插入图片描述

如果不装,那么相当于只把物品1装入,此时价值=6;
在这里插入图片描述
我们取两者最大值,即dp[2][3]=max(3,6)=6。
后面的多余容量同理。
在这里插入图片描述

三.重复上述步骤,得到dp数组

0 1 2 3 4 5 6 7 8 9
0 0 0 0 0 0 0 0 0 0 0
1 0 0 6 6 6 6 6 6 6 6
2 0 0 6 6 6 9 9 9 9 9
3 0 0 6 6 6 9 9 9 11 11
4 0 0 6 6 6 9 9 10 11 11

最后答案是dp[4][9],即把4个物品装入容量为9的背包,最大价值是11。

输出方案

现在回过头看装了哪些物品:
dp[4][9]=max(dp[3][4]+4,dp[3][9])=dp[3][9],说明没有装物品4;
dp[3][9]=max(dp[2][3]+5,dp[2][9])=dp[2][3]+5,说明装了物品3;
以此类推,如图箭头所示,即装了物品1和3,输出方案即可。
在这里插入图片描述
除了这样倒推外,也可以定义数组path[][],path[i][j]表示状态j下物品i是否被选中,初始化为0,置1表示选中,当更新dp的时候同时维护即可。

例题HDU-2602

HDU-2602 Bone Collector
说了这么多可能有点迷,看下例题和代码可能更易理解。

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
1
5 10
1 2 3 4 5
5 4 3 2 1
Sample Output
14

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 1003;
int t, n, m;
int dp[maxn][maxn], v[maxn], w[maxn], path[maxn][maxn];
int main() {
    
      
    scanf("%d", &t);
    while (t--) {
    
      
        memset(dp, 0, sizeof(dp));
        //memset(path, 0, sizeof(path));
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; i++)scanf("%d", &v[i]);
        for (int i = 1; i <= n; i++)scanf("%d", &w[i]);
        for (int i = 1; i <= n; i++)
            for (int j = 0; j <= m; j++)
                if (w[i] > j) //当前容量j小于物品i的重量,装不下
                    dp[i][j] = dp[i - 1][j];
                else {
    
        //可以装,取不装和装的最大值
                    dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
                    //if (dp[i - 1][j - w[i]] + v[i] > dp[i - 1][j])//记录路径
                    //    path[i][j] = 1;
                }
        printf("%d\n", dp[n][m]);
        /*for (int i = n, j = m; i >0 && j > 0;i--) {  //输出方案
            if (path[i][j]) {
                printf("%d ", i);
                j -= w[i];
            }
        }*/
    }
    return 0;
}

空间优化-滚动数组

如果数据很大的时候,我们无法定义这么大的二维表,那么就要考虑对空间复杂度进行优化。我们观察上述二维表dp[][],不难发现,每一行是从上一行算出来的,只与上一行有关系,与更前面的行没有关系。那么用新的一行覆盖原来的就好了(就是一行数组进行滚动),空间复杂度从O(NV)减少为O(V)。

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 1003;
int t, n, m;
int dp[maxn], v[maxn], w[maxn];
int main() {
    
      
    scanf("%d", &t);
    while (t--) {
    
      
        memset(dp, 0, sizeof(dp));
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; i++)scanf("%d", &v[i]);
        for (int i = 1; i <= n; i++)scanf("%d", &w[i]);
        for (int i = 1; i <= n; i++)
            for (int j = m; j >= w[i]; j--)
                dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
        printf("%d\n", dp[m]);
    }
    return 0;
}

注意j应该逆序循环,即从后面往前面覆盖,不然会多次考虑每件物品(恰恰是完全背包的解),但我们不能重复选择已经选择的物品。不过,滚动数组覆盖掉了中间转移状态,无法倒推输出方案,如果用path[][]记录就没有优化的意义了。
如果实在不理解为什么逆序的话(刚开始的确费劲 ),不妨就定义pre[]表示上一行:

		for (int i = 1; i <= n; i++) {
    
      
            for (int j = w[i]; j <= m; j++)
                dp[j] = max(pre[j], pre[j - w[i]] + v[i]);
            memcpy(pre, dp, sizeof(dp));
        }

如果memcpy卡超时,定义dp[2][n]交替即可。

完全背包


完全背包与0/1背包不同就是每种物品可以多次/无限选择,而0/1背包的每种物品至多只能选择一次。

转换为0/1背包

不难发现,完全背包其实可以转换成0/1背包,第i种物品的出现次数至多是背包容量m/物品i重量w[i],那么以此构建新的v[]和w[]即可,也可以再加一层循环表示物品个数,然后套用0/1背包。

二维

0/1背包转移方程是dp[i][j] = max(dp [i - 1] [j],dp[i - 1][j - w[i]] + v[i]),即决策dp[i][j](前i个物品,j容量时)是从前一行只考虑前i-1个物品的情况下为基础,避免重复选择。而完全背包可以多次选择第i个物品,所以是考虑前i个物品的情况下为基础来决策,即同一行,推出转移方程:dp[i][j] = max(dp [i] [j],dp[i - 1][j - w[i]] + v[i])

		for (int i = 1; i <= n; i++)
            for (int j = w[i]; j <= m; j++)
				dp[i][j] = max(dp[i][j],dp[i - 1][j - w[i]] + v[i])

一维

完全背包是顺序,顺序会覆盖以前的状态,即存在选择多次的情况。

		for (int i = 1; i <= n; i++)
            for (int j = w[i]; j <= m; j++)
                dp[j] = max(dp[j], dp[j - w[i]] + v[i]);

例题HDU-2159

HDU-2159 FATE

Problem Description
最近xhd正在玩一款叫做FATE的游戏,为了得到极品装备,xhd在不停的杀怪做任务。久而久之xhd开始对杀怪产生的厌恶感,但又不得不通过杀怪来升完这最后一级。现在的问题是,xhd升掉最后一级还需n的经验值,xhd还留有m的忍耐度,每杀一个怪xhd会得到相应的经验,并减掉相应的忍耐度。当忍耐度降到0或者0以下时,xhd就不会玩这游戏。xhd还说了他最多只杀s只怪。请问他能升掉这最后一级吗?
Input
输入数据有多组,对于每组数据第一行输入n,m,k,s(0 < n,m,k,s < 100)四个正整数。分别表示还需的经验值,保留的忍耐度,怪的种数和最多的杀怪数。接下来输入k行数据。每行数据输入两个正整数a,b(0 < a,b < 20);分别表示杀掉一只这种怪xhd会得到的经验值和会减掉的忍耐度。(每种怪都有无数个)
Output
输出升完这级还能保留的最大忍耐度,如果无法升完这级输出-1。
Sample Input
10 10 1 10
1 1
10 10 1 9
1 1
9 10 2 10
1 1
2 2
Sample Output
0
-1
1

忍耐度->容量,经验值->价值,同时再维护sum[]记录个数,输出达到价值n后的最大剩余容量。

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 102;
int n, m, k, s;
int v[maxn], w[maxn], dp[maxn], sum[maxn];
int main() {
    
      
    while (~scanf("%d%d%d%d", &n, &m, &k, &s)) {
    
      
        for (int i = 1; i <= k; i++)
            scanf("%d%d", &v[i], &w[i]);
        memset(dp, 0, sizeof(dp));
        memset(sum, 0, sizeof(sum));
        for(int i=1;i<=k;i++)
            for (int j = w[i]; j <= m; j++) {
    
      
                if (sum[j - w[i]] + 1 > s)continue;
                if (dp[j] < dp[j - w[i]] + v[i]) {
    
      
                    dp[j] = dp[j - w[i]] + v[i];
                    sum[j] = sum[j - w[i]] + 1;
                }
            }
        int ans = -1;
        for (int i = 1; i <= m; i++)
            if (dp[i] >= n) {
    
      
                ans = m - i;
                break;
            }
        printf("%d\n", ans);
    }
    return 0;
}

多重背包


多重背包比完全背包多了一个限制,即每种物品有个数限制,不再是无限。

转换为0/1背包

同样的,和完全背包一样进行转换,加循环或者重新构建即可,注意个数限制,可能超时

二进制拆分优化

仔细思考,不难发现我们做了大量重复性的工作,比如同种物品的不同个体,比如第1种物品有3个,编号a,b,c,那么选ab,ac,bc其实是等效的。我们可以通过「二进制分组」的方式优化拆分,一个数n可以拆分为k个数字,k=⌊log(n+1)⌋,复杂度为O(NMΣlogni)。

  • n=20 +21 +22 +…+2k-1+n−2k+1
  • 6=1+2+3
  • 13=1+2+4+6
  • 31=1+2+4+8+16
    由这k个数字可以组成1~n的所有数值,这样就能表示所有等效选择方式了,即n个物品拆分成log(n)个物品,然后愉快的套0/1背包就好了。

例题HDU-2844

HDU-2844 Coins

Problem Description
Whuacmers use coins.They have coins of value A1,A2,A3…An Silverland dollar. One day Hibix opened purse and found there were some coins. He decided to buy a very nice watch in a nearby shop. He wanted to pay the exact price(without change) and he known the price would not more than m.But he didn’t know the exact price of the watch.
You are to write a program which reads n,m,A1,A2,A3…An and C1,C2,C3…Cn corresponding to the number of Tony’s coins of value A1,A2,A3…An then calculate how many prices(form 1 to m) Tony can pay use these coins.
Input
The input contains several test cases. The first line of each test case contains two integers n(1 ≤ n ≤ 100),m(m ≤ 100000).The second line contains 2n integers, denoting A1,A2,A3…An,C1,C2,C3…Cn (1 ≤ Ai ≤ 100000,1 ≤ Ci ≤ 1000). The last test case is followed by two zeros.
Output
For each test case output the answer on a single line.
Sample Input
3 10
1 2 4 2 1 1
2 5
1 4 2 1
0 0
Sample Output
8
4

没有给重量,令重量=价值。

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 100005;
int n, m, cnt;
int value[102], num[102];
int dp[maxn], v[maxn];
int main() {
    
      
    while (~scanf("%d%d", &n, &m) && n && m) {
    
      
        memset(dp, 0, sizeof(dp));
        cnt = 0;
        for (int i = 1; i <= n; i++)scanf("%d", &value[i]);
        for (int i = 1; i <= n; i++) {
    
      
            scanf("%d", &num[i]);
            for (int j = 1; j <= num[i]; j *=2) {
    
        //二进制拆分优化
                v[++cnt] = value[i] * j;
                num[i] -= j;
            }
            if (num[i])v[++cnt] = value[i] * num[i];
        }
        for (int i = 1; i <= cnt; i++) 
            for (int j = m; j >= v[i]; j--)
                dp[j] = max(dp[j], dp[j - v[i]] + v[i]);
        int ans = 0;
        for (int i = 1; i <= m; i++)
            if (dp[i] == i)ans++;
        printf("%d\n", ans);
    }
    return 0;
}

单调队列优化

其实用二进制拆分就能AC绝大部分题了,但一些卡时间的题还是会TLE,用单调队列优化可以使复杂度降为O(NM)。
从状态转移方程入手,假设第i种物品重量w,价值v,数量num,背包容量m

  • dp[i][j]=max(dp[i−1][j−w∗k]+v∗k),k<=min(num,m/w)
    把当前容量j分成w组(0,1,…w-1),令d=j%w,s=j/w,推出
  • dp[i][j]=max(dp[i−1][d+w∗k]−v∗k)+v∗s
    每组互不影响,枚举余数d,对每个余数d都用单调队列优化。
    看代码好了,免得误人子弟
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 100005;
int n, m, cnt;
int v[102], num[102], dp[maxn];
struct Queue {
    
      
    int pos, val;
}q[maxn];
int main() {
    
      
    while (~scanf("%d%d", &n, &m) && n && m) {
    
      
        memset(dp, 0, sizeof(dp));
        cnt = 0;
        for (int i = 1; i <= n; i++)scanf("%d", &v[i]);
        for (int i = 1; i <= n; i++) {
    
      
            scanf("%d", &num[i]);
            if (v[i] * num[i] >= m) {
    
        //大于背包容量相当于完全背包
                for (int j = v[i]; j <= m; j++)
                    dp[j] = max(dp[j], dp[j - v[i]] + v[i]);
                continue; 
            }                       //单调队列优化
            for (int d = 0; d < v[i]; d++){
    
        //枚举余数
                int head = 1, rear = 0;
                for (int j = 0; j <= (m - d) / v[i]; j++){
    
      
                    int cur = dp[j * v[i] + d] - j * v[i];
                    while (head <= rear && q[head].pos < j - num[i]) head++;
                    while (head <= rear && q[rear].val < cur) rear--;
                    q[++rear].val = cur;
                    q[rear].pos = j;
                    dp[j * v[i] + d] = q[head].val + j * v[i];
                }
            }
        }
        int ans = 0;
        for (int i = 1; i <= m; i++)
            if (dp[i] == i)ans++;
        printf("%d\n", ans);
    }
    return 0;
}

感受一下时间差
二进制:
在这里插入图片描述
单调队列:
在这里插入图片描述

混合背包


就是混合上述三种背包,每种物品件数是一件、多件或无数件,判断再调用对应模板就好了。

背包问题还有分组背包,依赖背包等,最近一直在刷题,这篇博客也是放在草稿箱里好久了,留个位置以后更新吧(咕咕咕)

在这里插入图片描述

原创不易,请勿转载本不富裕的访问量雪上加霜
博主首页:https://blog.csdn.net/qq_45034708
如果文章对你有帮助,记得关注点赞收藏❤