leetcode与数据结构---动态规划总结(一)

这几天一直在做leetcode上关于动态规划方面的题目,虽然大二下的算法设计课上较为详细的讲过动态规划,奈何遇到新颖的题目或者稍加背景的题目立刻就原形毕露不知题目所云了。动态规划算是较难的一个专题了,但只要找到递推关系其最终的代码又相当简便。现在把这几天做过的题目整理总结一下,毕竟只求做题数量不求掌握精髓最终也没法提升自己的能力的。

从简单入手

先从简单的题目入手吧,代表题目:64.最小路径和120.三角形最小路径和62.不同路径63.不同路径II

以64题和62题为例,我们先看64题的的题目描述:


给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例:

输入:
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。

题目中说到路径从左下角到右下角,言下之意即为从路径的一个节点到下一个节点,只能向下走或者向右走,可以看出这是一个非常明显的暗示了。如下图:

          (i-1,j)
            |
            v
(i,j-1)-> (i, j)           

我们想要到达下一个节点(i,j),那我们是选择从节点(i, j-1)走过来呢还是从节点(i-1,j)走过来呢?既然题目要求路径和最小,那么我们肯定选择上节点对应路径值最小的作为中间节点。于是可以写出如下的递推式:

dp(i,j)=min(dp(i,j1),dp(i1,j))+v(i,j) d p ( i , j ) = m i n ( d p ( i , j − 1 ) , d p ( i − 1 , j ) ) + v ( i , j )

当然还有一些小细节要处理,当节点位于 i=0( j=0)的时候,是不可能从该节点的左边(该节点的上边走过来的),此时只有一种选择,因此要对这两种情况单独处理。整理一下,可得如下代码:

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int h = grid.size();
        int w = grid[0].size();
        for (int i=0; i<h; ++i){
            for (int j=0; j<w;++j){
                if (i==0 && j==0)
                    continue;
                if (i==0){
                    grid[0][j] += grid[0][j-1]; continue;
                }
                if (j==0){
                    grid[i][0] += grid[i-1][0]; continue;
                }
                grid[i][j] += min(grid[i-1][j], grid[i][j-1]);
            }
        }
        return grid[h-1][w-1];
    }
};

120题三角形最短路径和与该题类似,基本采用同样的方法。那么我们再来看看62题,62题的题目描述如下:


一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不同的路径?

img

例如,上图是一个7 x 3 的网格。有多少可能的路径?

说明mn 的值均不超过 100。

示例 1:

输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右

和64题大同小异,只是这次需要求解的是多少种可能的路径,同样我们注意到走到节点(i,j)有两种走法,一种是通过(i-1, j)从左边走过来,一种是通过(i, j-1)从上面走下来。于是这便提示我们建立一个二维数组dp(m, n),用于存储到达节点(i, j)时的路径数量。那么可以写出如下的递推关系:

dp(i,j)=dp(i1,j)+dp(i,j1) d p ( i , j ) = d p ( i − 1 , j ) + d p ( i , j − 1 )

同理,我们需要注意对边缘的处理,即 i=0j=0的情况,边缘只有一条路径。经过如上分析以后我们可以写出如下代码:

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> num(m, vector<int>(n, 0));
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (i == 0 || j == 0) {
                    num[i][j] = 1;
                    continue;
                }                   
                num[i][j] = num[i][j - 1] + num[i-1][j];
            }
        }
        return num[m - 1][n - 1];
    }
};

提升下难度

以上题目类型都以图网络作为背景,涉及到图网络除了采用图的相应算法外动态规划也是很好的解决方式,而以上题目动态规划的意图比较明显,也比较容易写出递推关系。下面题目在以上题目的基础上加深了难度,需要一定的技巧,代表题目:174.地下城游戏741.摘樱桃

我们先来看看174的题目描述:


一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。

骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。

有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。

为了尽快到达公主,骑士决定每次只向右或向下移动一步。

编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。

例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7

-2 (K)-33
-5-101
1030-5 (P)

说明:

  • 骑士的健康点数没有上限。
  • 任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。

说了一大堆,把没有用的话剔除,题目仍然是一道以图网络为背景的题目。即有一个图网络mxn其节点的权重有正有负。骑士K需要从(0, 0)节点走到(m-1, n-1)节点,只能选择向下或者向右走。骑士K初始时有一定的“生命值”,且每经过一个节点其“生命值”加上该节点的权重,并且在过程中骑士K的“生命值”必须为正,然后要我们求满足条件的最小“生命值”。

看上去比之前的题目确实复杂了不少,初步想法是建立一个二维数组dp(m, n)用于存储每个节点对应的最小生命值.我们还是看一个具体的过程来分析:

(i, j) <- (i+1,j)
   ^          
   |       
(i,j+1)        

还是三个相邻节点:(i+1,j)(i, j+1)(i, j)。整个过程可以看做是从节点(m-1,n-1)反着走到(0, 0)节点。为什么要反着想呢?因为题目要求是求出发时至少要多少生命值,即dp(0, 0) ,因此逆向走回去恰好可以求得(0, 0)点对应的生命值。那么至少现在可以写出如下递推式:

dp(i,j)=min(dp(i,j+1),dp(i+1,j))v(i,j) d p ( i , j ) = m i n ( d p ( i , j + 1 ) , d p ( i + 1 , j ) ) − v ( i , j )

但有一个问题,因为在路途过程中生命值始终不能低于0,最小都要为1,因此以上递推式应该还要加上一个约束条件,即为:
dp(i,j)=max(dp(i,j),1) d p ( i , j ) = m a x ( d p ( i , j ) , 1 )

同样我们需要注意对边缘的处理,因此最终答案如下:

class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        int h = dungeon.size();
        int w = dungeon[0].size();
        vector<vector<int>> OPT(h, vector<int>(w, 0));
        OPT[h-1][w-1] = dungeon[h-1][w-1] < 0 ? -dungeon[h-1][w-1]+1:1;
        for (int i=w-2; i>=0;--i) {
            OPT[h-1][i] = max(OPT[h-1][i+1]-dungeon[h-1][i], 1);
        }
        for (int i=h-2;i>=0;--i){
            OPT[i][w-1] = max(OPT[i+1][w-1]-dungeon[i][w-1], 1);
        }
        for (int i=h-2; i>=0;--i){
            for (int j=w-2; j>=0;--j){
                int right = max(OPT[i+1][j]-dungeon[i][j], 1);
                int down = max(OPT[i][j+1]-dungeon[i][j], 1);
                OPT[i][j] = min(right, down);
            }
        }
        return OPT[0][0];
    }
};

字符串为背景的题目

以字符串为背景的动态规划题目就很多了,包括回文串、字串啊、子序列等等。这里简单总结一下近期做过的相关题目:91.解码方法、72.编辑距离、139.单词拆分、140.单词拆分II、514.自由之路、516.最长回文子序列647.回文子串5.最长回文字串

先从回文串入手,首先需要注意字串和子序列是有区别的,字串必须连续,而子序列可以不连续。回文串是一个对称的字符串。知道一些基本概念后先来看第516题的题目描述:


给定一个字符串s,找到其中最长的回文子序列。可以假设s的最大长度为1000

示例 1:
输入:

"bbbab"

输出:

4

一个可能的最长回文子序列为 “bbbb”。

示例 2:
输入:

"cbbd"

输出:

2

一个可能的最长回文子序列为 “bb”。


碰到子序列即可以不连续的情况常常让人发难,觉得无从下手。题目要返回最长的回文子序列,经过前面几题的套路,我们可以想到建立一个二维数组dp其维度为MxM,其中M代表字符串的长度。那么dp(i, j)的含义为从字符串i到j的范围回文子序列的最大长度。我们举一个具体的例子:对于字符串bbacddccadd

可以知道:

dp(9, 10)=2, dp(8, 10)=2, dp(7, 10)=2, dp(6, 10)=2, dp(5, 10)=3, dp(4, 10) =5

dp(8, 9) =1, dp(7, 9)=1, dp(6, 9)=2, dp(5, 9)=4

… … … …

可以发现dp(i, j)的大小与dp(i+1, j)、dp(i, j-1)以及dp(i+1, j-1)有关系。而回文字符串的关键在于对称性,因此我们可以得出如下递推关系:

s[i] == s[j]时,有:

dp(i,j)=2+dp(i+1,j1) d p ( i , j ) = 2 + d p ( i + 1 , j − 1 )

其他时候则是:
dp(i,j)=max(dp(i+1,j),dp(i,j1)) d p ( i , j ) = m a x ( d p ( i + 1 , j ) , d p ( i , j − 1 ) )

因此可以写出如下代码:

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        if (s.empty())
            return 0;
        int len = s.length();
        vector<vector<int>> OPT(len, vector<int>(len, 0));
        for (int i=len-1; i>=0; --i){
            for (int j=i; j<len; ++j) {
                if (j == i)
                    OPT[i][j] = 1;
                else if (s[j] == s[i])
                    OPT[i][j] = OPT[i+1][j-1] + 2;
                else
                    OPT[i][j] = max(OPT[i+1][j], OPT[i][j-1]);                
            }
        }
        return OPT[0][len-1];
    }
};

接下来我们来看看647题回文子串,其题目描述如下:


给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被计为是不同的子串。

示例 1:

输入: "abc"
输出: 3
解释: 三个回文子串: "a", "b", "c".

示例 2:

输入: "aaa"
输出: 6
说明: 6个回文子串: "a", "a", "a", "aa", "aa", "aaa".

注意:

  1. 输入的字符串长度不会超过1000。

与上题不同之处在于不再是子序列而是字串,并且要求的不是最大长度而是字串的个数。那么首先我们需要建立一个二维数组dp,维度为MxM,M为字符串长度。那么dp(i,j)则代表字符串从i到j的回文字串个数。看上去好像蛮简单的样子,那么我们继续分析,用一个实例:

aiai+1ai+2......aj1aj:abccbbaai+1ai+2......aj1aj:bccbbaaiai+1ai+2......aj1:abccbbai+1ai+2......aj1:bccbb a i a i + 1 a i + 2 . . . . . . a j − 1 a j : a b c c b b a a i + 1 a i + 2 . . . . . . a j − 1 a j : b c c b b a a i a i + 1 a i + 2 . . . . . . a j − 1 : a b c c b b a i + 1 a i + 2 . . . . . . a j − 1 : b c c b b

可以知道:dp(1,6) = 9, dp(0, 5) = 9, dp(1,4) = 8, dp(0, 6) = 10

即dp(0, 6) = dp(1, 6) + dp(0, 5) - dp(1, 4)。这也可以非常直观的理解,即最后要减去公共部分重复计算的。那么我们可以得到如下递推式:

dp(i,j)=dp(i+1,j)+dp(i,j1)dp(i+1,j1) d p ( i , j ) = d p ( i + 1 , j ) + d p ( i , j − 1 ) − d p ( i + 1 , j − 1 )

但是这并没有结束,因为最终并不知道i到j整个字符串是否为回文串,因此还要进行判断。然而如果一个一个字符进行比对的话必定超时,因此在整个过程中我们可以事先用一个布尔类型的二维数组 OPT(i,j)表示字符串从i到j是否为一个回文字符串,因此我们有:

s[i]==s[j] s [ i ] == s [ j ] 并且 dp(i1,j+1)==true d p ( i − 1 , j + 1 ) == t r u e 时:

dp(i,j)=dp(i,j)+1 d p ( i , j ) = d p ( i , j ) + 1

因此代码如下:

class Solution {
public:
    int countSubstrings(string s) {
        if (s.empty())
            return 0;
        int len = s.length();
        vector<vector<int>> OPT(len, vector<int>(len, 0));
        vector<vector<bool>> record(len, vector<bool>(len, false));
        for (int i=len-1; i>=0; --i){
            for (int j=i; j<len; ++j){
                if (j == i){
                    OPT[i][j] = 1;
                    record[i][j] = true;
                }
                else if (j == i+1){
                    OPT[i][j] = s[i] == s[j] ? 3 : 2;
                    record[i][j] = (s[i] == s[j]);
                }
                else{
                    OPT[i][j] = OPT[i+1][j] + OPT[i][j-1]-OPT[i+1][j-1];
                    if (s[i] == s[j] && record[i+1][j-1]){
                        OPT[i][j]++;
                        record[i][j] = true;
                    }
                    else
                        record[i][j] = false;
                }
            }
        }
        return OPT[0][len-1];
    }
};
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值