动态规划---入门ⅤⅡ

本文探讨了动态规划在最长公共子串、背包问题和编辑距离等经典问题上的应用及优化。通过一维数组实现空间复杂度的降低,并介绍了如何转换问题以适应一维动态规划。同时,详细阐述了编辑距离问题的状态转移方程及其优化限制。
摘要由CSDN通过智能技术生成

最长公共子串

在这里插入图片描述

思路

在这里插入图片描述

代码

在这里插入图片描述

dp数组的表格
在这里插入图片描述

动态规划的优化

空间复杂度的优化,可以优化到只用一行也就是一维数组来动态规划。思路就是只用一行来记录,此外还需要一个变量来记录本次遍历中这个一维数组被覆盖掉的值,用来计算下一次的遍历。

除此之外,我们还可以挑选长度较短的字符串作为j(也就是作为列),这样一维数组的长度还可以进一步降低。

在这里插入图片描述

背包问题

在这里插入图片描述

思路

在这里插入图片描述

代码

在这里插入图片描述

动态规划时形成的二维数组
在这里插入图片描述

优化

从前我们计算二维数组的时候是从左往右计算的,那么在此次遍历的时候,我们就会用到遍历的数据的上面的数据(如果这个物品不带上的情况),或者这个数据上面一行左半边的任意一个数据(如果这个物品带上,哪一个数据取决于这个物品的重量)。在这种情况下,优化成一维数组是有难度的。
那么我们可以考虑换个方向来遍历,如果我们从右往左来遍历数组,那么问题就迎刃而解。
在这里插入图片描述

黄色的是现在一维数组里的数据,红色的圈代表我们现在正在计算的数据。

需要注意的是这里的i和j不再像之前一样可以我们自己选择较短的作为列。因为这里i和j的含义是不同的,i代表了当前能选择的是前i件,而j代表了当前背包能背的重量。在之前的题目中可以选择较短的作为列,是因为i和j代表的含义是一样的,他们都代表了字符串中第i/j个位置的字符。
在这里插入图片描述
在这里插入图片描述

再优化

在这里插入图片描述

之所以可以这样优化的原理是,当j已经小于weights【i - 1】的时候,说明现在的背包的承重量已经小于现在这个物体的重量,也就是说这个物体注定只能放弃,所以这时候的dp【j】就一定还是dp【j】。所以在后面的遍历都是没有意义的。

在这里插入图片描述

背包问题扩展

在这里插入图片描述

思路

在这里插入图片描述
在这里插入图片描述
之所以可以这样做是因为,我们给不可能凑到的值赋上无穷小之后,后续计算的时候如果出现凑不到的情况也会变成无穷小。

代码

在这里插入图片描述

01背包问题—分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

提示:

1 <= nums.length <= 200
1 <= nums[i] <= 100

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

思路

这题看起来和01背包问题没有一点关系,但是事实上是可以转换成01背包问题的,我们要从这个角度思考。等和的子集就意味着两个子集都刚好是数组总和的一半。我们把数组中每一个数字都看作背包里的每一个物品,他们的重量就是他们的值,我们背包的总承重是数组总和的一半。dp【i】【j】的意思是,i为可挑选的物品的件数,j为背包的承重。如果能刚好装完背包的承重j那么dp【i】【j】就是true,否则为false。

它的状态转移方程就是dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]
也就是说和01背包问题是一样的。如果我们不选择这个物品i,那么我们的结果就来源于dp【i-1】【j】。如果我们选择了物品i,那么我们的结果就来源于dp【i - 1】【j - nums【i】】。之所以是承重是j-nums【i】,是因为我们选择了物品i,所以在之前的件数中,我们就要选择减去了物品i的重量的情况。

代码

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum % 2 == 1) {
            return false;
        }
        int target = sum / 2;
        boolean[][] dp = new boolean[nums.length + 1][target + 1];

        for (int i = 1; i < nums.length + 1; i++) {
            for (int j = 1; j <= target; j++) {
                dp[i][j] = dp[i - 1][j];
                if (nums[i - 1] == j) {
                    dp[i][j] = true;
                    continue;
                }
                if (nums[i - 1] < j) {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
                }
            }
        }

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

优化

参考01背包问题可以优化成这样

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum % 2 == 1) {
            return false;
        }
        int target = sum / 2;
        boolean[] dp = new boolean[target + 1];

        for (int i = 1; i < nums.length + 1; i++) {
            for (int j = target; j >= nums[i - 1]; j--) {
                if (nums[i - 1] == j) {
                    dp[j] = true;
                    continue;
                }
                if (nums[i - 1] < j) {
                    dp[j] = dp[j] || dp[j - nums[i - 1]];
                }
            }
        }

        return dp[target];
        
    }
}

编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符

示例 1:

输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)

示例 2:

输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)

提示:

0 <= word1.length, word2.length <= 500
word1 和 word2 由小写英文字母组成

思路

这题也是动态规划题,一开始并没有思路也没有联想到这题是动态规划的解法。下面摘抄题解

我们可以对任意一个单词进行三种操作:

    插入一个字符;

    删除一个字符;

    替换一个字符。

题目给定了两个单词,设为 AB,这样我们就能够六种操作方法。

但我们可以发现,如果我们有单词 A 和单词 B:

    对单词 A 删除一个字符和对单词 B 插入一个字符是等价的。例如当单词 A 为 doge,单词 B 为 dog 时,我们既可以删除单词 A 的最后一个字符 e,得到相同的 dog,也可以在单词 B 末尾添加一个字符 e,得到相同的 doge;

    同理,对单词 B 删除一个字符和对单词 A 插入一个字符也是等价的;

    对单词 A 替换一个字符和对单词 B 替换一个字符是等价的。例如当单词 A 为 bat,单词 B 为 cat 时,我们修改单词 A 的第一个字母 b -> c,和修改单词 B 的第一个字母 c -> b 是等价的。

这样以来,本质不同的操作实际上只有三种:

    在单词 A 中插入一个字符;

    在单词 B 中插入一个字符;

    修改单词 A 的一个字符。

这样以来,我们就可以把原问题转化为规模较小的子问题。我们用 A = horse,B = ros 作为例子,来看一看是如何把这个问题转化为规模较小的若干子问题的。

    在单词 A 中插入一个字符:如果我们知道 horse 到 ro 的编辑距离为 a,那么显然 horse 到 ros 的编辑距离不会超过 a + 1。这是因为我们可以在 a 次操作后将 horse 和 ro 变为相同的字符串,只需要额外的 1 次操作,在单词 A 的末尾添加字符 s,就能在 a + 1 次操作后将 horse 和 ro 变为相同的字符串;

    在单词 B 中插入一个字符:如果我们知道 hors 到 ros 的编辑距离为 b,那么显然 horse 到 ros 的编辑距离不会超过 b + 1,原因同上;

    修改单词 A 的一个字符:如果我们知道 hors 到 ro 的编辑距离为 c,那么显然 horse 到 ros 的编辑距离不会超过 c + 1,原因同上。

那么从 horse 变成 ros 的编辑距离应该为 min(a + 1, b + 1, c + 1)。

注意:为什么我们总是在单词 AB 的末尾插入或者修改字符,能不能在其它的地方进行操作呢?答案是可以的,但是我们知道,操作的顺序是不影响最终的结果的。例如对于单词 cat,我们希望在 c 和 a 之间添加字符 d 并且将字符 t 修改为字符 b,那么这两个操作无论为什么顺序,都会得到最终的结果 cdab。

你可能觉得 horse 到 ro 这个问题也很难解决。但是没关系,我们可以继续用上面的方法拆分这个问题,对于这个问题拆分出来的所有子问题,我们也可以继续拆分,直到:

    字符串 A 为空,如从 转换到 ro,显然编辑距离为字符串 B 的长度,这里是 2;

    字符串 B 为空,如从 horse 转换到 ,显然编辑距离为字符串 A 的长度,这里是 5。

因此,我们就可以使用动态规划来解决这个问题了。我们用 D[i][j] 表示 A 的前 i 个字母和 B 的前 j 个字母之间的编辑距离。

如上所述,当我们获得 D[i][j-1]D[i-1][j]D[i-1][j-1] 的值之后就可以计算出 D[i][j]D[i][j-1]A 的前 i 个字符和 B 的前 j - 1 个字符编辑距离的子问题。即对于 B 的第 j 个字符,我们在 A 的末尾添加了一个相同的字符,那么 D[i][j] 最小可以为 D[i][j-1] + 1D[i-1][j]A 的前 i - 1 个字符和 B 的前 j 个字符编辑距离的子问题。即对于 A 的第 i 个字符,我们在 B 的末尾添加了一个相同的字符,那么 D[i][j] 最小可以为 D[i-1][j] + 1D[i-1][j-1]A 前 i - 1 个字符和 B 的前 j - 1 个字符编辑距离的子问题。即对于 B 的第 j 个字符,我们修改 A 的第 i 个字符使它们相同,那么 D[i][j] 最小可以为 D[i-1][j-1] + 1。特别地,如果 A 的第 i 个字符和 B 的第 j 个字符原本就相同,那么我们实际上不需要进行修改操作。在这种情况下,D[i][j] 最小可以为 D[i-1][j-1]。

那么我们可以写出如下的状态转移方程:

    若 AB 的最后一个字母相同:
D[i][j]=min(D[i][j−1]+1,D[i−1][j]+1,D[i−1][j−1])=1+min(D[i][j−1],D[i−1][j],D[i−1][j−1]1)​

若 AB 的最后一个字母不同:

D[i][j]=1+min⁡(D[i][j−1],D[i−1][j],D[i−1][j−1])

对于边界情况,一个空串和一个非空串的编辑距离为 D[i][0] = i 和 D[0][j] = j,D[i][0] 相当于对 word1 执行 i 次删除操作,D[0][j] 相当于对 word1执行 j 次插入操作。

综上我们得到了算法的全部流程。

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/edit-distance/solution/bian-ji-ju-chi-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


下面放上代码

class Solution {
    public int minDistance(String word1, String word2) {
        int n = word1.length();
        int m = word2.length();

        if (n * m == 0) {
            return n + m;
        }
        int dp[][] = new int[n + 1][m + 1];

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

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                int one = dp[i - 1][j] + 1;
                int two = dp[i][j - 1] + 1;
                int three = dp[i - 1][j - 1];
                if (!(word1.charAt(i - 1) == word2.charAt(j - 1))) {
                    three++;
                }
                dp[i][j] = Math.min(Math.min(one, two), three);
            }
        }
        return dp[n][m];
    }
}

需要提起的是,这题的状态转移方程不止来源于上一行,也来源于本行的上一次遍历的结果,所以不能把空间复杂度优化成一行,最多只能优化到两行。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值