Leetcode - 动态规划1

mhxin

5. 最长回文子串

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

示例 1:

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

示例 2:

输入: "cbbd"
输出: "bb"

思路1
中心扩展, 即依次以每个字符作为回文串的中心字符, 先两侧扩展. 这里需要注意的是, 需要分别考虑回文串为奇数和偶数的情况.

def longestPalindrome(s):

    if not s or len(s) < 2:
        return s
        
    n = len(s)

    max_len = 1
    start_idx = 0
    
    # 依次以每个字符作为回文串的中心, 向两侧扩张
    for i in range(n):
        j = 0
        
        # 当回文串的长度为奇数时
        while  i - j >= 0 and i + j < n:
            if s[i-j] == s[i + j]:
                if 2 * j + 1 > max_len:
                    max_len = 2 * j + 1
                    start_idx = i - j
                j += 1
            else:
                break

        # 当回文串的长度为偶数时
        j = 0
        while  i - j - 1 >= 0 and i + j < n:
            if s[i-j-1] == s[i + j]:
                if 2 * (j + 1) > max_len:
                    max_len = 2 * (j + 1)
                    start_idx = i - j - 1
                j += 1
            else:
                break
                
    return s[start_idx: start_idx+max_len]

思路2
动态规划

def longestPalindrome(s):
    n = len(s)
    dp = [[False] * n for _ in range(n)]
    ans = ""

    # 枚举子串的长度 l+1
    for l in range(n):
        # 枚举子串的起始位置 i,这样可以通过 j=i+l 得到子串的结束位置
        for i in range(n):
            j = i + l
            if j >= len(s):
                break
            if l == 0:
                dp[i][j] = True
            elif l == 1:
                dp[i][j] = (s[i] == s[j])
            else:
                dp[i][j] = (dp[i + 1][j - 1] and s[i] == s[j])
            if dp[i][j] and l + 1 > len(ans):
                ans = s[i:j+1]
    return ans

72. 编辑距离

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

你可以对一个单词进行如下三种操作:

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

示例 1:

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

示例 2:

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

以下网格表示word1和word2对应位置相互变换所需要的步数,用二维数组dp来表示

$dp[i][j]$表示将word1的i位置变到word2的j位置所需要的步数。

数组dp的计算过程如下

$$\begin{align} dp[i][j] &=dp[i-1][j-1], if\quad word1[i]=word2[j] \\ dp[i][j]&=min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1,other\end{align}$$

Note:针对第一行、第一列要单独考虑,引入‘ ’, 如下图所示。
Snipaste_2019-05-29_15-28-02.png
第一行表示从‘ ’变为word2的步数(也可以理解成word2变为‘ ’的步数, 是一致的),即插入的次数。

第一列表示从word1变为‘ ’的步数, 即删除的次数。

自底向上


def minDistance(word1, word2):
    if word1 == '':
        return len(word2)
    if word2 == '':
        return len(word1)
    n1 = len(word1)
    n2 = len(word2)
    
    
    dp = [[0] * (n2+1) for i in range(n1+1)]
    for i in range(n1+1):
        dp[i][0] = i
    for i in range(n2+1):
        dp[0][i] = i
        
    for i in range(1, n1+1):
        for j in range(1, n2+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-1], dp[i][j-1], dp[i-1][j]) + 1
    return dp[-1][-1]

时间复杂度为$O(n^2)$, 空间复杂度为$O(n^2)$

至顶向下, 递归解法


def minDistance(word1, word2):
    if word1 == '':
        return len(word2)
    if word2 == '':
        return len(word1)
    n1 = len(word1)
    n2 = len(word2)
    w1 = [i for i in range(n1+1)]
    w2 = [i for i in range(n2+1)]
    # 不添加下面的语句, 会超时
    
    import functools
    # functools.lru_cache会将中间的结果保存, 来避免大量的重复计算
    @functools.lru_cache(None)
    
    
    def helper(m, n):
        if m == 1:
            return w2[n-1]
        if n == 1:
            return w1[m-1]
        
        if word1[m-1] == word2[n-1]:
            return helper(m-1, n-1)
        else:
            return min(helper(m-1, n-1), helper(m-1, n), helper(m, n-1)) +1
    return helper(n1+1, n2+1)
        

198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
  偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
  偷窃到的最高金额 = 2 + 9 + 1 = 12 。

第$n$个数能偷的最大值为$dp(n)$

$dp(n) = max(dp(n-2) + nums(n), dp(n-1))$

  • 当前元素偷: 前一个不能偷, 能偷的最大值等于$dp(n-2)+nums(n)$
  • 当前元素不偷: 前一个元素能偷的最大值, 前一个元素偷不偷不要紧, 能偷的最大值为$dp(n-1)$
  • 能偷的最大值为$dp(n) = max(dp(n-2) + nums(n), dp(n-1))$
def rob(nums):
    if not nums:
        return 0
    if len(nums) == 1:
        return nums[0]
       if len(nums) == 2:
        return max(nums[0], nums[1])
    rob_money = [nums[0], max(nums[0], nums[1])]

    n = len(nums)
    for i in range(2, n):
        rob_money[0], rob_money[1] = rob_money[1], max(rob_money[0]+nums[i], rob_money[1])

    return rob_money[1]    

213. 打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:

输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
  偷窃到的最高金额 = 1 + 3 = 4 。

如果第一个元素偷了, 则最后一个不能偷. 反之, 最后一个偷了, 第一个就不能偷, 总之不能同时偷.

那么我们就可以分别计算不偷第一个的最大值和不偷最后一个的最大值


def rob(nums):
    if not nums:
        return 0
    if len(nums) == 1:
        return nums[0]
       if len(nums) == 2:
        return max(nums[0], nums[1])
    if len(nums) == 3:
        return max(nums)
    
    # 不偷第一个
    dp = [nums[1], max(nums[1], nums[2])]
    n = len(nums)
    for i in range(3, n):
        dp[0], dp[1] = dp[1], max(dp[0]+nums[i], dp[1])
        
    money_without_first = dp[1]
    
    # 不偷最后一个
    dp = [nums[0], max(nums[0], nums[1])]
    for i in range(2, n-1):
        dp[0], dp[1] = dp[1], max(dp[0]+nums[i], dp[1])
        
    money_without_last = dp[1]
    
    return max(money_without_first, money_without_last)

516. 最长回文子序列

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

 

示例 1:
输入:

"bbbab"

输出:

4

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

示例 2:
输入:

"cbbd"

输出:

2

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

def longestPalindromeSubseq(s) -> int:      
    n = len(s)

    if n <= 1 or s[::-1] == s:
        return n
    
    dp = [0]*n
    for i in range(n-1, -1, -1):
        temp = 0
        dp[i] = 1            
        for j in range(i+1, n):
            if s[i] == s[j]:
                temp, dp[j] = dp[j], temp + 2
            else:
                temp = dp[j]
                if dp[j-1] > dp[j]:
                    dp[j] = dp[j-1]
    return dp[-1]

674. 最长连续递增序列

给定一个未经排序的整数数组,找到最长且连续的的递增序列,并返回该序列的长度。

 

示例 1:

输入: [1,3,5,4,7]
输出: 3
解释: 最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为5和7在原数组里被4隔开。

示例 2:

输入: [2,2,2,2,2]
输出: 1
解释: 最长连续递增序列是 [2], 长度为1。
 

注意:数组长度不会超过10000。

这同样是一个典型的动态规划问题
状态: 以i为结尾的最长递增子序列长度, $f(i)$
选择: 是否将当前的数字加入到前一个最长子序列中.

状态转移方程:
$$\begin{align} f(n)=\begin{cases}f(n-1)+1,if(nums[n-1]>nums[n-2])\\ 1,else\end{cases}\end{align}$$

def findLengthOfLCIS(nums) -> int:
    if not nums or len(nums) == 0:
        return 0

    if len(nums) == 1:
        return 1

    # 状态转移方程为: dp(i) = dp(i-1) + 1 if nums[i] > nums[i-1] else 1 

    res = 1
    dp = 1
    
    n = len(nums)
    for i in range(1, n):
        if nums[i] > nums[i-1]:
            dp = dp + 1
            res = max(res, dp)
        else:
            dp = 1
    return res
阅读 598
82 声望
12 粉丝
0 条评论
82 声望
12 粉丝
文章目录
宣传栏