动态规划---入门ⅤⅢ

今天看的是01背包问题的相似题目

目标和

给你一个整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入:nums = [1], target = 1
输出:1

提示:

1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/target-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析

这题是01背包问题的相似题目,虽然我一开始并没有看出来。看了很久的题解之后对这题有了一点理解。

下面摘抄一部分题解的内容:

如何转化为01背包问题呢。

假设加法的总和为x,那么减法对应的总和就是sum - x。

所以我们要求的是 x - (sum - x) = S

x = (S + sum) / 2

此时问题就转化为,装满容量为x背包,有几种方法。

大家看到(S + sum) / 2 应该担心计算的过程中向下取整有没有影响。

这么担心就对了,例如sum 是5,S是2的话其实就是无解的,所以:

if ((S + sum) % 2 == 1) return 0; // 此时没有方案

看到这种表达式,应该本能的反应,两个int相加数值可能溢出的问题,当然本题并没有溢出。

再回归到01背包问题,为什么是01背包呢?

因为每个物品(题目中的1)只用一次!

这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。

本题则是装满有几种方法。其实这就是一个组合问题了。

确定dp数组以及下标的含义

dp[j] 表示:填满j(包括j)这么大容积的包,有dp[i]种方法

其实也可以使用二维dp数组来求解本题,dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。

确定递推公式

有哪些来源可以推出dp[j]呢?

不考虑nums[i]的情况下,填满容量为j - nums[i]的背包,有dp[j - nums[i]]种方法。

那么只要搞到nums[i]的话,凑成dp[j]就有dp[j - nums[i]] 种方法。

举一个例子,nums[i] = 2: dp[3],填满背包容量为3的话,有dp[3]种方法。

那么只需要搞到一个2(nums[i]),有dp[3]方法可以凑齐容量为3的背包,相应的就有多少种方法可以凑齐容量为5的背包。

那么需要把 这些方法累加起来就可以了,dp[j] += dp[j - nums[i]]

所以求组合类问题的公式,都是类似这种:

dp[j] += dp[j - nums[i]]

这个公式在后面在讲解背包解决排列组合问题的时候还会用到!

dp数组如何初始化

从递归公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递归结果将都是0。

dp[0] = 1,理论上也很好解释,装满容量为0的背包,有1种方法,就是装0件物品。

dp[j]其他下标对应的数值应该初始化为0,从递归公式也可以看出,dp[j]要保证是0的初始值,才能正确的由dp[j - nums[i]]推导出来。

确定遍历顺序

举例推导dp数组

输入:nums: [1, 1, 1, 1, 1], S: 3

bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4

在这里插入图片描述

作者:carlsun-2
链接:https://leetcode-cn.com/problems/target-sum/solution/dai-ma-sui-xiang-lu-494-mu-biao-he-01bei-rte9/
来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

理解

这里对于这题,我是这样理解的。我们想要求出目标和,那么我们允许加的x的值其实是固定的,剩下的是要减去的值,关于x的值我们可以通过假设加法的总和为x,那么减法对应的总和就是sum - x。

所以我们要求的是 x - (sum - x) = S

x = (S + sum) / 2

此时问题就转化为,装满容量为x背包,
类似题解的方法来获得。
这时候nums里的每一个值其实就相当于原始的背包问题里的每一个物品的weight,我们要通过这些weight来凑到x。x其实就相当于原始背包问题里背包的负重。这时候问题就已经和01背包问题相似了。

接下来是转换方程,对于转换方程的理解是这样的:

dp[j]代表的意义:填满容量为j的背包,有dp[j]种方法。因为填满容量为0的背包有且只有一种方法,所以dp[0] = 1
状态转移:dp[j] = dp[j] + dp[j - num],
当前填满容量为j的包的方法数 = 之前填满容量为j的包的方法数 + 之前填满容量为j - num的包的方法数
也就是当前数num的加入,可以把之前和为j - num的方法数加入进来。

感觉还是抽象,对于背包问题的理解还有待提高

代码

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if ((target + sum) % 2 == 1)return 0;
        int bagsize = (target + sum) / 2;
        int[] dp = new int[bagsize + 1];
        dp[0] = 1;
        for (int i = 0; i < nums.length; i++) {
            for (int j = bagsize; j >= nums[i]; j--) {
                dp[j] = dp[j] + dp[j - nums[i]];
            }
        }
        return dp[bagsize];
    }
}

下面的一题虽然不是背包问题,但是也是动态规划的范畴,一并收录

戳气球

有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。

求所能获得硬币的最大数量。

示例 1:

输入:nums = [3,1,5,8]
输出:167
解释:
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 315 + 358 + 138 + 181 = 167

示例 2:

输入:nums = [1,5]
输出:10

提示:

n == nums.length
1 <= n <= 500
0 <= nums[i] <= 100

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/burst-balloons
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

这题毕竟是一个hard题目,所以思路并不清晰。我也是思索了没有头绪, 下面贴上一个看完觉得讲解清晰的题解。

https://leetcode-cn.com/problems/burst-balloons/solution/zhe-ge-cai-pu-zi-ji-zai-jia-ye-neng-zuo-guan-jian-/

这里提一点自己理解中有困难的地方.
首先是动态规划数组设定的理解。这里的dp【i】【j】表示的i - 1,j - 1中最大的获得硬币数,其中戳气球不能戳头尾。里面的k是我们自己设定的,我们在遍历的时候,首先确定ij的长度,也就是戳的气球的范围range。接下来,我们从0开始遍历i,这里i的最大值是newlength - range,是因为i是我们戳气球的起点,而气球的范围是range,所以我们最后能戳气球的起点就是newlength - range。一旦确定了起点i,因为戳气球的范围range是确定的,所以我们戳气球的终点j也就确定了。
从这时开始我们就可以去设定k来遍历其中每一个可以最后戳的位置了,最后找到的最大值就是dp【i】【j】的值了。

代码

class Solution {
    public int maxCoins(int[] nums) {
        int numslength = nums.length;
        int newlength = numslength + 2;
        int[][] dp = new int[newlength][newlength];

        int[] newarray = new int[newlength];
        newarray[0] = 1;
        newarray[newlength - 1] = 1;
        for (int i = 1; i <= numslength; i++) {
            newarray[i] = nums[i - 1];
        }

        for (int range = 3; range <= newlength; range++) {
            for (int i = 0; i <= newlength - range; i++) {
                int j = i + range - 1;
                int max = 0;
                for (int k = i + 1; k < j; k++) {
                    int leftvalue = dp[i][k];
                    int rightvalue = dp[k][j];
                    max = Math.max(max, leftvalue + rightvalue + newarray[i] * newarray[j] * newarray[k]);
                }
                dp[i][j] = max;
            }
        }

        return dp[0][newlength - 1];
    }
}

正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。

'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

示例 1:

输入:s = “aa” p = “a”
输出:false
解释:“a” 无法匹配 “aa” 整个字符串。

示例 2:

输入:s = “aa” p = “a*”
输出:true
解释:因为 ‘*’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。

示例 3:

输入:s = “ab” p = “."
输出:true
解释:".
” 表示可匹配零个或多个(’*’)任意字符(’.’)。

示例 4:

输入:s = “aab” p = “cab”
输出:true
解释:因为 ‘*’ 表示零个或多个,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。

示例 5:

输入:s = “mississippi” p = “misisp*.”
输出:false

提示:

0 <= s.length <= 20
0 <= p.length <= 30
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
保证每次出现字符 * 时,前面都匹配到有效的字符

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/regular-expression-matching
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

先确定动态规划的状态定义,i和j表示字符串的角标。dp【i】【j】表示字符串s以i结尾和字符串p以j结尾时,匹配是否成功。
下面放上看到的很好的解释

    如果 p.charAt(j) == s.charAt(i) : dp[i][j] = dp[i-1][j-1];
    如果 p.charAt(j) == '.' : dp[i][j] = dp[i-1][j-1];
    如果 p.charAt(j) == '*':
        如果 p.charAt(j-1) != s.charAt(i) : dp[i][j] = dp[i][j-2] //in this case, a* only counts as empty
        如果 p.charAt(i-1) == s.charAt(i) or p.charAt(i-1) == '.':
            dp[i][j] = dp[i-1][j] //in this case, a* counts as multiple a
            or dp[i][j] = dp[i][j-1] // in this case, a* counts as single a
            or dp[i][j] = dp[i][j-2] // in this case, a* counts as empty

这里补充说明,之所以在字符为**的时候,如果判定j-1和i的字符相同之后有三种情况的原因是*可以变成零个或多个前一个字符,所以他可能是变成多个字符,或者一个字符,或者零个字符三种情况。而如果j-1和i的字符不同,那么就只能变成零个字符这一种情况。

代码

class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length();
        int n = p.length();
        boolean dp[][] = new boolean[m + 1][n + 1];
        dp[0][0] = true;

        for (int i = 0; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (p.charAt(j - 1) == '*') {
                    if (matches(s, p, i, j - 1)) {
                        dp[i][j] = dp[i - 1][j] || dp[i][j - 2] || dp[i][j - 1];
                    } else {
                        dp[i][j] = dp[i][j - 2];
                    }
                } else {
                    if (matches(s, p, i, j)) {
                        dp[i][j] = dp[i - 1][j - 1];
                    }
                }
            }
        }   
        return dp[m][n];
    }
    public boolean matches(String s, String p, int i, int j) {
        if (i == 0) {
            return false;
        }
        if (p.charAt(j - 1) == '.') {
            return true;
        }
        return s.charAt(i - 1) == p.charAt(j - 1);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值