算法学习之动态规划


前几天在力扣刷题时遇到了动态规划的问题,看了题解之后有些兴趣便自己下去多了解了一下,在此分享给大家(另外给的例题都是自己做过的,然后思考了下顺便分享下,希望大家喜欢~

本篇博客将分为两个部分:

  • 对动态规划进行阐述
  • 配合一些动态规划的题目进一步了解

那么接下来就让我们开始吧~


引入

定义

动态规划(Dynamic programming,简称 DP)中本阶段的状态往往是上一阶段状态和上一阶段决策的结果。换言之就是原问题可以拆解成若干个子问题,而这些子问题又可拆解……最后可以由初始问题的解来推出原问题的解。

而这之中拆解的过程又是耐人寻味的且有趣的。

使用要素(划重点!)

  • 最优子结构:如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。换言之,总问题包含很多个子问题,而这些子问题的解也是最优的。
  • 子问题的重叠性:子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。而动态规划利用了这个性质,对每一个子问题都计算一次,将结果保存在一个表格中(例如数组),之后要用的时候取出来就行,就不必再重新计算,提高了效率。

也许到这你还是对动态规划不是很理解,那么就来谈谈这当中的经典问题:爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

初看觉得可以用排列组合来解,但那样的话n 越大越难解,效率也会很低

这时就要提到动态规划了:

n阶台阶可以分为n-1阶台阶走一阶台阶和n-2阶台阶走两步,所以,n阶台阶的走法就是n-1阶台阶走法加上n-2阶台阶走法。这时可得状态转移方程:F(n) = F(n-1) + F(n-2) (n >= 3)(这里如果是n-2阶台阶走1 1 两次的话,又会回到n-1阶台阶上)

将原问题进行拆解,也就是随着n 的减少,到最后剩下1阶台阶和2阶台阶,而1阶台阶只能是走1步,2阶台阶可以是1 1 走两次和走2步到,所以可得F(1) = 1, F(2) = 2,这就是问题的边界

可以看出,从最开始的问题解可以逐渐推出最终问题的解,妙啊~

所以动态规划问题的精髓就在于状态转移方程


相关题目

LeetCode 面试题 17.16. 按摩师

算法学习之动态规划_第1张图片

初看完题目,可以确定的是一个状态所能达到的最长总时间与之前两个状态和本状态的分钟数有关

观察题目,因为是不能接收相邻的预约,而且预约的总时间要最长,所以第i次、第i-1次和第i-2次预约就有了关系:

  • 如果接收第i次预约,那么第i-1次预约就休息,此时i-2次预约所能达到的最长总时间 加上第i次预约的时间nums[i]就得大于第i-1次预约所能达到的最长总时间
  • 如果不接受第i次预约,那么第i-1次预约的状态就不确定(由i-1次预约前的总时间数确定,但i-1次预约所能达到的最长总时间数 是可以确定的),此时i-2次预约所能达到的最长总时间 加上第i次预约的时间nums[i]就得小于第i-1次预约所能达到的最长总时间

定义dp[i][0,i]区间内预约所能达到的最长总时间数

所以这时的状态转移方程就为dp[i] = max(dp[i-2] + nums[i], dp[i-1])

随着i 的减少,最后来到了边界上:i = 0i = 1i = 2即可由这两个值所得出)

  • i = 0时,因为之前没有总时间数所限制,此时dp[0] = nums[0]
  • i = 1时,因为和i = 0是相邻的,此时dp[1] = max(nums[0], nums[1])

到这里本题差不多就解完了,再贴出部分代码以帮助大家理解:

int len = nums.length;
int[] dp = new int[len];

dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);

for(int i = 2;i < len;i++){
	dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
}

LeetCode 32. 最长有效括号

算法学习之动态规划_第2张图片

这里先解释一下有效括号字符串:仅由 "("")" 构成的字符串,对于每个左括号,都能找到与之对应的右括号,反之亦然。换句话说,一个有效括号是由")"结尾的。

要注意的是题目找的是最长的有效括号的子串,也就是说满足定义的子串如果不连续就只需要最长的,举个例子:"()))()())",这里应该输出4。

根据题目给的例子可以看到,遇到"......()"这样的,最长有效括号长度就加2,另外可能还有"......(())"这样的,也是满足有效括号的定义的,这里就直接加4。

可以发现,当前元素为"("时,有效括号长度并不会发生变化,因此可以从")"寻找出dp方程的一个关系,这里可以定义dp[i]的值为以第i个元素结尾时,最长有效括号子字符串的长度,这样的话就会出现两种情况(这里为了简化,dp[i]"("时,其值就直接为 0):

  • 当元素")"前一个元素为"("时,也就是形如"......()"这样的子字符串,可以推出:

    dp[i] = dp[i - 2] + 2
  • 当元素")"前一个元素为")"时,也就是形如"......))"这样的子字符串。这里就要稍微复杂些,因为还要考虑前一个")"的情况。假设这样的子字符串是一个更长的有效子字符串的一部分,那么前面就有与之对应的"(",那么再前面的呢,又怎么去找?

    假设成立的情况下,这时的子字符串就类似于这样"...((...))",而中间一定会出现"...()..."这样的子串,这时就回到了第一种情况,于是就可以通过"...()..."中的")"来推出下一个元素")"dp[i]的值。仔细想想,由于是第一种情况推出来的,那么"......))"这里前一个元素")"dp[i]的值就意味着以它结尾的子串"...(...)"的有效子字符串的长度。

    想到这里,就可以去找出前面的有效子串的长度了:

    dp[i] = dp[i - dp[i-1] - 2] + dp[i-1] + 2

    这里举个例子方便去理解:

    i 0 1 2 3 4 5 6
    s[i] ( ( ) ( ( ) )
    dp[i] 0 0 2 0 0 2 6

    如果你想问假设出现很多个"......))))))"这样的情况的话,其实这都是第二种情况的递推,或者说第二种情况是该情况的子问题

    算法学习之动态规划_第3张图片

到这里本题差不多就解完了,但要注意越界的问题,这里贴出部分代码以帮助大家理解:

class Solution {
    public int longestValidParentheses(String s) {
        int len = s.length(), max = 0;
        int[] dp = new int[len];
        for (int i = 1; i < len; i++) {
            if (s.charAt(i) == ')'){
                if (s.charAt(i-1) == '('){
                    dp[i] = (i >= 2 ? dp[i-2] : 0) + 2;
                } else if (i - dp[i-1] > 0 && s.charAt(i - dp[i-1] - 1) == '('){
                    dp[i] = ((i - dp[i-1]) >= 2 ? dp[i - dp[i-1] - 2] : 0) + dp[i-1] + 2;
                }
                max = Math.max(max, dp[i]);
            }
        }
        return max;
    }
}

LeetCode72. 编辑距离

算法学习之动态规划_第4张图片

初看完这道题,我自己的想法是直接嵌套循环去找不同,但那些显然不合适(也不说写得出来不,时间估计都超了)

算法学习之动态规划_第5张图片

这道题其实在于怎么去寻找子问题,从而将原问题进行拆解:这一状态可以看成是由上一状态经过插入、删除、替换操作转换过来的

另外可以发现使用一维的dp[]不好去定义它,所以这里可以用二维的dp[][],那么这里dp[i][j]就代表word1中从头到i位置的部分 转换成 word2中从头到j位置的部分 所需要的最少操作数

将原问题进行拆解,拆解成子问题,子问题再进行拆解…那么初始情况又怎么去找呢,也就是边界怎么去确定呢:考虑到word1或者word2可能为空字符的情况,这里放一张表方便大家去理解(这里借由实例1,给出部分数据,以下讨论也是建立在实例1的基础上)

'' r o s
'' 0 1 2 3
h 1 ◆1 ▼2
o 2 ●2
r 3 2
s 4 3
e 5 4

''即为空字符,dp[i][j]的值即为最少操作数(比如说表中那个5即表示horse至少经过5次删除操作后可以得到空字符'')

这里还需要确定的是插入、删除、替换 操作怎么通过代码去实现(还请理解不到的朋友们仔细看表格)

回到给的表格中,可以看到第一个位置为0,意思是两者都为空字符,不需要进行操作,因此操作数为0,如果这时word1word2的第一个字符相等的话,那么也不需要进行操作,所以这里需要分情况讨论:

  • word1[i] == word2[j]的时候,表示最新的一步不需要进行操作,也就是说此时word1[1 ~ i-1]转换成word2[1 ~ j-1]的最少操作数 与 word1[1 ~ i]转换成word2[1 ~ j]的最少操作数是一样的

    所以可以得出:dp[i][j] = dp[i-1][j-1]

  • word1[i] != word2[j]的时候,表示最新的一步需要进行操作:

    • 替换操作:比如说word1第一个字符 与 word2第一个字符 可以直接替换所得(对应表格中打◆的1)

      这里可以得出:dp[i][j] = dp[i-1][j-1] + 1

    • 删除操作:前面有提到,比如说word1前两个字符ho先是经过一次替换得到ro,要再得到r,则需要一次删除操作(对应表格中打●的2)

      这里可以得到:dp[i][j] = dp[i-1][j] + 1

    • 插入操作:比如说word1前一个字符h要得到ro,先是h要进行替换得到r,再进行插入操作(对应表格中打▼的2)

      这里可以得到:dp[i][j] = dp[i][j-1] + 1

    因为要得到最少操作数,所以这里表示为

    dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]) + 1;

到这里本题也差不多解完了,这里再贴出部分代码帮助大家理解:

class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length(), len2 = word2.length();
        int[][] dp = new int[len1+1][len2+1];
        for (int i = 0; i <= len1; i++) {
            dp[i][0] = i;
        }
        for (int j = 0; j <= len2; j++) {
            dp[0][j] = j;
        }

        char[] words1 = word1.toCharArray();
        char[] words2 = word2.toCharArray();
        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                if (words1[i-1] == words2[j-1]){
                    dp[i][j] = dp[i-1][j-1];
                } else {
                    dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]), 
                                        dp[i-1][j-1]) + 1;
                }
            }
        }
        return dp[len1][len2];
    }
}

参考

  • 所给例题的题解
  • 程序员小灰—— 漫画:什么是动态规划?

后话

这篇文章自己是二十多天前开始写的,加上自己还不太会写博客,以及老师布置的作业实在是太多了… 最近几天也还是把它给码完了 QAQ

创作不易,如果大家喜欢的话可以点个赞

你可能感兴趣的:(算法学习)