算法与数据结构(二):动态规划(DP)总结

1. 最长公共子序列

题目描述

对于两个字符串,请设计一个高效算法,求他们的最长公共子序列的长度,这里的最长公共子序列定义为有两个序列U1,U2,U3…Un和V1,V2,V3…Vn,其中 Ui<Ui+1,Vi<Vi+1。且A[Ui] == B[Vi]。

给定两个字符串AB,同时给定两个串的长度nm,请返回最长公共子序列的长度。保证两串长度均小于等于300。

测试样例:

"1A2C3D4B56",10,"B1D23CA45B6A",12
返回:6

思路 - DP

  • DP 定义

    • s[0:i] := s 长度为 i 的**前缀**
    • 定义 dp[i][j] := s1[0:i] 和 s2[0:j] 最长公共子序列的长度
  • DP 初始化

    dp[i][j] = 0    当 i=0 或 j=0 时
    
  • DP 更新

    • s1[i] == s2[j] 时,dp[i][j] = dp[i-1][j-1] + 1

    • s1[i] != s2[j] 时,dp[i][j] = max(dp[i-1][j], dp[i][j-1])

  • 完整递推公式

    dp[i][j] = 0                              当 i=0 或 j=0 时
             = dp[i-1][j-1] + 1               当 `s1[i-1] == s2[j-1]` 时
             = max(dp[i-1][j], dp[i][j-1])    当 `s1[i-1] != s2[j-1]` 时
    

参考答案:

# -*- coding:utf-8 -*-

class LCS:
    def findLCS(self, A, n, B, m):
        # write code here
        dp = [[0] * (m + 1) for _ in range(n + 1)]
        
        for i in range(1, n + 1):
            for j in range(1, m + 1):
                if A[i-1] == B[j-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                else:
                    dp[i][j] = max(dp[i][j-1], dp[i-1][j])
        return dp[n][m]


2. 最长公共子串

题目描述

对于两个字符串,请设计一个时间复杂度为O(m*n)的算法(这里的m和n为两串的长度),求出两串的最长公共子串的长度。这里的最长公共子串的定义为两个序列U1,U2,…Un和V1,V2,…Vn,其中Ui + 1 == Ui+1,Vi + 1 == Vi+1,同时Ui == Vi。

给定两个字符串AB,同时给定两串的长度nm

测试样例:

"1AB2345CD",9,"12345EF",7
返回:4

思路 - DP

  • DP 定义

    • s[0:i] := s 长度为 i 的 前缀
    • 定义 dp[i][j] := s1[0:i] 和 s2[0:j] 最长公共子串的长度
    • dp[i][j] 只有当 s1[i] == s2[j] 的情况下才是 s1[0:i] 和 s2[0:j] 最长公共子串的长度
  • DP 初始化

    dp[i][j] = 0    当 i=0 或 j=0 时
    
  • DP 更新

    dp[i][j] = dp[i-1][j-1] + 1     if s[i] == s[j]
             = ;                    else pass
    

参考答案:

# -*- coding:utf-8 -*-

class LongestSubstring:
    def findLongest(self, A, n, B, m):
        # write code here
        dp = [[0] * (m+1) for _ in range(n+1)]
        temp_res = 0
        for i in range(1, n+1):
            for j in range(1, m+1):
                if A[i-1] == B[j-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                    temp_res = max(temp_res, dp[i][j])
        return temp_res

3. 最长递增子序列

题目描述

对于一个数字序列,请设计一个复杂度为O(nlogn)的算法,返回该序列的最长上升子序列的长度,这里的子序列定义为这样一个序列U1,U2…,其中Ui < Ui+1,且A[Ui] < A[Ui+1]。

给定一个数字序列A及序列的长度n,请返回最长上升子序列的长度。

测试样例:

[2,1,4,3,1,5,6],7
返回:4

思路0 - O(N^2)

  • LIS 可以转化成 LCS (最长公共子序列) 问题
  • 用另一个序列保存给定序列的排序结果 - O(NlogN)
  • 则问题转化为求这两个序列的 LCS 问题 - O(N^2)

参考答案:

# -*- coding:utf-8 -*-

class AscentSequence:
    def findLongest(self, A, n):
        # write code here
        B = sorted(A)
        return self.findCls(A, B)
        
        
    def findCls(self, A, B):
        dp = [[0] * (len(B) + 1) for _ in range(len(A)+1)]
        for i in range(1, len(A) + 1):
            for j in range(1, len(B) + 1):
                if A[i-1] == B[j-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                else:
                    dp[i][j] = max(dp[i][j-1], dp[i-1][j])
        return dp[-1][-1]

思路1 - O(N^2)解法

  • DP 定义

    • nums[0:i] := 序列 nums 的前 i 个元素构成的子序列
    • 定义 dp[i] := nums[0:i] 中 LIS 的长度
    • 实际并没有严格按照这个定义,中间使用一个变量记录当前全局最长的 LIS
  • DP 初始化

    dp[:] = 1  // 最长上升子序列的长度最短为 1
    
  • DP 更新 - O(N^2)的解法

    dp[i] = max{dp[j]} + 1,  if nums[i] > nums[j]
          = max{dp[j]},      else
    where 0 <= j < i
    

    如果只看这个递推公式,很可能会写出如下的错误代码

    // 牛客网
    class AscentSequence {
    public:
        int findLongest(vector<int> nums, int n) {
            vector<int> dp(n, 1);
    
            for (int i = 1; i < n; i++) {
                for (int j = 0; j < i; j++)
                    if (nums[i] > nums[j])
                        dp[i] = max(dp[i], dp[j] + 1);
                    else
                        dp[i] = max(dp[i], dp[j]);
            }
    
            return dp[n-1];
        }
    };
    
  • 下面是网上比较流行的一种递推公式

    dp[i] = dp[j] + 1,  if nums[i] > nums[j] && dp[i] < dp[j] + 1
          = pass,       else
    where 0 <= j < i
    
    • 注意:此时并没有严格按照定义处理 dp,它只记录了当 nums[i] > nums[j] && dp[i] < dp[j] + 1 时的 LIS;不满足该条件的情况跳过了;所以需要额外一个变量记录当前已知全局的 LIS
# -*- coding:utf-8 -*-

class AscentSequence:
    def findLongest(self, A, n):
        # write code here
        if n <= 1:
            return 1
        dp = [1] * (n)
        res = 1
        for i in range(1, n):
            for j in range(i):
                if A[i] > A[j] and dp[i] < dp[j] + 1:
                    dp[i] = dp[j] + 1
            res = max(res, dp[i])
        return res

4. 最长回文子序列

最长回文子序列 - LeetCode

问题描述

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

示例 1:
  输入:
    "bbbab"
  输出:
    4
  一个可能的最长回文子序列为 "bbbb"。

思路

  • 相比最长回文子串,最长回文子序列更像最长公共子序列,只是改变了循环方向

  • DP 定义

    • s[i:j] := 字符串 s 在区间 [i:j] 上的子串
    • 定义 dp[i][j] := s[i:j] 上回文序列的长度
  • DP 初始化

    dp[i][i]   = 1  // 单个字符也是一个回文序列
    
  • DP 更新

    dp[i][j] = dp[i+1][j-1] + 2,              if s[i] == s[j]
             = max(dp[i+1][j], dp[i][j-1]),   else
    
    比较一下 LCS 的递推公式
    dp[i][j] = 0                              当 i=0 或 j=0 时
             = dp[i-1][j-1] + 1               当 `s1[i-1] == s2[j-1]` 时
             = max(dp[i-1][j], dp[i][j-1])    当 `s1[i-1] != s2[j-1]` 时
    

class Solution(object):
    def longestPalindromeSubseq(self, s):
        """
        :type s: str
        :rtype: int
        """
        if not s:
            return 0
            
        dp = [[0] * len(s) for _ in range(len(s))]
        for i in range(len(s)):
            dp[i][i] = 1
            
        for j in range(1, len(s)):
            for i in range(j-1, -1, -1):
                if s[i] == s[j]:
                    dp[i][j] = dp[i+1][j-1] + 2
                else:
                    dp[i][j] = max(dp[i+1][j], dp[i][j-1])
        return dp[0][len(s)-1]
            

4. 最长回文子串

最长回文子串_牛客网

最长回文子串 - LeetCode

牛客网只需要输出长度;LeetCode 还需要输出一个具体的回文串

问题描述

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

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

思路 - O(N^2)

  • DP 定义

    • s[i:j] := 字符串 s 在区间 [i:j] 上的子串
    • 定义 dp[i][j] := s[i:j] 是否是一个回文串
  • DP 初始化

    dp[i][i]   = 1  // 单个字符也是一个回文串
    
  • DP 更新

    dp[i][j] = dp[i+1][j-1],  if s[i] == s[j]
             = 0,             else
    
    注意到:如果 j - i < 2 的话(比如 j=2, i=1),dp[i+1][j-1]=dp[2][1] 会出现不符合 DP 定义的情况
    所以需要添加边界条件
      
      dp[i][i+1] = 1,  if s[i] == s[i+1]
                 = 0,  else
      
    该边界条件可以放在初始化部分完成;但是建议放在递推过程中完成过更好(为了兼容牛客和LeetCode)
    

Python DP 版本:

class Solution(object):
    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str
        """
        if not s or len(s) == 1:
            return s
        dp = [[0] * len(s) for _ in range(len(s))]
        for i in range(len(s)):
            dp[i][i] = 1
        begin = 0
        end = 0
        length = 1
        for j in range(1, len(s)):
            for i in range(j-1, -1, -1):
                if j - i < 2:
                    dp[i][j] = 1 if s[i] == s[j] else 0
                elif s[i] == s[j]:
                    dp[i][j] = dp[i+1][j-1]
                else:
                    dp[i][j] = 0
                if dp[i][j] and j - i + 1 > length:
                    begin = i
                    length = j - i + 1
                    
        return s[begin : begin+length]
        

Python 双指针版本:

class Solution(object):
    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str
        """

        if len(s) <= 1:
            return s
        self.begin, self.str_len = 0, 0
        for index in range(len(s)-1):
            self.helper(s, index, index)
            self.helper(s, index, index+1)
        return s[self.begin: self.begin + self.str_len]
            
                
    def helper(self, s, left, right):
        length = len(s)
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left -= 1
            right += 1
        if right - left - 1 > self.str_len:
            self.begin = left + 1
            self.str_len = right - left - 1
           

5. 最大连续子序列和

最大连续子序列_牛客网

牛客网要求同时输出最大子序列的首尾元素

思路 - 基本问题:只输出最大连续子序列和

  • DP 定义

    • a[0:i] := 序列 a 在区间 [0:i] 上的子序列
    • 定义 dp[i] := a[0:i] 上的最大子序列和
    • 实际并没有严格按照上面的定义,中间使用一个变量记录当前全局的最大连续子序列和
  • DP 初始化

    dp[0] = a[0]
    
  • DP 更新

    // 只要 dp[i] > 0 就一直累加下去,一旦小于 0 就重新开始
    dp[i] = dp[i-1] + a[i],     if dp[i-1] > 0
          = a[i],               else
    
    ret = max{ret, dp[i]}       // 只要大于 0 就累加会导致 dp[i] 保存的并不是 a[0:i] 中的最大连续子序列和
                                // 所以需要一个变量保存当前全局的最大连续子序列和
    

Python 原始DP:


void foo() {
    int n;
    while (cin >> n) {
        vector<int> a(n);
        for (int i = 0; i<n; i++)
            cin >> a[i];

        vector<int> dp(n);
        dp[0] = a[0];

        int ret = a[0];
        for (int i = 1; i < n; i++) {
            if (dp[i - 1] > 0)
                dp[i] = dp[i - 1] + a[i];
            else
                dp[i] = a[i];

            ret = max(ret, dp[i]);
        }
        cout << ret << endl;
    }
}
/*
输入
5
1 5 -3 2 4
6
1 -2 3 4 -10 6
4
-3 -1 -2 -5

输出
9
7
-1
*/

DP 优化

注意到每次递归实际只用到了 dp[i-1],实际只要用到一个变量,空间复杂度 O(1)

void foo2() {
    int n;
    while (cin >> n) {
        vector<int> a(n);
        for (int i = 0; i<n; i++)
            cin >> a[i];

        int ret = INT_MIN;
        int max_cur = 0;

        for (int i = 0; i < n; i++) {
            if (max_cur > 0)       // 如果大于 0 就一直累加
                max_cur += a[i];
            else                   // 一旦小于 0 就重新开始
                max_cur = a[i];

            if (max_cur > ret)     // 保存找到的最大结果
                ret = max_cur;

            // 以上可以简写成下面两行代码
            //max_cur = max(max_cur + a[i], a[i]);
            //ret = max(ret, max_cur);
        }
        cout << ret << endl;
    }
}

6. 编辑距离

LeetCode-编辑距离

问题描述

给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数。

你可以对一个单词进行如下三种操作:
  插入一个字符
  删除一个字符
  替换一个字符

示例:
  输入: word1 = "horse", word2 = "ros"
  输出: 3
  解释: 
  horse -> rorse (将 'h' 替换为 'r')
  rorse -> rose (删除 'r')
  rose -> ros (删除 'e')
  • 注意:编辑距离指的是将 word1 转换成 word2

思路

  • 用一个 dp 数组维护两个字符串的前缀编辑距离

  • DP 定义

    • word[0:i] := word 长度为 i 的**前缀子串**
    • 定义 dp[i][j] := 将 word1[0:i] 转换为 word2[0:j] 的操作数
  • 初始化

    dp[i][0] = i  // 每次从 word1 删除一个字符
    dp[0][j] = j  // 每次向 word1 插入一个字符
    
  • 递推公式

    • word1[i] == word1[j]dp[i][j] = dp[i-1][j-1]

    • word1[i] != word1[j] 时,有三种更新方式,

      取最小

      // word[1:i] 表示 word 长度为 i 的前缀子串
      dp[i][j] = min({ dp[i-1][j]   + 1 ,     // 将 word1[1:i-1] 转换为 word2[1:j] 的操作数 + 删除 word1[i] 的操作数(1)
                       dp[i][j-1]   + 1 ,     // 将 word1[0:i] 转换为 word2[0:j-1] 的操作数 + 将 word2[j] 插入到 word1[0:i] 之后的操作数(1)
                       dp[i-1][j-1] + 1 })    // 将 word1[0:i-1] 转换为 word2[0:j-1] 的操作数 + 将 word1[i] 替换为 word2[j] 的操作数(1)
      

Python DP:

class Solution(object):
    def minDistance(self, word1, word2):
        """
        :type word1: str
        :type word2: str
        :rtype: int
        """
        if (not word1) and (not word2):
            return 0
        if not word1:
            return len(word2)
        if not word2:
            return len(word1)
        
        dp = [[0] * (len(word2) + 1) for _ in range(len(word1) + 1)]
        for i in range(len(word1)+1):
            dp[i][0] = i
        for j in range(len(word2)+1):
            dp[0][j] = j
        
        for i in range(1, len(word1) + 1):
            for j in range(1, len(word2) + 1):
                if word1[i-1] == word2[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 
        return dp[-1][-1]

7. 矩阵中的最大正方形

LeetCode-221. 最大正方形

问题描述

在一个由 0 和 1 组成的二维矩阵 M 内,找到只包含 1 的最大正方形,并返回其面积。

示例:

输入: 
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0

输出: 
4

思路

  • DP 定义

    dp[i][j] := 以 M[i][j] 为正方形**右下角**所能找到的最大正方形的边长
    
    • 注意保存的是边长
    • 因为 dp 保存的不是全局最大值,所以需要用一个额外变量更新结果
  • 初始化

    dp[i][0] = M[i][0]
    dp[0][j] = M[0][j]
    
  • 递推公式

    dp[i][j] = min{dp[i-1][j], 
                   dp[i][j-1], 
                   dp[i-1][j-1]} + 1  若 M[i][j] == 1
             = 0                      否则
    

    注意到,本题的递推公式与 编辑距离 完全一致

Python:

class Solution(object):
    def maximalSquare(self, matrix):
        """
        :type matrix: List[List[str]]
        :rtype: int
        """
        if not matrix or not matrix[0]:
            return 0
        dp = [[0] * len(matrix[0]) for _ in range(len(matrix))]
        base = 0
        for i in range(len(matrix)):
            dp[i][0] = int(matrix[i][0])
            base = max(base, dp[i][0])
        for j in range(len(matrix[0])):
            dp[0][j] = int(matrix[0][j])
            base = max(base, dp[0][j])
        for i in range(1, len(matrix)):
            for j in range(1, len(matrix[0])):
                if matrix[i][j] == '1':
                    dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
                    base = max(base, dp[i][j])
                else:
                    dp[i][j] = 0
        
        return base * base
                

8. 硬币找零

LeetCode - 322. 零钱兑换

问题描述

给定不同面额的硬币 coins 和一个总金额 amount。
编写一个函数来计算可以凑成总金额所需的最少的硬币个数。
如果没有任何一种硬币组合能组成总金额,返回 -1。

示例 1:

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

示例 2:

    输入: coins = [2], amount = 3
    输出: -1
    
说明:
    你可以认为每种硬币的数量是无限的。

思路

  • 定义dp[i] := 组成总金额 i 时的最少硬币数

  • 初始化:

    dp[i] = 0       若 i=0
          = INF     其他
    
  • 状态转移

    dp[j] = min{ dp[j-coins[i]] + 1 | i=0,..,n-1 }
        
    其中 coins[i] 表示硬币的币值,共 n 种硬币
    

Python DP:

class Solution(object):
    def coinChange(self, coins, amount):
        """
        :type coins: List[int]
        :type amount: int
        :rtype: int
        """
        if not coins or amount < 0:
            return -1
      
        dp = [amount + 1] * (amount + 1)
        dp[0] = 0
        for coin in coins:
            for i in range(coin, amount + 1):       
                dp[i] = min(dp[i], dp[i - coin] + 1)
        return dp[amount] if dp[amount] != amount + 1 else -1

9. 硬币组合

LeetCode - 518. 零钱兑换 II

问题描述:

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

Python

class Solution(object):
    def change(self, amount, coins):
        """
        :type amount: int
        :type coins: List[int]
        :rtype: int
        """

        dp = [0] * (amount + 1)
        dp[0] = 1
        for coin in coins:
            for i in range(coin, amount+1):
                dp[i] += dp[i - coin]
        return dp[amount]

你可能感兴趣的:(算法与数据结构)