动态规划-01背包问题


1. 分割等和子集(416)

题目描述:
在这里插入图片描述
状态表示:
根据题目意思,要分割数组,使得两个子集元素和相等,等于说我们就是要找出一个子集和为数组的总和的二分之一即可。那么我们可以这样想,假如有一个sum/2的空间的背包,需要我们不断添加只有体积属性的物品直到加满背包。那么我们就可以建立一个二维数组dp来表示这种状态,dp[i][j]表示从前i个物品中任意选出元素是否能够装满j这么大的空间。dp数组的行数就是nums数组中的元素的个数,列数就是数组元素的和sum/2。
状态转移方程:
状态分析还是老样子,分析最后的元素,即第i个元素被加入的情况,如果我们不选第i个元素加入背包,那么dp[i][j]=dp[i-1][j];如果我们选择第i个元素加入背包,如果说j-nums[i]>=0,那么dp[i][j]=dp[i-1][j-nums[i]]。最终dp[i][j]=dp[i-1][j] || dp[i-1][j-nums[i]]。
初始化:
为了方便我们后续循环里面的运算以及防止越界,因此我将dp数组加上第0行和第0列。此时第0行表示数组为0时,第0列表示要求满足的背包容量为0。先填第0行,此时数组为空,只有当背包要求满足的空间为0也就是(0,0)位置被赋为true,其它都被赋为false。第0列此时要满足的背包容量为0,因此第0列只要不放元素就能够满足条件,因此都赋为true。这个过程是很好理解的。
填表顺序:
根据状态转移方程,填表顺序就是从上到下,从左至右。
返回值:
先求出数组的所有元素的和sum,然后得到sum/2,所以返回dp[数组元素个数][sum/2]即可。不过要注意一个小细节就是,如果说数组的和是个奇数,那么,直接返回false即可。
代码如下:

class Solution {
    public boolean canPartition(int[] nums) {
        int m = nums.length;
        int n = 0;

        for (int x : nums) {
            n += x;
        }

        if (n % 2 != 0) {
            return false;
        }

        n = n / 2;

        boolean[][] dp = new boolean[m + 1][n + 1];

        dp[0][0] = true;
        for (int i = 1; i <= m; i++) {
            dp[i][0] = true;
        }

        for (int i = 1; i <= m; i++) {

            for (int j = 1; j <= n; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j - nums[i - 1] >= 0) {
                    dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]];
                }

            }
        }

        return dp[m][n];

    }
}

此时的运行效率:
在这里插入图片描述
优化后代码:

class Solution {
    public boolean canPartition(int[] nums) {
        int m = nums.length;
        int n = 0;

        for (int x : nums) {
            n += x;
        }

        if (n % 2 != 0) {
            return false;
        }

        n = n / 2;

        boolean[] dp = new boolean[n + 1];

        dp[0] = true;
        for (int i = 1; i <= m; i++) {
            dp[0] = true;
        }

        for (int i = 1; i <= m; i++) {

            for (int j = n; j >= nums[i - 1]; j--) {

                dp[j] = dp[j] || dp[j - nums[i - 1]];

            }
        }

        return dp[n];

    }
}

优化后的运行效率:
在这里插入图片描述

题目链接
时间复杂度:O(N^2)
优化前空间复杂度:O(N^2)
优化后空间复杂度:O(N)

2. 目标和(494)

题目描述:
在这里插入图片描述
状态表示:
这题需要理解题意,它要我们给取出的数字加上+或者-号,并且要求最终值与target相等,然后需要得到有多少种选法。我们可以这样理解,题目这样要求就相当于我们可以将数组中的数分为两个子集分别为a和b,a中是选出来要加上+的数字,b是选出来要加上-的数字,那么a和b的绝对值就有下面的一组关系,设nums数组数字的和为sum,那么a+b=sum并且a-b=target。得到上述的a和b的关系,那么就可以得到aim=(target+sum)/;2这样的表达式,这就意味我们只需要在数组中选出若干个数字使其和满足aim的值,并且最终得到有多少种选法就行,这就是一个01背包问题。因此我们根据题目要求以及经验设置一个二维数组dp[i][j]来表示在前i个数字当中进行选择能够使得数字和等于j时有多少种选法。
状态转移方程:
对于状态分析还是老样子,0到i这个区间的最后一个数字i位置的数字进行分析,当最终不选择i位置的数字时,那么选法就是前i-1个数字的选法即dp[i][j]=dp[i-1][j],当最终选择i位置的数字时,那么对于dp[i][j]的前一个场景选出的数字和就要满足j-nums[i],此时肯定要进行判断的,防止这个减完之后值小于0,完成判断之后,dp[i][j]=dp[i-1][j-nums[i]]。
初始化:
初始化也是一样,给dp数组加上一行和一列,对于dp[0][0]表示不选择数字使得数字和满足0显然有一种选法,因此赋为1。对于第0行除dp[0][0]之外,其余都是表示数组无元素但是需要去满足不等于0的值,这个很好理解直接都赋为0即可。对于第0列除dp[0][0]之外的元素,表示在数组有多个数字的情况下,有多少种选法能够使得数字和为0,显然这种情况下是可能存在多种选法的,因此我们需要额外考虑这样会很麻烦。但是事实上不是这样,因此这里加上第0列是为了便于运算以及防止越界,既然觉得给第0列初始化麻烦,那么我们这里直接从第0列开始运算。防越界的状态转移方程就是dp[i][j]=dp[i-1][j-nums[i]],如果你要利用这个状态转移方程就需要满足前面j-nums[i]>=0这样的判断条件,对于第0列j就是0,此时满足条件的nums[i]也是0,所以j-nums[i]只要满足判断的条件值就是0,那么计算第0列的各个值就是用的上一行同一列的值。不满足j-nums[i]>=0判断条件那就更不会越界了,因此我们在这一题对于dp数组的运算,关于dp数组的列的循环计算直接从第0列开始。
填表顺序:
从上到下,从左至右。
返回值:
dp数组在建立时使用的行数和列数分别为nums.length+1以及aim+1,因此返回值根据题意就是dp[nums.length][aim]。不过要注意一个细节,因为我们得到的aim在逻辑上是nums数组中数字被加上+的数字的和,因此它不能够是负数,并且如果说你target+sum是个奇数除2得到的aim也不能通过nums中的数累加得到,因此对于这两种情况直接返回0即可。
代码如下:

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int x : nums) {
            sum += x;
        }

        int aim = (sum + target) / 2;
        if (aim < 0 || (sum + target) % 2 == 1) {
            return 0;
        }

        int[][] dp = new int[nums.length + 1][aim + 1];
        dp[0][0] = 1;

        for (int i = 1; i <= nums.length; i++) {

            for (int j = 0; j <= aim; j++) {

                dp[i][j] = dp[i - 1][j];
                if (j - nums[i - 1] >= 0) {
                    dp[i][j] += dp[i - 1][j - nums[i - 1]];
                }
            }
        }

        return dp[nums.length][aim];
    }
}

此时的运行效率:
在这里插入图片描述

优化后代码:
01背包问题优化就是使用滚动数组,要注意两个点即可,第一就是使用一维数组,第二就是要注意列的计算顺序是从右至左,第三点就是可以将内循环的条件进行修改,将判断条件融合进去从而达到常数级别的运行效率的提升。

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int x : nums) {
            sum += x;
        }

        int aim = (sum + target) / 2;
        if (aim < 0 || (sum + target) % 2 == 1) {
            return 0;
        }

        int[] dp = new int[aim + 1];
        dp[0] = 1;

        for (int i = 1; i <= nums.length; i++) {

            for (int j = aim; j >= nums[i - 1]; j--) {
                dp[j] += dp[j - nums[i - 1]];
            }
        }

        return dp[aim];
    }
}

优化后的运行效率:
在这里插入图片描述

题目链接
时间复杂度:O(N^2)
优化前空间复杂度:O(N^2)
优化后空间复杂度:O(N)

3. 最后一块石头的重量 II(1049)

题目描述:
在这里插入图片描述
状态表示:
这题也是一样需要先将题目转换成一个01背包的问题。题目中的意思就是每次都会选两个数字,如果两个数字相等那么就直接消除两个数字,如果不等就保留成一个数字,数字的值为两数字差的绝对值。假如我们stones数组中有abcde这样五个数字,那么这个过程就如下图。首先拿出来b和d,然后是a和c…
在这里插入图片描述
从上图的过程中我们可以发现,最终数组中数字间的关系和上一题目标和极为相似,也是可以将stones数组分为两个部分,一般加正号,一半加负号。因为题目要求最终剩下的值是最小值,就说明上面说明的stones的两部分的绝对值越接近越好,所以我们可以将这题转换成一个01背包问题,先求出stones数组中的数字的和sum,然后去stones数组里面取数字,然后使得取出数字的和尽量逼近sum/2。因此我们建立二维数组dp,让dp[i][j]去表示在前i个数字中能够取到的接近j的最大数字和。
状态转移方程:
状态转移方程和前几题是很类似的,就是对0到i区间的最后一个数字也就是i位置的数字进行分析,当选择了第i个数字,那么此时就要进行判断,如果说j-stones[i]>=0那么就可以得到dp[i][j]=dp[i-1][j-stones[i]]+stones[i];当没选择第i个数字,那么此时dp[i][j]=dp[i-1][j]。最终dp[i][j]=max(dp[i-1][j-stones[i]]+stones[i],dp[i-1][j])。
初始化:
初始化也是类似,加上第0行和第0列,对于第0行意味着i等于0此时没有数字那么和肯定都是0,所以不难理解第0行全部赋为0。第0列无需初始化直接参与计算即可,原因可以看上一题的初始化。
填表顺序:
从上至下,从左至右。
返回值:
我们建立的dp数组的dp[n][sum/2]最终返回的只是一个部分的数字的和,另一个部分就是sum-dp[n][sum/2],然后根据题目的意思返回值就是它们俩的差,因为此时dp[n][sum/2]已经是最接近sum/2的和了,所以两个部分的和在此时是最相近的进而两者之差也是最小的。最终返回时还要对这个差进行一个绝对值的操作,最终返回的就是Math.abs(dp[n][sum/2] - (sum-dp[n][sum/2] ));
代码如下:

class Solution {
    public int lastStoneWeightII(int[] stones) {
        int n = stones.length;
        int sum = 0;

        for (int x : stones) {
            sum += x;
        }
        int aim = sum / 2;

        int[][] dp = new int[n + 1][aim + 1];

        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= aim; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j >= stones[i - 1]) {
                    dp[i][j] = Math.max(dp[i - 1][j - stones[i - 1]] + stones[i - 1], dp[i][j]);
                }

            }
        }
        return Math.abs(2 * dp[n][aim] - sum);
    }
}

此时的运行效率:
在这里插入图片描述

优化后代码:

class Solution {
    public int lastStoneWeightII(int[] stones) {
        int n = stones.length;
        int sum = 0;

        for (int x : stones) {
            sum += x;
        }
        int aim = sum / 2;

        int[] dp = new int[aim + 1];

        for (int i = 1; i <= n; i++) {
            for (int j = aim; j >= 0; j--) {
                if (j >= stones[i - 1]) {
                    dp[j] = Math.max(dp[j - stones[i - 1]] + stones[i - 1], dp[j]);
                }

            }
        }
        return Math.abs(2 * dp[aim] - sum);
    }
}

优化后的运行效率:
在这里插入图片描述
题目链接
时间复杂度:O(N^2)
优化前空间复杂度:O(N^2)
优化后空间复杂度:O(N)

  • 27
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值