动态规划思想:0-1背包问题 滚动数组 压缩空间

0. 说明

首先,还请不要被滚动数组这种词给吓到,它的本质其实很简单,在之前的经历中你很可能已经接触过,例如比较经典的斐波那契数列。

其次,读者可能更想知道0-1背包压缩为什么要倒序遍历,请跳转到第三部分0-1背包问题分析,我一定努力将该问题表达清楚。

1. 思想介绍

在动态规划运行过程中,申请到的数组空间可以进行压缩。根据dp方程判断可以抛弃哪些数据,从而只用更少的空间来存在动态规划过程中产生的数据,并不断用新数据覆盖旧数据,减少空间的使用。

2. 一维数组空间的压缩

leetcode链接:509. 斐波那契数
70. 爬楼梯

以斐波那契数列为例:

class Solution {
    public int fib(int n) {
        if (n <= 1) return n;
        int[] dp = new int[n + 1];
        // 初始化
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }

        return dp[n];
    }
}

上述求解方式,申请了n个空间,并通过 dp[i] 来表示第 i 个数的斐波那契数值。但是,通过分析状态转移方程 dp[i] = dp[i - 1] + dp[i - 2] ,可以发现,其实在推导过程中只需要维护两个数值就够了,并不需要记录整个数列过程中的值。

即我们的目的是为了得到 dp[n] ,那么只需要维护在循环过程中的最新的两个数值,保证他们不被覆盖,最终能够得到正确的数值即可。而并不需要保留每一个i的数值 dp[i]

压缩空间后的代码如下所示:

class Solution {
    public int fib(int n) {
        if (n < 2) return n;
        int a = 0, b = 1, c = 0;
        for (int i = 1; i < n; i++) {
            c = a + b;
            a = b;
            b = c;
        }
        return c;
    }

3. 二维数组空间的压缩(0-1背包问题)

leetcode链接:416. 分割等和子集

下面来分析0-1背包问题的空间压缩。

3.1 理论基础

首先来理一下0-1背包的理论基础,先搞清楚如何用二维数组表示该问题。
假设有n个物体,标号分别为[1,2,… ,n],每个物体都有各自的重量用 weight 数组来表示,以及对应的价值用 value 数组表示。题目的需求是在有限背包容量的情况下,尽可能装更多价值的物品。
dp[i][j] 来表示下标在 1~i 范围内的物品里任意取,放入容量为 j 的背包里,价值总和最大为多少

推导 dp[i][j] 过程如下:对于物品 i ,有两种选择方式,不放入背包或放入背包,dp[i][j] 要取这两种选择方式下对应的最大价值。分别对其进行分析。

  • i 不放入背包:可以表示为,标号为 [1, 2, …, i - 1] 的物品里任意取(因为 i 已经确定不放入背包了),放入容量为j 的背包里,那么其价值总和最大为 dp[i - 1][j]。即这种情况下可以表示为 dp[i][j] = dp[i - 1][j] (但是不一定,因为这种情况下不一定取到最大值,只是为了方便理解)

  • i 放入背包:dp[i - 1][j - weight[i]] 表示背包容量为 j - weight[i] 的时候不放物品 i 的最大价值,那么 dp[i - 1][j - weight[i]] + value[i] ,就是背包放物品i得到的最大价值

所以求出了二维数组表示的递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]

dp进行初始化后,就可以通过两层循环求出需要的结果,如下:

// weight数组的大小 就是物品个数
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
    for(int i = 1; i < weight.size(); i++) { // 遍历物品
        if (j < weight[i]) dp[i][j] = dp[i - 1][j];
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    }
}

3.2 用一维dp数组表示背包问题

重头戏来了!!!

参考斐波那契数列压缩时的方式:在循环过程中只需要维护两个变量的值就足够了。

带着这个思路来观察上一小节得到的递推公式,dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i],可以发现在递推的过程中,事实上也只是用到了两行的数据,即在求第 i 行时,并没有用到 i - 1 行之前的结果,所以就考虑只维持一个一维数组,在循环中不断进行数据覆盖与更新,来得到需要的结果。

推导dp[j]过程同上:

  • i 不放入背包:可以表示为,标号为[1, 2, …, i - 1]的物品里任意取,放入容量为 j 的背包里,那么其价值总和最大为 dp[j]
  • i 放入背包:dp[j - weight[i]] 表示背包容量为 j - weight[i] 的时候不放物品i的最大价值,那么 dp[j - weight[i]] + value[i] ,就是背包放物品 i 得到的最大价值

一维数组表示出的递推公式为:dp[j] = max(dp[j], dp[j - weight[i] + value[i])

具体实现代码如下:

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

为什么第二层循环要倒序呢?

  • 首先要想明白:用一行的空间来表示的时候,如何能够用原来上一行的数据表示出本行的数据呢?
    事实上,未进行第 i 行遍历(外层循环)时,dp数组中存放的数据就为第 i - 1 行对应的值。能理解这一点非常重要。例如,在进行这段循环的代码之前(也就是循环i = 0 之前),dp的值就为刚刚初始化的值;而当 i = 2 时的循环开始前,其实 dp 中存放的值是第一行的数据值,也即 dp[1][j];以此类推,当 i 的循环开始之前,dp中存放的值是第 i - 1行的数据。
  • 继续分析,根据二维的递推式,dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i],每一个第 i 层的数据都是由第 i - 1层得来,且这个过程中,用到的两个中间dp的列坐标 j 和 j - weight[i],都小于等于所求的dp的列坐标 j (即 j >= [j, j - weight[i]])。
  • 得出上面两点后,再去看一维递推实现的代码过程:即在遍历过程中所需要求解的 dp[j]是属第 i 行的值,而赋值等式的右边用到的两个数据 dp[j], dp[j - weight[i]] + value[i]是第 i - 1行的数据,所以如果正序遍历的话就会导致下标小的dp先进行赋值(此时下标小的dp已经变为了第i行数据),而下标大的dp再进行赋值时,需要下标小的第 i - 1 行的数据,但此时该数据已经被第 i 行给覆盖了,故下标大的dp赋值出现错误!!所以,用倒序遍历的话,就可以先更新大下标的dp值,而不会影响后续更新的过程。

至此分析完毕,希望读者能有所收获,感谢!

参考链接:
代码随想录_0-1背包理论基础
滚动数组(简单说明)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值