动态规划算法模板和demo

 

366. 斐波纳契数列

中文
English

查找斐波纳契数列中第 N 个数。

所谓的斐波纳契数列是指:

  • 前2个数是 0 和 1 。
  • i 个数是第 i-1 个数和第i-2 个数的和。

斐波纳契数列的前10个数字是:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34 ...

样例

样例  1:
	输入:  1
	输出: 0
	
	样例解释: 
	返回斐波那契的第一个数字,是0.

样例 2:
	输入:  2
	输出: 1
	
	样例解释: 
	返回斐波那契的第二个数字是1.

class Solution:
    """
    @param n: an integer
    @return: an ineger f(n)
    """
    def fibonacci(self, n):
        # write your code here
        if n == 1:
            return 0
        if n == 2:
            return 1
            
        dp = [0]*n
        dp[1] = 1
        for i in range(2, n):
            dp[i] = dp[i-1]+dp[i-2]
        return dp[n-1]

 最原始的DP!!!

 

在测试数据中第 N 个斐波那契数不会超过32位带符号整数的表示范围

纯用递归会超时,如果用带有记忆化的递归就可以,使用循环和记忆化递归的时间复杂度一样,都是O(n)

 

优化:

class Solution:
    def fibonacci(self, n):
        a = 0
        b = 1
        for i in range(n - 1):
            a, b = b, a + b
        return a

 

 

近期(20230124)补充含子状态的dp! 309. 最佳买卖股票时机含冷冻期  详见:https://www.cnblogs.com/bonelee/p/17077984.html

 

322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

 

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

示例 3:

输入:coins = [1], amount = 0
输出:0

 

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 231 - 1
  • 0 <= amount <= 104
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        if amount == 0:
            return 0

        inf = float('inf')
        dp = [inf]*(amount+1)
        coins.sort()
        for i in range(1, amount+1):
            for j in range(len(coins)):
                if coins[j] > i:
                    break
                
                if coins[j] == i:
                    dp[i] = 1
                    break

                dp[i] = min(dp[i], dp[coins[j]]+dp[i-coins[j]])                    
                
        if dp[amount] == inf:
            return -1
        
        return dp[amount]

 关键是注意2个for的正确设置!!!我原本两个for amount的,结果超时了!所以上面包含了剪纸的技巧。



337. 打家劫舍 III

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

 

示例 1:

输入: root = [3,2,3,null,3,null,1]
输出: 7 
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

示例 2:

输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9

这个是属于树类dp,有两种解法,见:https://leetcode.cn/problems/house-robber-iii/solution/san-chong-fang-fa-jie-jue-shu-xing-dong-tai-gui-hu/

本质上是dfs,无非是返回节点标记了2个state!
class Solution:
    def rob(self, root: Optional[TreeNode]) -> int:        
        def dfs(root):
            if not root:
                return 0, 0
            l_steal, l_unsteal = dfs(root.left)
            r_steal, r_unsteal = dfs(root.right)
            unsteal = max(l_steal, l_unsteal) + max(r_steal, r_unsteal)
            steal = l_unsteal + r_unsteal + root.val
            return steal, unsteal
        
        return max(dfs(root))

 

110. 最小路径和

中文
English

给定一个只含非负整数的m*n网格,找到一条从左上角到右下角的可以使数字和最小的路径。

 

样例

样例 1:
	输入:  [[1,3,1],[1,5,1],[4,2,1]]
	输出: 7
	
	样例解释:
	路线为: 1 -> 3 -> 1 -> 1 -> 1。


样例 2:
	输入:  [[1,3,2]]
	输出:  6
	
	解释:  
	路线是: 1 -> 3 -> 2

注意事项

你在同一时间只能向下或者向右移动一步

Dp[i][j] 存储从(0, 0) 到(i, j)的最短路径。
Dp[i][j] = min(Dp[i-1][j]), Dp[i][j-1]) + grid[i][j];

 

class Solution:
    """
    @param grid: a list of lists of integers
    @return: An integer, minimizes the sum of all numbers along its path
    """
    def minPathSum(self, grid):
        # write your code here
        row, col = len(grid), len(grid[0])
        dp = [[0]*col for i in range(row)]
        
        dp[0][0] = grid[0][0]
        for i in range(1, row):
            dp[i][0] = grid[i][0]+dp[i-1][0]
        
        for j in range(1, col):
            dp[0][j] = grid[0][j]+dp[0][j-1]
        
        for i in range(1, row):
            for j in range(1, col):
                dp[i][j] = min(dp[i-1][j], dp[i][j-1])+grid[i][j]
        
        return dp[row-1][col-1]

 记得画图,二维矩阵。

dp的过程,初始化,状态转移方程

空间优化:

滚动数组代码,java,考试不用这么肝,面试有明确思路即可:

public class Solution {
    /**
     * @param grid: a list of lists of integers.
     * @return: An integer, minimizes the sum of all numbers along its path
     */
    public int minPathSum(int[][] A) {
        if (A == null || A.length == 0 || A[0].length == 0) {
            return 0;
        }
        
        int m = A.length, n = A[0].length;
        int[][] f = new int[2][n];
        int i, j;
        int old, now = 0; // f[i] is stored in rolling array f[0]
        for (i = 0; i < m; ++i) {
            old = now;
            now = 1 - now; // 0 --> 1, 1 --> 0
            
            for (j = 0; j < n; ++j) {
                if (i == 0 && j == 0) {
                    f[now][j] = A[0][0];
                    continue;
                }
                
                f[now][j] = Integer.MAX_VALUE;
                if (i > 0) {
                    f[now][j] = Math.min(f[now][j], f[old][j]);
                }
                
                if (j > 0) {
                    f[now][j] = Math.min(f[now][j], f[now][j - 1]);
                }
                
                f[now][j] += A[i][j];
            }
        }
        
        return f[now][n - 1];
    }
}

 其实滚动数组的代码还是很简单,画一个图就知道了:

class Solution:
    """
    @param grid: a list of lists of integers
    @return: An integer, minimizes the sum of all numbers along its path
    """
    def minPathSum(self, grid):
        # write your code here
        m, n = len(grid), len(grid[0])
        dp = [0] * n
        
        dp[0] = grid[0][0]
        for j in range(1, n):
            dp[j] = dp[j-1] + grid[0][j]
        
        for i in range(1, m):
            dp[0] = dp[0] + grid[i][0]
            for j in range(1, n):
                dp[j] = min(dp[j-1], dp[j]) + grid[i][j]
        
        return dp[n-1]

 

436. 最大正方形

中文
English

在一个二维01矩阵中找到全为1的最大正方形, 返回它的面积.

样例

样例 1:

输入:
[
  [1, 0, 1, 0, 0],
  [1, 0, 1, 1, 1],
  [1, 1, 1, 1, 1],
  [1, 0, 0, 1, 0]
]
输出: 4

样例 2:

输入: 
[
  [0, 0, 0],
  [1, 1, 1]
]
输出: 1

设定状态: dp[i][j] 表示以(i, j)为右下顶点的最大全1矩阵的边长.

状态转移方程:

if matrix[i][j] == 0
	dp[i][j] = 0
else                 // 此时为dp[i-1][j-1], dp[i-1][j], dp[i][j-1] 确定的区域的最大全1矩阵
	dp[i][j] = min{dp[i-1][j-1], dp[i-1][j], dp[i][j-1]} + 1	// 得到此方程需要一定推导, 纸笔画一下

边界: if i == 0 or j == 0: dp[i][j] = matrix[i][j]

答案: max{dp[i][j]}^2

class Solution:
    """
    @param grid: a matrix of 0 and 1
    @return: an integer
    """
    def maxSquare(self, grid):
        # write your code here
        row, col = len(grid), len(grid[0])
        dp = [[0]*col for i in range(row)]
        
        for i in range(0, row):
            dp[i][0] = grid[i][0]
        
        for j in range(0, col):
            dp[0][j] = grid[0][j]
        
        for i in range(1, row):
            for j in range(1, col):
                if grid[i][j]:
                    dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1])+1
                else:
                    dp[i][j] = 0
        
        max_edge = max(max(dp[i]) for i in range(row))
        return max_edge*max_edge

 也可以利用滚动数组优化。

也不复杂!!!

class Solution:
    """
    @param matrix: a matrix of 0 and 1
    @return: an integer
    """
    def maxSquare(self, matrix):
        # write your code here
        m, n = len(matrix), len(matrix[0])
        dp = [[0] * n for i in range(2)]
        
        ans = 0
        for i in range(m):
            for j in range(n):
                if  i == 0 or j == 0:
                    dp[i&1][j] = matrix[i][j]
                else:
                    if matrix[i][j]:
                        dp[i&1][j] = min(dp[(i-1)&1][j-1], dp[i&1][j-1], \
                                     dp[(i-1)&1][j]) + 1
                    else:
                        dp[i&1][j] = 0
                
                ans = max(dp[i&1][j]**2, ans)
                
        return ans

 

 补充下最大矩形,比最大正方形要复杂:

动态规划8最大矩形(maximal-rectangle)

1. 问题描述

??给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。


??示例:
??输入:
??[
?? [“1”,“0”,“1”,“0”,“0”],
?? [“1”,“0”,“1”,“1”,“1”],
?? [“1”,“1”,“1”,“1”,“1”],
?? [“1”,“0”,“0”,“1”,“0”]
??]
??输出: 6


2. 解法

2.1 暴力法

2.2 动态规划1【容易理解 掌握】

 

 

 

??我们定义一个dp数组,用于存储矩阵中以给定坐标为右下角的矩形的最大长度(每一行的更新可以视为row[i] = row[i - 1] + 1 if row[i] == ‘1’)。确定了每个点对应的最大长度,就可以在线性时间内计算出以该点为右下角的最大矩形。当我们遍历列时,可知从初始点到当前点矩形的最大高度,C++代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//解法2:动态规划1:时间复杂度O(N^2*M)[计算每个点的最大面积需O(N),全部N*M个点],空间复杂度O(NM)
    int maximalRectangle(vector<vector<char>>& matrix) {
        if (matrix.size() == 0) return 0;
        int maxarea = 0;
        vector<vector<int>> dp(matrix.size(), vector<int>(matrix[0].size(), 0));

        for (int i = 0; i < matrix.size(); i++) {
            for (int j = 0; j < matrix[0].size(); j++) {
                if (matrix[i][j] == '1') {
                    //逐行更新该点对应的最大长度(横向)
                    dp[i][j] = j == 0 ? 1 : dp[i][j - 1] + 1;
                    int length = dp[i][j];
                    //自当前点竖直向上遍历得到不同的高度,在该高度下我们应该选这一列中较小的宽度
                    //毕竟短板才是能构成最大的。
                    for (int k = i; k >= 0; k--) {
                        length = min(length, dp[k][j]);
                        maxarea = max(maxarea, length * (i - k + 1));
                    }
                }
            }
        }
        return maxarea;
    }

2.3 动态规划2【技巧性很强】

??考虑这样一个算法,对于每个点,我们先不断向上直到遇到零,然后向两边扩展,直到某列出现0。这种方式构建的矩形中必然存在最大矩形。
??我们通过定义三个数组height,left,right来记录每个点的高度,左边界和右边界,问题转化为如何更新每个数组。
??对于height数组,new_height[j]=old_height[j]+1 if row[j]==‘1’。
??对于left数组,new_left[j]=max(old_left[j],cur_left),其中cur_left是遇到的最右边的0的序号加1,向左扩展矩形时不能超过改点,否则会遇到0。
??对于right数组,new_right[j]=min(old_right[j],cur_right),cur_right表示我们遇到的最左边的0的序号。这里不减去1是因为这样就可以用height[j]*(right[j]-left[j])来计算矩形面积,也就是说矩形的底边由半开半闭区间[l,r)决定。为了记录正确的cur_right需要从右向左迭代,因此更新right时需要从右向左。C++代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//解法3:动态规划2:时间复杂度O(NM)[],空间复杂度O(N)
    int maximalRectangle(vector<vector<char>>& matrix) {
        if (matrix.size() == 0) return 0;
        int m = matrix.size();
        int n = matrix[0].size();
        //分别存储每一行上元素对应的最大矩形的高度以及左右区间
        vector<int> left(n, 0);//最大的左边界为左端
        vector<int> right(n, n);//最大的有边界为右端
        vector<int> height(n, 0);

        int maxarea = 0;
        for (int i = 0; i < m; i++) {
            int cur_left = 0, cur_right = n;
            //更新高度,最后存储在height数组的就是每一列‘1’的个数
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '1')
                    height[j]++;
                else
                    height[j] = 0;
            }
            //更新左边界,
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '1')
                    left[j] = max(left[j], cur_left);
                else {
                    left[j] = 0;
                    cur_left = j + 1;
                }
            }
            //更新右边界
            for (int j = n - 1; j >= 0; j--) {
                if (matrix[i][j] == '1')
                    right[j] = min(right[j], cur_right);
                else
                {
                    right[j] = n;
                    cur_right = j;
                }
            }
            for (int j = 0; j < n; j++) {
                maxarea = max(maxarea, (right[j] - left[j]) * height[j]);
            }
        }
        return maxarea;
    }

114. 不同的路径

中文
English

有一个机器人的位于一个 m × n 个网格左上角。

机器人每一时刻只能向下或者向右移动一步。机器人试图达到网格的右下角。

问有多少条不同的路径?

样例

Example 1:

Input: n = 1, m = 3
Output: 1	
Explanation: Only one path to target position.

Example 2:

Input:  n = 3, m = 3
Output: 6	
Explanation:
	D : Down
	R : Right
	1) DDRR
	2) DRDR
	3) DRRD
	4) RRDD
	5) RDRD
	6) RDDR

注意事项

n和m均不超过100
且答案保证在32位整数可表示范围内。

解法1:
数学模型:在n-1 + m-1长度的序列中,有n-1个D,和m-1个组成。
其中D表示向下,R表示向右。
因此满足组合数:C(n+m-2, n-1), 从n+m-2个位置中选n-1个位置放D的方案数。

解法2:
可以用Dp过程来求解:
Dp[i][j] 表示走到(i,j)的路径数,
考虑最后一步是从上往下走,还是从左往右走。
Dp[i][j] = Dp[i-1][j] + Dp[i][j-1];

class Solution:
    # @return an integer
    """
    def c(self, m, n):
        mp = {}
        for i in range(m):
            for j in range(n):
                if(i == 0 or j == 0):
                    mp[(i, j)] = 1
                else:
                    mp[(i, j)] = mp[(i - 1, j)] + mp[(i, j - 1)]
        return mp[(m - 1, n - 1)]

    def uniquePaths(self, m, n):
        return self.c(m, n)
    """
    
    def uniquePaths(self, m, n):
        dp = [[0]*n for i in range(m)]
        
        for i in range(m):
            dp[i][0] = 1    
            
        for j in range(n):
            dp[0][j] = 1
                
        for i in range(1, m):
            for j in range(1, n):
                dp[i][j] = dp[i-1][j] + dp[i][j-1]
        
        return dp[m-1][n-1]

 

 

200. 最长回文子串

中文
English

给出一个字符串(假设长度最长为1000),求出它的最长回文子串,你可以假定只有一个满足条件的最长回文串。

样例

样例 1:

输入:"abcdzdcab"
输出:"cdzdc"

样例 2:

输入:"aba"
输出:"aba"

挑战

O(n2) 时间复杂度的算法是可以接受的,如果你能用 O(n) 的算法那自然更好。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        dp = [[False]*n for i in range(n)]

        ans = ""
        for i in range(n-1, -1, -1):
            for j in range(i, n):
                if j == i:
                    dp[i][j] = True
                elif j == i + 1:
                    dp[i][j] = s[i] == s[j]
                else:
                    if s[j] == s[i]:
                        dp[i][j] = dp[i+1][j-1] 
        
                if dp[i][j] and len(ans) < (j-i+1):
                    ans = s[i:j+1]
            
        return ans

  

我的最新解法:2个for递增!!!记住下面的写法,几个if逻辑非常清晰!!!

class Solution99:
    """
    @param s: input string
    @return: the longest palindromic substring
    """
    def longestPalindrome(self, s):        
        def calc_dp(s):     
            n = len(s)
            dp = [[False]*n for i in range(n)]
            for j in range(n):
                for i in range(0, j+1):
                    if i == j:
                        dp[i][j] = True
                    elif i == j - 1:
                        dp[i][j] = s[i] == s[j]
                    else:                        
                        if i < n - 1 and j > 0 and s[i] == s[j]:
                            dp[i][j] = dp[i+1][j-1]                                 
                    # print(i, j, dp[i][j])
            return dp
        
        n = len(s)
        dp = calc_dp(s)
        ans = ""
        for i in range(n):
            for j in range(i+1, n):
                if dp[i][j] and len(ans) < (j-i+1):
                    ans = s[i:j+1]        
        return ans
        
print(Solution99().longestPalindrome("abcdzdcab")) # 

 

我的老式解法,不建议再使用了,容易出错:

class Solution:
    """
    @param s: input string
    @return: the longest palindromic substring
    """
    def longestPalindrome(self, s):
        # write your code here
        start,end = 0,0
        n = len(s)

        def locatePalindrome(s, i, j):
            nonlocal start,end
            while i>=0 and j<n:
                if s[i] == s[j]:
                    if end-start < j-i+1:
                        start,end = i,j+1
                else:
                    break
                i -= 1
                j += 1
                
        for i in range(n):
            # center i
            locatePalindrome(s, i, i)
            # center i,i+1
            locatePalindrome(s, i, i+1)
        return s[start:end]            

            

 最常规的解法。

使用dp的:

我们首先初始化一字母和二字母的回文,然后找到所有三字母回文,并依此类推…

R%`HVT[3D88A86WRHC5CM5Q

 注意为什么两个for一个递减一个递增???==》为了帮助理解,我来示意下:

 

举例说明:

字符串为:a b c d a X a d c 3 2 1
假设i,j位置如下:

<-----i
              j=i+1 ---->
a b c d a X a d c 3 2 1

为了知道d a X a d是不是回文?怎么办呢?
因为左右两边都是d,所以看a X a是不是回文对不对!!!那再往前追呢?
因为左右两边都是a,所以看X是不是回文。

而DP的本质就是用迭代的思路,上面反过来迭代就是DP了!!!

所以才得出来,i,j的方向是两边扩展!!!

因为一开始的时候,只知道单个字符是回文!如果是两个字符,则当两个字符相等的时候才是回文!
好了,上面2个是初始条件。

好了,开始迭代:

a b c d a X a d c 3 2 1
----------------------i在这里最后一个位置 ,j越界,dp[i][i] = True

a b c d a X a d c 3 2 1
--------------------i在这里,j在1的位置,dp[i][j] = False,因为2!=1 


a b c d a X a d c 3 2 1
------------------i在这里,j在2的位置,dp[i][j] = False,因为3!=2

a b c d a X a d c 3 2 1
------------------i在这里,j在1的位置,dp[i][j] = False,因为3!=1

 ==>哈哈哈,意识到本质了,两个for递增也可行的了。

class Solution:
    """
    @param s: input string
    @return: the longest palindromic substring
    """
    def longestPalindrome(self, s):
        # write your code here
        # dp[i][j] = dp[i+1][j-1] + (s[i]==s[j])  if i<j         
        n = len(s)
        dp = [[False]*n for i in range(n)]
        
        ans = s[0]
        for i in range(n):
            dp[i][i] = True
            if i < n-1 and s[i] == s[i+1]:
                dp[i][i+1] = True
                
        for i in range(n, -1, -1):
            for j in range(i+1, n):
                if i < n-1 and j > 0: 
                    if s[i] == s[j] and j != i+1:
                        dp[i][j] = dp[i+1][j-1]
        
        for i in range(n):
            for j in range(i+1, n):
                if dp[i][j] and len(ans) < (j-i+1):
                    ans = s[i:j+1]
        return ans
                                 

 

换一种两个for递增的写法:

 

 

398. 最长上升连续子序列 II

中文
English

给定一个整数矩阵. 找出矩阵中的最长连续上升子序列, 返回它的长度.

最长连续上升子序列可以从任意位置开始, 向上/下/左/右移动.

样例

样例 1:

输入: 
    [
      [1, 2, 3, 4, 5],
      [16,17,24,23,6],
      [15,18,25,22,7],
      [14,19,20,21,8],
      [13,12,11,10,9]
    ]
输出: 25
解释: 1 -> 2 -> 3 -> 4 -> 5 -> ... -> 25 (由外向内螺旋)

样例 2:

输入: 
    [
      [1, 2],
      [5, 3]
    ]
输出: 5
解释: 1 -> 2 -> 3 -> 5

挑战

假定这是一个 N x M 的矩阵. 在 O(NM) 的时间复杂度和空间复杂度内解决这个问题.

动态规划的实现方式:
环(从小到大递推)
记忆化搜索(从大到小搜索)

 

这个题目要用dfs+记忆化搜索比较方便。

分析:

 

多重循DP遇到的困难:
到从上到下循环不能解决问题 初始状态不好定义

那我们有没有可以比较暴力解决的方法呢? DFS



 

动态规划, 设定状态 f[i][j] 表示矩阵中坐标 (i, j) 的点开始的最长上升子序列

状态转移方程:

int dx[4] = {0, 1, -1, 0};
int dy[4] = {1, 0, 0, -1};

f[i][j] = max{ f[i + dx[k]][j + dy[k]] + 1 }

k = 0, 1, 2, 3, matrix[i + dx[k]][j + dy[k]] > matrix[i][j]

这道题目可以向四个方向走, 所以推荐使用记忆化搜索(递归)的写法. 本质上就是dfs+cache!!!

public class Solution {
    /**
     * @param matrix: A 2D-array of integers
     * @return: an integer
     */

    int[][] dp;
    int n, m;

    public int longestContinuousIncreasingSubsequence2(int[][] A) {
        if (A.length == 0) {
            return 0;
        }

        n = A.length;
        m = A[0].length;
        int ans = 0;
        dp = new int[n][m]; // dp[i][j] means the longest continuous increasing path from (i,j)
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < m; ++j) {
                dp[i][j] = -1; // dp[i][j] has not been calculated yet
            }
        }

        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < m; ++j) {
                search(i, j, A);
                ans = Math.max(ans, dp[i][j]);
            }
        }

        return ans;
    }

    int[] dx = { 1, -1, 0, 0 };
    int[] dy = { 0, 0, 1, -1 };

    void search(int x, int y, int[][] A) {
        if (dp[x][y] != -1) { // if dp[i][j] has been calculated, return directly
            return;
        }

        int nx, ny;
        dp[x][y] = 1;
        for (int i = 0; i < 4; ++i) {
            nx = x + dx[i];
            ny = y + dy[i];
            if (nx >= 0 && nx < n && ny >= 0 && ny < m) {
                if (A[nx][ny] > A[x][y]) {
                    search(nx, ny, A); // dp[nx][ny] must be calcuted
                    dp[x][y] = Math.max(dp[x][y], dp[nx][ny] + 1);
                }
            }
        }
    }
}

 

什么时候用记忆化搜索? 1. 状态转移特别麻烦,不是顺序性

2. 初始化状态不是很容易找到

 

记忆化搜索的缺陷

费更多空间,无法使用滚动数组优化 递归深度可能会很深

 

 

博弈类动态规划 Game DP

见:https://www.cnblogs.com/bonelee/p/17076086.html

 

间类DP

 

特点:
1. 求一段区间的解max/min/count 2. 转移方程通过区间更新
3. 大区间的值依赖于小区间

 

 

 

 

168. 吹气球

 https://leetcode.cn/problems/burst-balloons/

中文

 

English

 

有n个气球,编号为0n-1,每个气球都有一个分数,存在nums数组中。每次吹气球i可以得到的分数为 nums[left] * nums[i] * nums[right],left和right分别表示i气球相邻的两个气球。当i气球被吹爆后,其左右两气球即为相邻。要求吹爆所有气球,得到最多的分数。

 

样例

样例 1:

输入:[4, 1, 5, 10]
输出:270
解释:
nums = [4, 1, 5, 10] 吹爆 1, 得分 4 * 1 * 5 = 20
nums = [4, 5, 10]   吹爆 5, 得分 4 * 5 * 10 = 200 
nums = [4, 10]   吹爆 4, 得分 1 * 4 * 10 = 40
nums = [10]   吹爆 10, 得分 1 * 10 * 1 = 10
总得分 20 + 200 + 40 + 10 = 270

样例 2:

输入:[3,1,5]
输出:35
解释:
nums = [3, 1, 5] 吹爆 1, 得分 3 * 1 * 5 = 15
nums = [3, 5] 吹爆 3, 得分 1 * 3 * 5 = 15
nums = [5] 吹爆 5, 得分 1 * 5 * 1 = 5
总得分 15 + 15 + 5  = 35

 

注意事项

  1. 你可以假设nums[-1] = nums[n] = 1。-1和n位置上的气球不真实存在,因此不能吹爆它们。
  2. 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100

dp[i][j]含义:

 

 

 

 

 

class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        n = len(nums)
        rec = [[0] * (n + 2) for _ in range(n + 2)]
        val = [1] + nums + [1]

        for i in range(n - 1, -1, -1):
            for j in range(i + 2, n + 2):
                for k in range(i + 1, j):
                    total = val[i] * val[k] * val[j]
                    total += rec[i][k] + rec[k][j]
                    rec[i][j] = max(rec[i][j], total)
        
        return rec[0][n + 1]
 

 注意n是递减的,因为dp[i][j] = dp[i][k] + dp[k][j] k比i大,所以i如果递增就取不到后面dp[k][j]的值!

 

class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [[0] * (n+2) for i in range(n+2)]
        nums = [1] + nums + [1]

        for i in range(n+1, -1, -1):
            for j in range(i+2, n+2):
                for k in range(i+1, j):
                    dp[i][j] = max(dp[i][j], dp[i][k]+dp[k][j]+nums[i]*nums[k]*nums[j])

        return dp[0][n+1]

 

396. 硬币排成线 III

中文
English

n 个硬币排成一条线, 第 i 枚硬币的价值为 values[i].

两个参赛者轮流从任意一边取一枚硬币, 直到没有硬币为止. 拿到硬币总价值更高的获胜.

请判定 第一个玩家 会赢还是会输.

样例

样例 1:

输入: [3, 2, 2]
输出: true
解释: 第一个玩家在刚开始的时候拿走 3, 然后两个人分别拿到一枚 2.

样例 2:

输入: [1, 20, 4]
输出: false
解释: 无论第一个玩家在第一轮拿走 1 还是 4, 第二个玩家都可以拿到 20.

挑战

n 是偶数时做到O(1) 空间, O(n) 时间

 

区间动态规划问题, 具体定义状态的方式有很多种, 但是大同小异, 时空复杂度都相似. 这里只介绍 version 1 的具体实现.

设定 dp[i][j] 表示当前剩余硬币的区间为 [i, j] 时, 此时该拿硬币的人能获取的最大值.

注意, dp[i][j] 并没有包含角色信息, dp[0][values.length - 1] 表示的是先手的人能获得的最大值, 而 dp[1][values.length -1] 表示的则是后手的人能获得的最大值. 需要这样做是因为: 两个人都会采用最优策略.

当前的人的决策就是拿左边还是拿右边, 而下一个人仍然会最优决策, 所以应该是最小值中取最大值:

dp[i][j] = max(	                                     // 取max表示当前的人选用最优策略		
    min(dp[i + 2][j], dp[i + 1][j - 1]) + values[i], // 取min表示下一个人选用最优策略
    min(dp[i][j - 2], dp[i + 1][j - 1]) + values[j]
)

几个边界:

i > j : dp[i][j] = 0
i == j : dp[i][j] = values[i]
i + 1 == j : dp[i][j] = max(values[i], values[j])


class Solution:
    # @param values: a list of integers
    # @return: a boolean which equals to True if the first player will win
    def firstWillWin(self, values):
        if not values:
            return False
            
        n = len(values)
        dp = [[0] * n for _  in range(n)]
        sum = [[0] * n for _  in range(n)]
        
        for i in range(n):
            dp[i][i] = values[i]
            sum[i][i] = values[i]
        
        for i in range(n - 2, -1, -1):  # n-2 => 0
            for j in range(i + 1, n):  # i+1 => n-1
                sum[i][j] = sum[i + 1][j] + values[i]
                dp[i][j] = sum[i][j] - min(dp[i + 1][j], dp[i][j - 1])
                
        return dp[0][n - 1] > sum[0][n - 1] - dp[0][n - 1]

 

区间类的动态规划还是挺难的!!!看情况掌握吧。

 

双序列动态规划

77. 最长公共子序列

中文
English

给出两个字符串,找到最长公共子序列(LCS),返回LCS的长度。

样例

样例 1:
	输入:  "ABCD" and "EDCA"
	输出:  1
	
	解释:
	LCS 是 'A' 或  'D' 或 'C'


样例 2:
	输入: "ABCD" and "EACB"
	输出:  2
	
	解释: 
	LCS 是 "AC"

说明

最长公共子序列的定义:

  • 最长公共子序列问题是在一组序列(通常2个)中找到最长公共子序列(注意:不同于子串,LCS不需要是连续的子串)。该问题是典型的计算机科学问题,是文件差异比较程序的基础,在生物信息学中也有所应用。
  • https://en.wikipedia.org/wiki/Longest_common_subsequence_problem
class Solution:
    """
    @param A: A string
    @param B: A string
    @return: The length of longest common subsequence of A and B
    """
    def longestCommonSubsequence(self, A, B):
        # write your code here
        if not A or not B:
            return 0
            
        m, n = len(A), len(B)
        dp = [[0]*n for i in range(m)]
        
        for i in range(0, m):
            if A[i] == B[0]:
                dp[i][0] = 1
        
        for j in range(0, n):
            if A[0] == B[j]:
                dp[0][j] = 1
        
        for i in range(1, m):
            for j in range(1, n):
                if A[i] == B[j]:
                    dp[i][j] = max(dp[i-1][j-1]+1, dp[i][j-1], dp[i-1][j])
                else:
                    dp[i][j] = max(dp[i][j-1], dp[i-1][j])
        
        return dp[m-1][n-1]
        

 记得画个二维的图!!!

Dp[i][j] 表示A序列前i个数,与B的前j个数的LCS长度。
对A的每个位置i,枚举B的每个位置j。

更精简的代码:

class Solution:
    """
    @param A, B: Two strings.
    @return: The length of longest common subsequence of A and B.
    """
    def longestCommonSubsequence(self, A, B):
        n, m = len(A), len(B)
        f = [[0] * (n + 1) for i in range(m + 1)]
        for i in range(n):
            for j in range(m):
                f[i + 1][j + 1] = max(f[i][j + 1], f[i + 1][j])
                if A[i] == B[j]:
                    f[i + 1][j + 1] = f[i][j] + 1
        return f[n][m]

备注:也可以使用滚动数组进行优化!!!

 

119. 编辑距离

中文
English

给出两个单词word1和word2,计算出将word1 转换为word2的最少操作次数。

你总共三种操作方法:

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

样例

样例 1:

输入: 
"horse"
"ros"
输出: 3
解释: 
horse -> rorse (替换 'h' 为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

样例 2:

输入: 
"intention"
"execution"
输出: 5
解释: 
intention -> inention (删除 't')
inention -> enention (替换 'i' 为 'e')
enention -> exention (替换 'n' 为 'x')
exention -> exection (替换 'n' 为 'c')
exection -> execution (插入 'u')


class Solution:
    """
    @param A: A string
    @param B: A string
    @return: The minimum number of steps.
    """
    def minDistance(self, A, B):
        # write your code here
        m, n = len(A), len(B)
        dp = [[0]*(n+1) for i in range(m+1)]
        
        for i in range(m+1):
            dp[i][0] = i
            
        for j in range(n+1):
            dp[0][j] = j
        
        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]
                else:
                    dp[i][j] = min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j])+1
        
        return dp[m][n]

 

dp[i][j] 代表第一个字符串以i结尾匹配上(编辑成)第二个字符串以j结尾的字符串,最少需要多少次编辑。
通过去判断i与j的匹配关系来变为更小的状态。

字符串相似性!!!

 

10. 正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。

  • '.' 匹配任意单个字符
  • '*' 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

 

示例 1:

输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。

示例 2:

输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3:

输入:s = "ab", p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。


核心是对于.*场景 匹配2个以上的状态转移推导!如下:
摘录自:https://leetcode.cn/problems/regular-expression-matching/solution/by-flix-musv/

 

 

 

 

 

 

 

 

 

 

 代码如下:

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        m, n = len(s), len(p)
        dp = [[False]*(n+1) for _ in range(m+1)]
        dp[0][0] = True

        for j in range(2, n+1): # 这里容易踩坑,不要漏掉了
            if p[j-1] == '*':
                dp[0][j] = dp[0][j-2]

        # 下面就是公式转换了
        for i in range(1, m+1):
            for j in range(1, n+1):
                if p[j-1] == '*':
                    if p[j-2] != '.':
                        if p[j-2] != s[i-1]:
                            dp[i][j] = dp[i][j-2]
                        else:
                            dp[i][j] = dp[i][j-2] or dp[i-1][j]
                    else:
                        dp[i][j] = dp[i][j-2] or dp[i-1][j]
                elif p[j-1] == '.':
                    dp[i][j] = dp[i-1][j-1]
                else:
                    if p[j-1] == s[i-1]:
                        dp[i][j] = dp[i-1][j-1]

        return dp[m][n]

 

精简点可以:

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        m, n = len(s), len(p)
        dp = [[False] * (n+1) for _ in range(m+1)]
        
        # 初始化
        dp[0][0] = True
        for j in range(1, n+1):
            if p[j-1] == '*':
                dp[0][j] = dp[0][j-2]

        # 状态更新
        for i in range(1, m+1):
            for j in range(1, n+1):
                if s[i-1] == p[j-1] or p[j-1] == '.':
                    dp[i][j] = dp[i-1][j-1]
                elif p[j-1] == '*':     # 【题目保证'*'号不会是第一个字符,所以此处有j>=2】
                    if s[i-1] != p[j-2] and p[j-2] != '.':
                        dp[i][j] = dp[i][j-2]
                    else:
                        dp[i][j] = dp[i][j-2] | dp[i-1][j]
        
        return dp[m][n]

 

总之,这个题目还是有难度的,公式推导也有难度,尤其是匹配2个以上的情形。

 

29. 交叉字符串

中文
English

给出三个字符串:s1s2s3,判断s3是否由s1s2交叉构成。

样例

样例 1:

输入:
"aabcc"
"dbbca"
"aadbbcbcac"
输出:
true

样例 2:

输入:
""
""
"1"
输出:
false

样例 3:

输入:
"aabcc"
"dbbca"
"aadbbbaccc"
输出:
false

挑战

要求时间复杂度为O(n2)或者更好

class Solution:
    """
    @param A: A string
    @param B: A string
    @param C: A string
    @return: Determine whether s3 is formed by interleaving of s1 and s2
    """
    def isInterleave(self, A, B, C):
        # write your code here
        m, n = len(A), len(B)
        
        if m + n != len(C):
            return False
            
        dp = [[False]*(n+1) for i in range(m+1)]
        
        dp[0][0] = True
        
        for i in range(1, m+1):
            if A[i-1] == C[i-1]:
                dp[i][0] = dp[i-1][0]
            
        for j in range(1, n+1):
            if B[j-1] == C[j-1]:
                dp[0][j] = dp[0][j-1]
        
        for i in range(1, m+1):
            for j in range(1, n+1):
                if A[i-1] == C[i+j-1]:
                    dp[i][j] = dp[i-1][j]
                if B[j-1] == C[i+j-1]:
                    dp[i][j] |= dp[i][j-1]
                
        return dp[m][n]
        
        

 dp[i][j]代表由s1的前i个字母和s2的前j个字母是否能构成当前i+j个字母。
然后状态转移即可。(看第i+j+1个是否能被s1的第i+1个构成或被s2的第j+1个构成)

623. K步编辑

中文
English

给出一个只含有小写字母的字符串的集合以及一个目标串,输出所有可以经过不多于 k 次操作得到目标字符串的字符串。

你可以对字符串进行一下的3种操作:

  • 加入1个字母

  • 删除1个字母

  • 替换1个字母

样例

样例 1:

给出字符串 `["abc","abd","abcd","adc"]`,目标字符串为 `"ac"` ,k = `1`
返回 `["abc","adc"]`
输入:
["abc", "abd", "abcd", "adc"]
"ac"
1
输出:
["abc","adc"]

解释:
"abc" 去掉 "b"
"adc" 去掉 "d"

样例 2:

输入:
["acc","abcd","ade","abbcd"]
"abc"
2
输出:
["acc","abcd","ade","abbcd"]

解释:
"acc" 把 "c" 变成 "b"
"abcd" 去掉 "d"
"ade" 把 "d" 变成 "b"把 "e" 变成 "c"
"abbcd" 去掉 "b" 和 "d"
class TrieNode:
    def __init__(self):
        # Initialize your data structure here.
        self.children = [None for i in range(26)]
        self.hasWord = False
        self.str = None
    
    @classmethod
    def addWord(cls, root, word):
        node = root
        for letter in word:
            child = node.children[ord(letter) - ord('a')]
            if child is None:
                child = TrieNode()
                node.children[ord(letter) - ord('a')] = child
            node = child
    
        node.hasWord = True
        node.str = word

class Solution:
    # @param {string[]} words a set of strings
    # @param {string} target a target string
    # @param {int} k an integer
    # @return {string[]} output all the stirngs that meet the requirements 
    def kDistance(self, words, target, k):
        # Write your code here
        root = TrieNode()
        for word in words:
            TrieNode.addWord(root, word)

        result = []
        n = len(target)
        dp = [i for i in range(n + 1)]

        self.find(root, result, k, target, dp)
        return result

    def find(self, node, result, k, target, dp):
        n = len(target)

        if node.hasWord and dp[n] <= k:
            result.append(node.str)

        next = [0 for i in range(n + 1)]

        for i in range(26):
            if node.children[i] is not None:
                next[0] = dp[0] + 1
                for j in range(1, n + 1):
                    if ord(target[j - 1]) - ord('a') == i:
                        next[j] = min(dp[j - 1], min(next[j - 1] + 1, dp[j] + 1))
                    else:
                        next[j] = min(dp[j - 1] + 1, min(next[j - 1] + 1, dp[j] + 1))

                self.find(node.children[i], result, k, target, next)

 挺难的题目,使用了Trie,结合DP。


背包类DP

92. 背包问题

中文
English

在n个物品中挑选若干物品装入背包,最多能装多满?假设背包的大小为m,每个物品的大小为A[i]

样例

样例 1:
	输入:  [3,4,8,5], backpack size=10
	输出:  9

样例 2:
	输入:  [2,3,5,7], backpack size=12
	输出:  12
	

挑战

O(n x m) time and O(m) memory.

O(n x m) memory is also acceptable if you do not know how to optimize memory.

注意事项

你不可以将物品进行切割。


class Solution:
    """
    @param m: An integer m denotes the size of a backpack
    @param A: Given n items with size A[i]
    @return: The maximum size
    """
    def backPack(self, m, A):
        # write your code here
        # f[i][j]表示前i个物品选一些物品放入容量为j的背包中能否放满。
        n = len(A)
        f = [[False] * (m + 1) for _ in range(n + 1)]
        
        f[0][0] = True
        for i in range(1, n + 1):
            f[i][0] = True
            for j in range(1, m + 1):
                if j >= A[i - 1]:
                    f[i][j] = f[i - 1][j] or f[i - 1][j - A[i - 1]]
                else:
                    f[i][j] = f[i - 1][j]
                    
        for i in range(m, -1, -1):
            if f[n][i]:
                return i
        return 0
            
        
        

 

 

自己画一个二维的矩阵图,推演下。

 

 

125. 背包问题 II

中文
English

n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值.

问最多能装入背包的总价值是多大?

样例

样例 1:

输入: m = 10, A = [2, 3, 5, 7], V = [1, 5, 2, 4]
输出: 9
解释: 装入 A[1] 和 A[3] 可以得到最大价值, V[1] + V[3] = 9 

样例 2:

输入: m = 10, A = [2, 3, 8], V = [2, 5, 8]
输出: 10
解释: 装入 A[0] 和 A[2] 可以得到最大价值, V[0] + V[2] = 10

挑战

O(nm) 空间复杂度可以通过, 不过你可以尝试 O(m) 空间复杂度吗?

注意事项

  1. A[i], V[i], n, m 均为整数
  2. 你不能将物品进行切分
  3. 你所挑选的要装入背包的物品的总大小不能超过 m
  4. 每个物品只能取一次

 

 

 

设定 f[i][j] 表示前 i 个物品装入大小为 j 的背包里, 可以获取的最大价值总和. 决策就是第i个物品装不装入背包, 所以状态转移方程就是 f[i][j] = max(f[i - 1][j], f[i - 1][j - A[i]] + V[i])

class Solution:
    # @param m: An integer m denotes the size of a backpack
    # @param A & V: Given n items with size A[i] and value V[i]
    def backPackII(self, m, A, V):
        # write your code here
        f = [0 for i in range(m+1)]
        n = len(A)
        for i in range(n):
            for j in range(m, A[i]-1, -1):
                f[j] = max(f[j] , f[j-A[i]] + V[i])
        return f[m]

 

440. 背包问题 III

中文
English

给定 n 种物品, 每种物品都有无限个. 第 i 个物品的体积为 A[i], 价值为 V[i].

再给定一个容量为 m 的背包. 问可以装入背包的最大价值是多少?

样例

样例 1:

输入: A = [2, 3, 5, 7], V = [1, 5, 2, 4], m = 10
输出: 15
解释: 装入三个物品 1 (A[1] = 3, V[1] = 5), 总价值 15.

样例 2:

输入: A = [1, 2, 3], V = [1, 2, 3], m = 5
输出: 5
解释: 策略不唯一. 比如, 装入五个物品 0 (A[0] = 1, V[0] = 1).

注意事项

  1. 不能将一个物品分成小块.
  2. 放入背包的物品的总大小不能超过 m.

 

class Solution:
    # @param {int[]} A an integer array
    # @param {int[]} V an integer array
    # @param {int} m an integer
    # @return {int} an array
    def backPackIII(self, A, V, m):
        # Write your code here
        f = [0 for i in range(m+1)]
        for (a, v) in zip(A, V):
            for j in range(a, m+1):
                if f[j - a] + v > f[j]:
                    f[j] = f[j - a] + v
        return f[m]

 

 

 

 

 

 

 

 

posted @ 2019-12-01 21:06  bonelee  阅读(782)  评论(0编辑  收藏  举报