【Acwing动态规划】背包问题

Acwing算法基础课动态规划第一节——背包问题。个人认为还算记得比较细,有需要的小伙伴可以看看我的一些思路~自己先在Typora做的笔记导进来的,图片大小没有调整,主要是备份笔记用的。

01背包

问题描述

有n个物品,每个物品体积为v[i],价值为w[i];背包体积容量m。每个物品只能装入背包一次,问满足不超过背包体积的情况下的最大价值。

思路分析

用 f[i] [j] 表示:前 i 个物品中,总体积不超过 j 的最大价值,则f [n] [m]就是我们要求的答案。

求 f[i] [j] 可以分成两个子集:

image-20230920121146948

左边指的是前 i-1 个物品(不含第 i 个物品的情况),则最大价值表示为 f[i-1] [j] ;右边指的是含第 i 个物品的情况,为前 i-1 个物品、总体积为 j - v[i] 的情况下的最大价值 + 第 i 个物品的价值 w[i] ,即最大价值表示为 f[i - 1] [j - v[i] ] + w[i]。f[i] [j] 的值就为左右两子集的最大值。

代码实现

import java.util.Scanner;

//二维数组解决01背包问题
public class bag01 {
    static int N = 1010;
    static int[] v = new int[N];
    static int[] w = new int[N];
    static int[][] f = new int[N][N];

    public static void main(String[] args) {
        Scanner myscanner = new Scanner(System.in);

        //输入物品个数 n和背包体积 m
        int n = myscanner.nextInt();
        int m = myscanner.nextInt();

        //输入物品体积数组和价值数组
        for (int i = 1; i <= n; i++) {
            v[i] = myscanner.nextInt();
            w[i] = myscanner.nextInt();
        }

        for (int i = 1; i <= n; i++) {   //i=0时f都为0(前0个物品,没东西装)
            for (int j = 0; j <= m; j++) {
                f[i][j] = f[i - 1][j];  //f=左子集
                if (j >= v[i]) {  //只有在背包体积 > 第 i个物品体积时才有右子集
                    f[i][j] = Math.max(f[i][j], f[i - 1][j - v[i]] + w[i]);
                }
            }
        }
        System.out.println(f[n][m]);
    }
}

优化

二维转化为一维:
删掉了第一维:在前i个物品中取。
f[j]表示:总体积不超过 j 的情况下的最大总价值。

为何能转化为一维?
二维时的更新方式:f [i] [j] = max( f [i - 1] [j] , f [i - 1] [ j - v[i] ] + w[i]);

我们发现,对于每次循环的下一组 i,只会用到 i - 1 来更新当前值,不会用到之前的值。所以在更新某一组 i 的时候就可以考虑把刚刚用过的 i - 1 这一组值给覆盖(即用滚动数组)。具体看下图:

image-20230920164115209

对于二维数组:A就是我们原本每次要更新的 f [i] [j] ;B是我们要用到的 f [i - 1] [j] ;因为 j-v[i] 一定< j ,则 f [i - 1] [ j - v[i] ]一定在B之前,则假设为C。由图显而易见,更新A的话一定要用到C点,那么覆盖数组的时候就一定要从每行的最后往前覆盖(即从m到0)。因为如果从前往后就会先把C覆盖了,A要用的时候就不是原来的C。

如何转化为一维呢?

对于拿前 i 个物品背包容量为 j 的情况:

  1. 如果第 i 个不拿的话(即左子集),和前一位置的信息(原来i-1数组的这个位置上的值)是相同的,即A=B,原先的B就是这里要的A,所以可以直接删除原先的 f[i] [j] = f [i - 1] [j] 这一行代码。
  2. 如果第 i 个拿的话,则A = max(B, C)。所以,更新方式就为:f[j]=max(f[j], f[ j - v[i] ] + w[i]);
  3. j 到 v[i] 就可以了,因为 j 再小的话就装不下第 i 个物品,再往前的值就和原数组的值一样了。(即上1)
//一维数组解决01背包问题(只显示需修改之处)
static int[] f = new int[N];
for (int i = 1; i <= n; i++) { 
    for (int j = m; j >= v[i]; j--) 
        f[j] = Math.max(f[j], f[j - v[i]] + w[i]);
}
System.out.println(f[m]);

完全背包

问题描述

和01背包的区别:每个物品不限装入背包的次数

思路分析

f [i] [j] 集合划分为若干子集:装0个第 i 个物品,装1个第 i 个物品…装k个第 i 个物品。如下图:

image-20230921102236673

装0个第 i 个物品的情况就是 f [i - 1] [j] ;装k个第 i 个物品就是 f [ i-1 ] [ j - k * v[i] ] + k * w[i]。我们发现,当k=0时正好也满足前者,所以可以合并。使得对于每一个物品 i ,从小到大的 j ,遍历 k,不断更新最大值。

代码实现

//只显示与01背包二维实现不同的部分
for (int i = 1; i <= n; i++) {   //i=0时f都为0(前0个物品,没东西装)
    for (int j = 0; j <= m; j++) {
        for (int k = 0; k*v[i] <= j ; k++) {
            f[i][j] = Math.max(f[i][j], f[i - 1][j - k*v[i]] + k*w[i]);
        }
    }
}

优化1

因为走三重循环,导致运行效率不高,考虑优化。

image-20230921112801909

根据之前的子集划分,得到 1 式。令 1 式的 j = j-v ,可得 2 式。观察得知,除了第一项,1式与2式的每一项都刚好差一个w[i] ,因此 f [i] [j] 可以改写为image-20230921114904395

for (int i = 1; i <= n; i++) {   //i=0时f都为0(前0个物品,没东西装)
    for (int j = 0; j <= m; j++) {
        f[i][j] = f[i - 1][j];  
        if(j >= v[i])    
            f[i][j] = Math.max(f[i][j], f[i][j - v[i]] + w[i]);
    }
}

优化2

如1.4节,优化为一维数组表示。注意:此处的 j 要从小到大循环了,因为如下图:

image-20230921121910464 image-20230921122419267

1式为01背包问题,用的是 i-1 这一行的数据,即求A需要BC两点;而完全背包根据2式,求A点是需要BD两点,所以应该先更新D点再求A,即 j 应该从小到大遍历。

for (int i = 1; i <= n; i++) {   //i=0时f都为0(前0个物品,没东西装)
    for (int j = v[i]; j <= m; j++)
        f[j] = Math.max(f[j], f[j - v[i]] + w[i]);
}

多重背包

问题描述

第 i 个物品最多装入背包的次数为 s[i]

思路分析

就把完全背包的k当成 s[i] 就行

代码实现

暴力解法:

  1. 定义数组s:
static int[] s = new int[N];
  1. 输入s:
for (int i = 1; i <= n; i++) {
    v[i] = myscanner.nextInt();
    w[i] = myscanner.nextInt();
    s[i] = myscanner.nextInt();
}
  1. 修改k跳出循环的条件:
k*v[i] <= j && k <=s[i]

优化

当物品种类N和背包容量V较大时,采用以上暴力解法可能会超时,这时就要考虑优化。像2.4的优化思路写出式子后发现, 2式会比1式最后多一项,所以2.4的优化思路不能拿过来。 参考二进制,想到一种优化思路:

若第 i 个物品允许装入的最大个数为 s,则不一定要按装1个、装2个…装s个来枚举。先考虑特殊的 s=1023,根据二进制算法,可以考虑将1个第 i 个物品、2个第 i 个物品、4个第 i 个物品…2^k个第 i 个物品打包成一件大物品,然后就等价于01背包问题。

为什么等价01背包问题?

对于每一个第 i 个物品,都是一个01背包问题:第1个物品的体积v为v[ i ],价值为w[ i ];第2个物品的体积v为2v[ i ],价值为2w[ i ];第3个物品的体积v为4v[ i ],价值为4w[ i ]…第 k个物品的体积v为kv[ i ],价值为kw[ i ]。根据二进制可知,每个物品现在都是要么取要么不取,取的话只能取一次,不就是01背包问题。现在就和这个s没有关系了。对于所有物品,则是若干个01背包问题的集合,也是01背包问题。

为什么可以等价成01背包问题(为什么可以用二进制优化)?

因为二进制可以表示所有数。当按上述思路打包物品时,可以保证第 i 个物品拿0~s[i] 件。

具体打包方式

先考虑特殊的 s=1023:打包成1个物品、2个物品、4个物品…512个物品。根据等比数列求和(前k项和为2^(k+1) -1,打包完之后组合,就可以满足装 0~1023(1+2+4+…512)个物品。

对于一般的s=200,则打包成1个物品、2个物品、4个物品…64个物品(前面求和为127,下一个大件物品若为128就>200了),所以最后一个大件物品=73个物品。因此打包大件的原则如下:,其中image-20230925120657854。前k项满足取到的物品个数为,加上c则为image-20230925120914739。那我们需要考虑,是否两个区间中间会有空隙(即取不到的个数),如下图所示:image-20230925121230547但因为image-20230925120657854,所以可以保证B点在A点的位置或其左边,即两个区间衔接得上。

代码实现

import java.util.Scanner;
class Main{
    static int N = 25000;
    static int[] v = new int[N];
    static int[] w = new int[N];
    static int[] f = new int[N];

    public static void main(String[] args) {
        Scanner myscanner = new Scanner(System.in);

        //输入物品个数 n和背包体积 m
        int n = myscanner.nextInt();
        int m = myscanner.nextInt();
        int cnt = 0;   //记录转换成01背包问题后的体积数组v的下标

        //输入第i个物品体积、价值、允许装入的最大个数
        for (int i = 1; i <= n; i++) {
            int vi, wi, si;
            vi = myscanner.nextInt();
            wi = myscanner.nextInt();
            si = myscanner.nextInt();
            int k = 1;    //打包的大件物品中含第i个物品的个数
            while (k <= si) {
                cnt++;
                v[cnt] = vi * k;  //转换成01背包问题后第cnt个物品的v
                w[cnt] = wi * k;  //转换成01背包问题后第cnt个物品的w
                si -= k;
                k *= 2;
            }
            if (si > 0) {   //此时的si就是最后一件大包物品的第i个物品的个数
                cnt++;
                v[cnt] = vi * si;
                w[cnt] = wi * si;
            }
        }  //循环完就得到v和w数组,就和01背包问题一毛一样
        n = cnt;
        for (int i = 1; i <= n; i++) {   //i=0时f都为0(前0个物品,没东西装)
            for (int j = m; j >= v[i]; j--)
                f[j] = Math.max(f[j], f[j - v[i]] + w[i]);
        }
        System.out.println(f[m]);
    }
}

分组背包

问题描述

特殊性:每组物品有若干个,**同一组内的物品最多只能选一个

image-20230925105855375

思路分析

image-20230925135530242

对于前 i 组的情况 f [i] [j] ,分为第 i 组的物品根本不要——f [ i-1 ] [j] ;和要第 i 组的第 k 个的情况——(如上)

代码实现

import java.util.Scanner;
class Main{
    static int N = 1110;
    static int[][] v = new int[N][N];
    static int[][] w = new int[N][N];
    static int[] f = new int[N];
    static int[] s = new int[N];

    public static void main(String[] args) {
        Scanner myscanner = new Scanner(System.in);

        //输入物品个数 n和背包体积 m
        int n = myscanner.nextInt();
        int m = myscanner.nextInt();

        //输入物品体积和价值
        for (int i = 1; i <= n; i++) {
            s[i] = myscanner.nextInt();    //第i个分组所含物品种类数
            for (int j = 0; j < s[i]; j++) {
                v[i][j] = myscanner.nextInt();
                w[i][j] = myscanner.nextInt();
            }
        }

        for (int i = 1; i <= n; i++) 
            for (int j = m; j >= 0; j--)
                for (int k = 0; k < s[i]; k++) 
                    if(v[i][k] <= j)
                        f[j] = Math.max(f[j], f[j - v[i][k]] + w[i][k]);
        System.out.println(f[m]);
    }
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值