动态规划总结

简述

动态规划是一种将一个复杂问题分解为多个简单的子问题求解的方法。将子问题的答案存储在记忆数据结构中,当子问题再次需要解决时,只需查表查看结果,而不需要再次重复计算,因此节约了计算时间。
国外知乎 Quora 上一个帖子问应该怎样给四岁的孩子解释什么是动态规划,其中一个非常经典的回答如下:
How should I explain dynamic programming to a 4-year-old?


动态规划通常基于一个递推公式和一个或多个初始状态。当前问题的解可以分解为多个子问题解得出。使用动态规划只需要多项式时间复杂度,因为比回溯法和暴力法快很多。

动态规划中非常重要的两个概念:状态和状态转移方程。

下面通过例子由浅至深详细讲解。
所有例题均来自leetcode,所示代码均通过所有测试。

例题目录

不断更新中...

例题

1、最长回文子串
题目描述:

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

示例 1:

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

算法

定义 dp[i][j] 为 s[i:j+1] 是回文字符串时的长度,如果 s[i:j+1] 不是回文,则 dp[i][j] = 0.
转移方程:

  • s[i] == s[j]
    dp[i][j] = dp[i+1][j-1] + 2

初始状态:
dp[i][i] = 1
dp[i][i-1] = 2 (s[i] == s[i-1])

代码
class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        dp = [[-1] * n for _ in range(n)]

        ans = (0, 0)
        for i in range(n):
            dp[i][i] = 1
            if i > 0 and s[i] == s[i-1]:
                dp[i-1][i] = 2
                ans = (i-1, i)

        for i in range(2, n):
            for j in range(0, i-1):
                if s[j] == s[i] and dp[j+1][i-1] != -1:
                    dp[j][i] = dp[j+1][i-1] + 2
                    if dp[j][i] > ans[1] - ans[0] + 1:
                        ans = (j, i)
        return s[ans[0]:ans[1]+1]

更好理解版本,依次查询长度从小到大

public String longestPalindrome(String s) {
        int n = s.length();
        if (n == 0) return "";
        boolean[][] dp = new boolean[n][n];
        int maxLen = 1, startId = 0;
        for (int len = 0; len < n; len++) {
            for (int i = 0; i < n - len; i++) {
                int j = i + len;
                if (len == 0) {
                    dp[i][j] = true;
                } else if (len == 1) {
                    dp[i][j] = (s.charAt(i) == s.charAt(j));
                } else {
                    dp[i][j] = (dp[i+1][j-1] && s.charAt(i) == s.charAt(j));
                }
                if (dp[i][j] && len + 1 > maxLen) {
                    maxLen = len + 1;
                    startId = i;
                }
            }
        }
        return s.substring(startId, startId + maxLen);
    }

时间复杂度:O(n^2)

类似题目:

leetcode 516. 最长回文子序列

转移方程:

  • s[i] != s[j]
    dp[i][j] = max(dp[i][j-1], dp[i+1][j])

  • s[i] == s[j]
    dp[i][j] = dp[i+1][j-1] + 2

2、最长有效括号
题目描述:

给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。

示例 1:

输入: "(()"
输出: 2
解释: 最长有效括号子串为 "()"

算法

定义 dp[i] 为以 i 结束的最长有效括号长度。
状态转移方程:

  1. s[i] == '('
    dp[i] = 0
  2. s[i] == ')'
  • s[i-1] == '(': dp[i] = dp[i-2] + 2
    例如:()()
  • s[i-1] == ')' and s[i - 1 - dp[i-1]] == '(': dp[i] = dp[i-1] + dp[i - 1 - dp[i-1] - 1] + 2
    例如:()(())

注意数组越界情况。

代码
class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n = len(s)
        dp = [0] * n
        res = 0
        for i in range(1, n):
            if s[i] == ')':
                if s[i-1] == '(':
                    if i >= 2:
                        dp[i] = dp[i-2] + 2
                    else:
                        dp[i] = 2
                elif i - 1 - dp[i-1] >= 0 and s[i - 1 - dp[i-1]] == '(':
                    if i - 2 - dp[i-1] >= 0:
                        dp[i] = dp[i-1] + dp[i - 2 - dp[i-1]] + 2
                    else:
                        dp[i] = dp[i-1] + 2
                res = max(res, dp[i])
        return res

时间复杂度:O(n)

这道题也可以用栈在线性时间范围内解决。

3、最长重复子数组
题目描述:

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。

示例 1:

输入:

A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出: 3
解释:
长度最长的公共子数组是 [3, 2, 1]。

说明:
1 <= len(A), len(B) <= 1000
0 <= A[i], B[i] < 100

题目描述:

dp[i][j] 表示以 A[i] 和 B[j] 结束的公共数组长度。

状态转移:

  • A[i] == B[j]
    dp[i][j] = dp[i-1][j-1] + 1
代码
class Solution:
    def findLength(self, A: List[int], B: List[int]) -> int:
        """
        A[i] == B[j]  dp[i][j] = dp[i-1][j-1] + 1
        """
        m, n = len(A), len(B)
        dp = [[0] * (n+1) for _ in range(m+1)]

        res = 0
        for i in range(1, m+1):
            for j in range(1, n+1):
                if A[i-1] == B[j-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                    res = max(res, dp[i][j])
        return res

时间复杂度:O(mn)

类似题目

1143. 最长公共子序列

子数组变成来子序列,也就是公共的部分可以不是连续的。

dp[i][j] text1[:i] 和 text2[:j] 的最长公共子序列

状态转移:

  • text1[i] == text2[j]
    dp[i][j] = dp[i-1][j-1] + 1
  • text1[i] != text2[j]
    dp[i][j] = max(dp[i-1][j], dp[i][j-1])

代码:

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        """
        dp[i][j]
        if text1[i] == text2[j]: dp[i][j] = dp[i-1][j-1] + 1
        else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
        """
        m, n = len(text1), len(text2)
        dp = [[0] * (n+1) for _ in range(m+1)]

        for i in range(1, m+1):
            for j in range(1, n+1):
                if text1[i-1] == text2[j-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])
        return dp[-1][-1]
7、恢复数组
题目描述:

某个程序本来应该输出一个整数数组。但是这个程序忘记输出空格了以致输出了一个数字字符串,我们所知道的信息只有:数组中所有整数都在 [1, k] 之间,且数组中的数字都没有前导 0 。

给你字符串 s 和整数 k 。可能会有多种不同的数组恢复结果。

按照上述程序,请你返回所有可能输出字符串 s 的数组方案数。

由于数组方案数可能会很大,请你返回它对 10^9 + 7 取余 后的结果。

示例 1:

输入:s = "1000", k = 10000
输出:1
解释:唯一一种可能的数组方案是 [1000]

示例 2:

输入:s = "1317", k = 2000
输出:8
解释:可行的数组方案为 [1317],[131,7],[13,17],[1,317],[13,1,7],[1,31,7],[1,3,17],[1,3,1,7]

算法

参考

1. 不考虑0,且k足够大时。
以 s = "1317", k = 2000 为例
字符串从下标 1 开始。
i = 0 时,s = "",dp[0] = 1;
i = 1 时,s = "1",[1],dp[1] = 1;
i = 2 时,s = "13",[1, 3] 和 [13],dp[2] = dp[1] + dp[0]; 可以理解为 "3" 接在 dp[1] 后面或者 "13" 接在 dp[0] 后面
i = 3 时,s = "131",dp[3] = dp[2] + dp[1] + dp[0] = 4;
i = 4 时,s = "1317",dp[4] = dp[3] + dp[2] + dp[1] + dp[0] = 8;

2. 考虑0,且k有限时。
对每次状态转移进行判断是否以 0 开头,是否超出 k 的范围。

代码
class Solution:
    def numberOfArrays(self, s: str, k: int) -> int:
        mod = 10 ** 9 + 7
        n = len(s)
        dp = [0] * (n + 1)
        dp[0] = 1

        for i in range(1, n+1):
            for j in range(i-1, -1, -1):
                if s[j] == '0':
                    continue
                if int(s[j:i]) <= k:
                    dp[i] += dp[j]
                else:
                    # 当前字符为0,大于k,且dp[0]为零,可以直接返回0,因为没法构成合适的数字了
                    if s[i - 1] == "0" and dp[i] == 0: 
                        return 0
                    break
            dp[i] %= mod

        return dp[-1]

时间复杂度:两层循环,由于有字符串到数字的强制转换,所以时间会稍微多一些。O(n^2)
空间复杂度:O(n)

8、生成数组
题目描述:

给你三个整数 n、m 和 k 。下图描述的算法用于找出正整数数组中最大的元素。

请你生成一个具有下述属性的数组 arr :

arr 中有 n 个整数。
1 <= arr[i] <= m 其中 (0 <= i < n) 。
将上面提到的算法应用于 arr ,search_cost 的值等于 k 。
返回上述条件下生成数组 arr 的 方法数 ,由于答案可能会很大,所以 必须 对 10^9 + 7 取余。

示例 1:

输入:n = 2, m = 3, k = 1
输出:6

解释:可能的数组分别为 [1, 1], [2, 1], [2, 2], [3, 1], [3, 2] [3, 3]

提示:

  • 1 <= n <= 50
  • 1 <= m <= 100
  • 0 <= k <= n
算法

根据提示的数据范围,如果DFS搜索答案的话,复杂度是 100^50,显然肯定不是合适的方法。自然想到动态规划。

定义 dp[n][i][k] 表示长度为 n,最大值为 i,search_cost 为 k 的满足条件的方法数,\sum_{i=1}^{m}dp[n][i][k]即为所求答案。

状态转移分为两种情况:

  1. 当最大值 i 位于最后一个元素时,那么方法数为:\sum_{j=1}^{i-1}dp[n-1][j][k-1]
  2. 当最大值出现在前 n-1 个元素之中时:dp[n-1][i][k] * i,乘上 i 表示最后的位置可以在 [1, i] 中任意选择。

初始状态:
dp[0][i][k] = dp[n][0][k] = dp[n][i][k] = 0
dp[1][i][k] = 1

接下来可以写代码了。

代码
class Solution:
    def numOfArrays(self, n: int, m: int, k: int) -> int:
        mod = 10 ** 9 + 7
        memo = {}

        def dp(n, i, k):
            if n == 0 or i == 0 or k == 0:
                return 0
            elif n == k == 1:
                return 1
            if (n, i, k) in memo:
                return memo[(n, i, k)]
            res = 0
            for j in range(1, i):
                res += dp(n-1, j, k-1)
                res %= mod
            res += i * dp(n-1, i, k)
            res %= mod
            memo[(n, i, k)] = res
            return res

        res = 0
        for i in range(1, m+1):
            res += dp(n, i, k)
            res %= mod
        return res

时间复杂度:需要填满三维数组 dp,O(mnk)
空间复杂度:O(mnk)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,406评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,976评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,302评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,366评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,372评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,457评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,872评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,521评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,717评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,523评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,590评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,299评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,859评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,883评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,127评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,760评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,290评论 2 342

推荐阅读更多精彩内容

  •   为了准备三月份蓝桥杯的比赛和提高下自己数据结构和算法方面的水平,所以开始在leetcode上刷起了题。刚刚开始...
    冯宇祺阅读 3,569评论 0 5
  • 动态规划的三大步骤 动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存...
    郑小才阅读 185评论 0 0
  • 1.确定数组维数,如果是一维数组行形式,千万不要复杂化问题; 2.注意思考问题无思路时,有限考虑倒数第二部并设为x...
    都市打工人之小熊保安阅读 491评论 0 0
  • 动态规划 通过子问题递推求解最优的方法, 动态规划常常适用于有重叠子问题和最优子结构性质的问题 。 解题思路 动态...
    GeorgeDon阅读 128评论 0 0
  • 总结:https://labuladong.gitbook.io/algo/ 最长回文子串 https://lee...
    我是小曼巴阅读 560评论 0 1