动态规划之背包问题

文章目录

前言

目前在学习AcWing课程的算法基础课,当前博客中主要包含背包类问题的模板题,之后会陆续更新一些相对应题目的题单。

当前文章已收录到博客文件目录索引:博客目录索引(持续更新)


一、认识背包问题(简洁说明)

①01背包(每件物品能选一次或不选):每件物品最后多只用一次。

  • 01背包是在M件物品取出若干件放在空间为W的背包里,每件物品的体积为W1,W2至Wn,与之相对应的价值为P1,P2至Pn,每种物品有且只有一个。

  • 在01背包问题中,因为每种物品只有一个,对于每个物品只需要考虑选与不选两种情况。如果不选择将其放入背包中,则不需要处理。如果选择将其放入背包中,由于不清楚之前放入的物品占据了多大的空间,需要枚举将这个物品放入背包后可能占据背包空间的所有情况。

②完全背包:每件物品有无限个。

③多重背包(含优化):每件物品最多有有限个,题目给出限制。

④分组背包:有多组,每一组里面可以选一个。


二、01背包

AcWing 2. 01背包问题(模板题)

题目链接:AcWing 2. 01背包问题

分析:

image-20230323195934227

题解:

二维数组:

复杂度分析:时间复杂度O(n^2^);空间复杂度O(n^2^)

import java.util.*;
import java.io.*;

class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 1010;
    static int[] v = new int[N], w = new int[N];
    static int[][] fn = new int[N][N];
    static int n, m;
    
    public static void main(String[] args) throws Exception{
        String[] ss = cin.readLine().split(" ");
        n = Integer.parseInt(ss[0]);
        m = Integer.parseInt(ss[1]);
        for (int i = 1; i <= n; i ++) {
            ss = cin.readLine().split(" ");
            v[i] = Integer.parseInt(ss[0]);
            w[i] = Integer.parseInt(ss[1]);
        }
        //初始化默认为0
        //dp状态转移
        for (int i = 1; i <= n; i ++) {
            for (int j = 1; j <= m; j ++) {
                //不选
                fn[i][j] = fn[i - 1][j];
                if (j >= v[i]) {
                    //选
                    fn[i][j] = Math.max(fn[i][j], fn[i - 1][j - v[i]] + w[i]);
                }
            }
        }
        //最大装的重量
        System.out.println(fn[n][m]);
    }
    
}

image-20230323201641771

一维数组(优化):改成一维数组的滚动数组时,需要将第二层遍历m循环从后往前,若是从前往后的话fn[j - v[i]]就会使用到最新已更新的数据,而不是上一层旧的数据

复杂度分析:时间复杂度O(n^2^);空间复杂度O(n)

import java.util.*;
import java.io.*;

class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 1010;
    static int[] v = new int[N], w = new int[N];
    static int[] fn = new int[N];
    static int n, m;
    
    public static void main(String[] args) throws Exception{
        String[] ss = cin.readLine().split(" ");
        n = Integer.parseInt(ss[0]);
        m = Integer.parseInt(ss[1]);
        for (int i = 1; i <= n; i ++) {
            ss = cin.readLine().split(" ");
            v[i] = Integer.parseInt(ss[0]);
            w[i] = Integer.parseInt(ss[1]);
        }
        //初始化默认为0
        //dp状态转移
        for (int i = 1; i <= n; i ++) {
            //为了上一层数据不被覆盖为新的,从后往前来进行更新背包最大重量
            for (int j = m; j >= 1; j --) {
                //不选
                //fn[j] = fn[j];
                if (j >= v[i]) {
                    //选
                    fn[j] = Math.max(fn[j], fn[j - v[i]] + w[i]);
                }
            }
        }
        //最大装的重量
        System.out.println(fn[m]);
    }
    
}

image-20230323201645991


三、完全背包问题

AcWing 3. 完全背包问题(模板题,含优化)

题目链接:AcWing 3. 完全背包问题

image-20230323203135918

复杂度分析:时间复杂度O(n^3^);空间复杂度O(n^2^)

import java.util.*;
import java.io.*;

class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 1010;
    static int[] v = new int[N], w = new int[N];
    static int[][] fn = new int[N][N];
    static int n, m;
    
    public static void main(String[] args) throws Exception{
        String[] ss = cin.readLine().split(" ");
        n = Integer.parseInt(ss[0]);
        m = Integer.parseInt(ss[1]);
        for (int i = 1; i <= n; i ++) {
            ss = cin.readLine().split(" ");
            v[i] = Integer.parseInt(ss[0]);
            w[i] = Integer.parseInt(ss[1]);
        }
        //多种循环
        for (int i = 1; i <= n; i ++) {  //前i个物品
            for (int j = 0; j <= m; j ++) {  //体积为k
                int t = j / v[i];//计算最多装多少个物品i
                for (int k = 0; k <= t; k ++) { //多个物品
                    //由于k是从0开始的,所以我们可省略该句
                    //fn[i][j] = Math.max(fn[i][j], fn[i - 1][j]);
                    //不拿当前第i个物品;拿当前第i个物品k个
                    fn[i][j] = Math.max (fn[i][j], fn[i - 1][j - k * v[i]] + k * w[i]);
                }
            }
        }
        System.out.println(fn[n][m]);
    }
    
}

image-20230323205934628

三重循环优化为两重循环:

我们将j - k * v[i]看作变量v。

目标转移方程:dp[i][j]= max(dp[i - 1][j], dp[i - 1][j - v] + w, dp[i - 1][j - 2 * v] + 2 * w, dp[i - 1][j - 3 * v] + 3 * w, ...);

而我们可以发现在上面方程之前:dp[i][j - v] = max(dp[i - 1][j - v], dp[i - 1][j - 2 * v] + w, dp[i - 1][j - 3 * v] + 2 * w, .....);

那么我们可以将目标转移方程优化为:dp[i][j]= max(dp[i - 1][j], dp[i][j - v] + w)

import java.util.*;
import java.io.*;

class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 1010;
    static int[] v = new int[N], w = new int[N];
    static int[][] fn = new int[N][N];
    static int n, m;
    
    public static void main(String[] args) throws Exception{
        String[] ss = cin.readLine().split(" ");
        n = Integer.parseInt(ss[0]);
        m = Integer.parseInt(ss[1]);
        for (int i = 1; i <= n; i ++) {
            ss = cin.readLine().split(" ");
            v[i] = Integer.parseInt(ss[0]);
            w[i] = Integer.parseInt(ss[1]);
        }
        //多种循环
        for (int i = 1; i <= n; i ++) {  //前i个物品
            for (int j = 0; j <= m; j ++) {  //体积为k
                fn[i][j] = fn[i - 1][j];
                //优化
                if (j >= v[i])
                    fn[i][j] = Math.max (fn[i][j], fn[i][j - v[i]] + w[i]);
            }
        }
        System.out.println(fn[n][m]);
    }
    
}

image-20230323211153260

优化空间二维为一维(滚动数组):

import java.util.*;
import java.io.*;

class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 1010;
    static int[] v = new int[N], w = new int[N];
    static int[] fn = new int[N];
    static int n, m;
    
    public static void main(String[] args) throws Exception{
        String[] ss = cin.readLine().split(" ");
        n = Integer.parseInt(ss[0]);
        m = Integer.parseInt(ss[1]);
        for (int i = 1; i <= n; i ++) {
            ss = cin.readLine().split(" ");
            v[i] = Integer.parseInt(ss[0]);
            w[i] = Integer.parseInt(ss[1]);
        }
        //多种循环
        for (int i = 1; i <= n; i ++) {  //前i个物品
            for (int j = 0; j <= m; j ++) {  //体积为k
                //优化
                if (j >= v[i])
                    fn[j] = Math.max (fn[j], fn[j - v[i]] + w[i]);
            }
        }
        System.out.println(fn[m]);
    }
    
}

image-20230323211303477

对应与01背包的区别:上01、下完全

image-20230323211342835


四、多重背包问题

AcWing 4. 多重背包问题 I(模板题,可三重循环暴力)

题目链接:AcWing 4. 多重背包问题 I

分析

与完全背包类似,这里给定了我们每个物品的指定数量,所以我们无需进行自行计算每个物品最大的数量了,直接三重循环暴力走一波。

由于N,V给的数量不大,三重循环也能够通过。

image-20230331155334522

题解

复杂度分析:时间复杂度O(n^3^);空间复杂度O(n^2^)

import java.util.*;
import java.io.*;

class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 110;
    static int[] v = new int[N], w = new int[N], s = new int[N];
    static int[][] fn = new int[N][N];
    static int n, m;
    
    public static void main(String[] args) throws Exception{
        String[] ss = cin.readLine().split(" ");
        n = Integer.parseInt(ss[0]);
        m = Integer.parseInt(ss[1]);
        for (int i = 1; i <= n; i ++) {
            ss = cin.readLine().split(" ");
            v[i] = Integer.parseInt(ss[0]);
            w[i] = Integer.parseInt(ss[1]);
            s[i] = Integer.parseInt(ss[2]);
        }
        //多种循环
        for (int i = 1; i <= n; i ++) {  //前i个物品
            for (int j = 0; j <= m; j ++) {  //体积为k
                //指定物品的数量(注意:k * v[i] <= j范围下进行选择)
                for (int k = 0; k <= s[i] && k * v[i] <= j; k ++) {
                    //由于k从0开始的,所以可以进行省略
                    //fn[i][j] = Math.max(fn[i][j], fn[i - 1][j]);
                    fn[i][j] = Math.max (fn[i][j], fn[i - 1][j - k * v[i]] + k * w[i]);
                }
            }
        }
        System.out.println(fn[n][m]);
    }
    
}

image-20230331155428312


AcWing 5. 多重背包问题 II(模板题,二进制优化为01背包问题)

题目链接:AcWing 5. 多重背包问题 II

分析

简述:利用二进制去将1个物品s个数量优化为logs个组合箱子数,接着就是一个01背包思路解决方案。

  • 将多重背包问题去优化为01背包问题。

问题1:为什么多重背包问题会多出来一项?

//完全背包问题
dp[i][j]= max(dp[i - 1][j], dp[i - 1][j - v] + w, dp[i - 1][j - 2 * v] + 2 * w, dp[i - 1][j - 3 * v] + 3 * w, ...);
dp[i][j - v] = max(dp[i - 1][j - v], dp[i - 1][j - 2 * v] + w, dp[i - 1][j - 3 * v] + 2 * w, .....);
最终优化为:dp[i][j]= max(dp[i - 1][j], dp[i][j - v] + w)
    
//多重背包问题
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v] + w, dp[i - 1][j - 2v] + 2w, dp[i - 1][j - 3v] + 3w, ..., dp[i - 1][j - sv] + sw)
dp[i][j - v] = max(dp[i - 1][j - v], dp[i - 1][j - 2v] + w, dp[i - 1][j - 3v] + 2w, ..., dp[i - 1][j - sv] + (s - 1)w, dp[i - 1][j - (s + 1)v] + sw)

注意:这边多重背包多出来一项dp[i - 1][j - (s + 1)v] + sw,什么原因呢?因为针对于dp[i][j - v]同样也是来看做是从0*v[i] 不断减到s*v[i],所以最后一个为j - v - s*v[i] = j - (s + 1)v。

问题2:不能用完全背包问题解法来解决的原因?

image-20230331170425675

给定我们所有数的最大值,能够根据最后一个最大值来求出来前面元素的最大值呢?

  • 这个是不能求得的,不能做减法的,max是不能做减法的,加法是可以做出来的。
  • 若是最大值,我们并不能通过总共的最大值和最后1个最大值来求出来前面的最大值,所以不能够使用完全背包问题解决。

问题3:二进制优化的点在哪里,如何进行优化?

原本的暴力做法时间复杂度为O(n * m * s),可以通过组合形式来压缩计算的次数。

这里拿一个案例说明:取自博客 动态规划专题——背包问题

image-20230331173822819

针对于根据给定的每个物品s数量,我们来将一个物品s个数量去拆为logs组,即原本要暴力求得n*s转为n*logn

//实现代码
int a,b,s;
cin >> a >> b >> s;
int k = 1; // 组别里面的个数
while(k<=s)
{
    cnt ++ ; //组别先增加
    v[cnt] = a * k ; //整体体积
    w[cnt] = b * k; // 整体价值
    s -= k; // s要减小
    k *= 2; // 组别里的个数增加
}
//剩余的一组
if(s>0)
{
    cnt ++ ;
    v[cnt] = a*s; 
    w[cnt] = b*s;
}

//举实际案例
//案例1:重量为4,价值为3,数量为5
//对应的1个物品数量5个,拆分多组5 => 1 2 2    后面分别表示1组1个,1组2个,1组2个,可以由这三组构成数量0-5任何一个数
1 <= 5    cnt = 0, v[0] = 4, w[0] = 3, s = 4, k = 2
2 <= 4    cnt = 1, v[1] = 8, w[1] = 6, s = 2, k = 4
//由于此时4>2,所以这里单独取s剩余数
cnt = 2, v[2]=2*4=8,w[2]=2*3=6 
    
//举例2:重量为4,价值为3,数量为7
//对应的1个物品数量7个,拆分多组7 => 1 2 4    后面分别表示1组1个,1组2个,1组4个,可以由这三组构成数量0-7任何一个数
1 <= 7    cnt = 0, v[0] = 4, w[0] = 3, s = 6, k = 2
2 <= 6    cnt = 1, v[1] = 8, w[2] = 6, s = 4, k = 4
4 <= 4    cnt = 2, v[2] = 16, w[2] = 12, s = 0, k = 8
//结束

最终我们对目标重量来进行依次枚举m次,此时时间复杂度为O(n*logns*m),原本O(n*m*s)需要运行40亿次,此时优化为两千四百万样子,即可AC。

题解

复杂度分析:时间复杂度O(n*logns*m);空间复杂度O(n*logns)

import java.util.*;
import java.io.*;

class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 12010, M = 2010;//N表示n*logs,1000 * 12 + 10,M表示最大的体积重量
    static int[] v = new int[N], w = new int[N];//空间都为总组数
    static int[] fn = new int[M];//一维滚动数组优化,这里数量为体积大小
    static int n, m;
    
    public static void main(String[] args) throws Exception{
        String[] ss = cin.readLine().split(" ");
        n = Integer.parseInt(ss[0]);
        m = Integer.parseInt(ss[1]);
        int cnt = 0;//最终的总组数
        //读取每组的体积和质量
        for (int i = 1; i <= n; i ++) {
            ss = cin.readLine().split(" ");
            int vv = Integer.parseInt(ss[0]);
            int ww = Integer.parseInt(ss[1]);
            int s = Integer.parseInt(ss[2]);
            int k = 1;
            //根据s来分配每组物品的数量
            while (k <= s) {
                cnt ++;
                v[cnt] = k * vv;
                w[cnt] = k * ww;
                s -= k;//减去总数
                k *= 2;//2的倍乘
            }
            //处理多余的s
            if (s > 0) {
                cnt ++;
                v[cnt] = s * vv;
                w[cnt] = s * ww;
            }
        }
        
        //01背包问题解决方案
        for (int i = 1; i <= cnt; i ++) { //总数量为cnt个
            //滚动一维数组需要从后往前
            for (int j = m; j >= v[i]; j --) {
                fn[j] = Math.max(fn[j], fn[j - v[i]] + w[i]);
            }
        }
        System.out.println(fn[m]);
    }
}

image-20230331180847601


五、分组背包问题

AcWing 9. 分组背包问题(模板题)

题目链接:AcWing 9. 分组背包问题

分析

分组背包:有多组,每一组里面可以选一个。

以第i组为单位来进行选,多加一层循环遍历组内的物品,本质还是01背包问题。

image-20230331185151272

题解

复杂度分析:时间复杂度O(n^3^);空间复杂度O(n^2^)

import java.util.*;
import java.io.*;

class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 110;
    static int[][] v = new int[N][N], w = new int[N][N];//重量和价值在指定i组k个 v[i][k]  w[i][k]
    static int[] fn = new int[N];//滚动数组
    static int[] s = new int[N];//记录每组的数量
    static int n, m;//组数,重量
    
    public static void main(String[] args) throws Exception{
        String[] ss = cin.readLine().split(" ");
        n = Integer.parseInt(ss[0]);
        m = Integer.parseInt(ss[1]);
        for (int i = 1; i <= n; i ++) {
            s[i] = Integer.parseInt(cin.readLine());
            //读取每组的物品
            for (int j = 0; j < s[i]; j ++) {
                ss = cin.readLine().split(" ");
                v[i][j] = Integer.parseInt(ss[0]);
                w[i][j] = Integer.parseInt(ss[1]);
            }
        }
        //dp
        for (int i = 1; i <= n; i ++) {  //遍历前i组
            for (int j = m; j >= 0; j --) {  //体积重量 滚动数组,从后往前(01背包)
                for (int k = 0; k < s[i]; k ++) { //组内第k个
                    //必须放在for循环内部,否则会直接提前结束
                    if (j >= v[i][k]) {
                        fn[j] = Math.max(fn[j], fn[j - v[i][k]] + w[i][k]);
                    }
                }
            }
        }
        System.out.println(fn[m]);
    }
    
}

image-20230331185411364


参考文章

[1]. 动态规划专题——背包问题:通俗易懂十分详细,尤其是其中的多重背包问题优化。

[2]. AcWing 5. 二进制优化,它为什么正确,为什么合理,凭什么可以这样分??:关于多重背包问题的疑问和解答比较详细,推荐。

[3]. AcWing 9. 分组背包问题(算法基础课):y总分组背包视频讲解

评论区请在客户端页面查看