常用算法整理:动态规划中篇

上一章讲了动态对话的一些 Easy 和 Medium 难度的题,如果会做这些题目,远远谈不上掌握了动态规划,最多只能算“略懂皮毛”。

这里我们挑几个更复杂的动规题目,全部是 Hard 难度的题,并且全部都是双序列问题,能把这些题目做到 bug free 才算是 基本掌握了动态规划。

第一题

题目地址:https://leetcode.com/problems/edit-distance/ 
大意就是对一个单词进行最少的操作变成另一个单词,求最少的操作次数。

先分析题目,为什么这一题会用DP来解。首先看到 求最少操作次数,这种十有八九就是DP,然后分析题目,发现其实很容易把这个问题分解为若干个小问题来解,并且每一步都依赖上一步的结果。

还是针对前面说的两个难点:

1, 如何定义 dp[i][j]

dp[i][j] 我们可以有两种最常见的定义:

  • word1 的 0~i 个字符变成 word2 的 0~j 个字符需要的最少操作次数
  • word1 的前 i 个 字符变成 word2 的前 j 个字符需要的最少操作次数

这里我们会选择第二种定义。因为其实初始值不是有一个字符,而是一个字符都没有,如果用第一种定义,初始值0其实就代表了第一个字符,那么我们就不好处理一个字符都没有的情况。因此我们选择第二种定义。

2,如何求解 dp[i][j]

显然 dp[i][[j] 依赖于前面的结果,并且和当前 word1[i] 和 word2[j] 是否相等有关,我们来分情况讨论

  • word1[i-1] !== word2[j-1] ,这种情况下,我们有三种选择可以做:在word1上删除第i个,在word2上删除第j个,或者把word1[i]替换成word2[j],这三种情况都需要额外的一步操作,所以我们只需要比较这三种情况所依赖的前提条件的大小即可,即求 Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
  • word1[i-1] === word2[j-1],这种情况会简单一点,因为最后一个已经相等了,所以不需要额外的操作,所以其实就和 dp[i-1][j-1] 是一样的。

然后是第三个难点,如何设置初始值,没有特别强调这个不是因为它不重要,而是想清楚了前两个问题之后,自然就知道怎么设置初始值了。其实这里就是对 i===0 和 j===0 的情况设置初始值。

完整的代码如下:

var minDistance = function(word1, word2) {
    var dp = [];

    for(var i=0;i<=word1.length;i++) {
        var row = [];
        for(var j=0;j<=word2.length;j++) {
            if(i===0) row.push(j);
            else if(j===0) row.push(i);
            else row.push(0);
        }
        dp.push(row);
    }

    for(i=1;i<=word1.length;i++) {
        for(j=1;j<=word2.length;j++) {
            if(word2[j-1] === word1[i-1]) {
                dp[i][j] = dp[i-1][j-1];
            } else {
                dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])+1;
            }
        }
    }

    return dp[word1.length][word2.length];
};

可能有些人对 相等的情况下 dp[i][j] = dp[i-1][j-1]; 不太放心,感觉可能这个不是最优解,那么没关系,这里也可以把各种情况都比较一下取一个最小值。事实上会发现 dp[i-1][j-1] 永远是最小值,其实原因就是对两个相同的字符不做任何操作总是最优方案。

第二题

题目地址:https://leetcode.com/problems/distinct-subsequences/ 
很经典的一个子序列问题,其实仔细想和上一题挺像的。通过上面一题可以看出,对这种两个序列的问题,基本上 dp[i][j] 的定义都是 前 i,j 个元素而不是 第 i, j 个元素,因为我们的起始点总是没有元素的情况,而 第 0,0个元素 其实两个序列各有一个元素。

所以这一题我们依然沿用这个定义。

那么怎么求解 dp[i][j]呢,显然依然要分情况讨论:

  • T[i-1] === S[j-1] 这种情况下,分两种情况,取s[j] 和 不取 S[j],为什么可以不取 S[j] ? 因为 可能j前面还有字母和 T[i]相等的。两种情况分别是 dp[i-1][j-1] 和 dp[i][j-1]
  • T[i-1] !== S[j-1] 只有一种可能了,就是不取 S[j],也就是 dp[i][j-1]。

代码如下:

var numDistinct = function(s, t) {
    var dp = [];

    for(var i=0;i<=t.length;i++) {
        var row = [];
        for(var j=0;j<=s.length;j++) {
            if(i===0) row.push(1);
            else row.push(0);
        }
        dp.push(row);
    }

    for(i=1;i<=t.length;i++) {
        for(j=i;j<=s.length;j++) {
            if(t[i-1] === s[j-1]) {
                dp[i][j] = dp[i-1][j-1] + dp[i][j-1];
            } else {
                dp[i][j] = dp[i][j-1];
            }
        }
    }

    return dp[t.length][s.length];
};

第三题

这个题目猛一看会比上面的两个难一些,因为他有三个字符串。我们需要用 dp[i][j][k] 表示s1的前i个字符和s2的前j个字符组成s3的前k个字符的吗,这样时间复杂度直接上升到 O(n^3) 显然是不行的。 
仔细看因为其实 k===i+j,所以其实我们只需要这样定义就行了:

dp[i][j] 表示s1的前i个字符和s2的前j个字符组成s3的前i+j个字符

那么这个定义好了之后,我们怎么求解 dp[i][j]。其实因为是按顺序交叉,所以 s3的最后一个字符要么来自 s1 要么来自 s2,分情况讨论

  • s1[i-1] === s3[i+j-1] && s2[j-1] === s3[i+j-1] ,双方最后一个字符都相等,s3的最后一个字符即可以从s1取也可以从s2取,那么只要有一种情况满足就行了就行了 dp[i][j] = dp[i-1][j] || dp[i][j-1];
  • s1[i-1] === s3[i+j-1],只能从s1取, dp[i][j] = dp[i-1][j];
  • s2[i-1] === s3[i+j-1],只能从s2取, dp[i][j] = dp[i][j-1];
  • dp[i][j] = false;

另外这一题特别要注意初始化的问题,dp[0][j] 和 dp[i][0] 的初始化都要注意。

代码如下:

var isInterleave = function(s1, s2, s3) {
    if(s3.length !== (s1.length + s2.length)) return false;
    if(s3.length === 0) return true;

    var dp = [];

    for(var i=0;i<=s1.length;i++) {
        var row = [];
        for(var j=0;j<=s2.length;j++) {
            if(i===0) row.push(s2.slice(0,j) === s3.slice(0,i+j));
            else if(j===0) row.push(s1.slice(0,i) === s3.slice(0,i+j));
            else row.push(false);
        }
        dp.push(row);
    }

    for(i=1;i<=s1.length;i++) {
        for(j=1;j<=s2.length;j++) {
            if(s1[i-1] === s3[i+j-1] && s2[j-1] === s3[i+j-1]) {
                dp[i][j] = dp[i-1][j] || dp[i][j-1];
            } else if(s1[i-1] === s3[i+j-1]) {
                dp[i][j] = dp[i-1][j];
            } else if(s2[j-1] === s3[i+j-1]) {
                dp[i][j] = dp[i][j-1];
            } else {
                dp[i][j] = false;
            }
        }
    }

    return dp[s1.length][s2.length];
};

除了双序列都是 hard 难度的之外,下一章我们再讲一下其他几种 hard 难度的动规题。

https://leetcode.com/problems/maximal-rectangle/

https://leetcode.com/problems/longest-valid-parentheses/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值