动态规划

一 递归,贪心,动态规划,搜索

迷宫的例子

二 DP四要素

三 DP的本质

四 DP的应用

1 什么时候用DP 求最值,判断可行性,统计方案个数

2 什么时候一定不用DP 求出所有方案, 输入的是一个集合而不是序列(任意改变顺序不影响结果,背包型问题除外)

暴力算法已经是多项式级别(DP擅长把指数级别的算法改写成多项式级别)

五 根据状态划分为不同的问题

1)坐标型

从特定的方向移动,不回头向前走,不能形成环(无法区分大状态和小状态)

state: f[x][y[表示从起点到坐标(x, y)...

function: 前一个位置与当前位置的关系

initialize: 起点

answer: 终点

例子:64 Minimum Path Sum

Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right which minimizes the sum of all numbers along its path. You can only move either down or right at any point in time.

求最小值,改变顺序影响结果,暴力算法是指数级别,判断可以使用DP

class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        m = len(grid)
        n = len(grid[0])
        dp = [[0 for i in range(n)] for j in range(m)]

        # state dp[i][j]表示从起点到(i,j)的最短距离
        dp[0][0] = grid[0][0]
        for i in range(1, m):
            dp[i][0] = dp[i - 1][0] + grid[i][0]
        
        for j in range(1, n):
            dp[0][j] = dp[0][j - 1] + grid[0][j]
            
        for i in range(1, m):
            for j in range(1, n):
                dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
        # 可进行优化,将空间优化为一维数组        

        return dp[m-1][n-1]

62 Unique Paths

A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).

The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).

How many possible unique paths are there?

求方案总数,改变grid顺序影响结果,只能从特定方向走,暴力依旧是指数级别,判断可用坐标性DP

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[1 for i in range(n)] for j in range(m)]
        # dp[i][j]表示到达坐标(i, j)的方案数
        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[-1][-1]

63. Unique Paths II

Now consider if some obstacles are added to the grids. How many unique paths would there be?

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        if obstacleGrid[0][0] == 1:
            return 0
        
        m = len(obstacleGrid)
        n = len(obstacleGrid[0])
        
        dp = [[1 for i in range(n)] for j in range(m)]
        for i in range(1, m):
            if obstacleGrid[i][0] == 1:
                dp[i][0] = 0
            else:
                dp[i][0] = dp[i - 1][0]
        
        for j in range(1, n):
            if obstacleGrid[0][j] == 1:
                dp[0][j] = 0
            else:
                dp[0][j] = dp[0][j - 1]
        
        # dp[i][j]表示到达坐标(i, j)的方案数
        for i in range(1, m):
            for j in range(1, n):
                if obstacleGrid[i][j] == 1:
                    dp[i][j] = 0
                else:
                    dp[i][j] = dp[i][j - 1] + dp[i - 1][j]
       
        return dp[-1][-1]

# 这是一道典型的二维动态规划,但只需要dp[i - 1][j]和dp[i][j - 1] 就能更新dp[i][j], 所以可以用一维替代二维空间,即为dp[j] = dp[j] + dp[j - 1], (dp[j]表示dp[i - 1][j]) and dp[j - 1]表示dp[i][j - 1])

        dp = [1 for i in range(n)]
        for j in range(n):
            if obstacleGrid[0][j] == 1:
                dp[j:] = [0] * len(dp[j:])
                break
                
        for i in range(1, m):
            for j in range(n):
                if obstacleGrid[i][j] == 1:
                    dp[j] = 0
                elif j > 0:
                    dp[j] = dp[j] + dp[j - 1]  # 上一行的j更新当前j
                    
        return dp[-1]



55. Jump Game

Given an array of non-negative integers, you are initially positioned at the first index of the array.

Each element in the array represents your maximum jump length at that position.

Determine if you are able to reach the last index.

判断可行性,不可随意交换次序,暴力方法是指数级别,特定方向移动,坐标型dp

本题最优的解法是贪心算法,贪心算法只能解决一道题,不能解决一类题,面试不予考虑。DP的时间复杂度是O(n^2),这道题ace不了,不用care。

#         reach = 0
#         l = len(nums)
        
#         for i in range(0, l):
#             if i > reach or reach >= l:
#                 break
#             else:
#                 reach = max(reach, i + nums[i])
        
#         return (reach >= l-1)
        
#         l = len(nums)
#         dp = [True] * l
    
#         for i in range(l):
#             for j in range(i)[::-1]:
#                 if nums[j] + j >= i and dp[j]:
#                     dp[i] = True
#                     break
#                 if j == 0:
#                     dp[i] = False
        
#         return dp[-1]

45. Jump Game II

Given an array of non-negative integers, you are initially positioned at the first index of the array.

Each element in the array represents your maximum jump length at that position.

Your goal is to reach the last index in the minimum number of jumps.

求最小步数,不可交换,暴力指数,单向移动,可用坐标型DP

class Solution:
    def jump(self, nums: List[int]) -> int:
        
        l = len(nums)
        dp = [0] * l
        for i in range(1, l):
            mini = float('inf')
            for j in range(i):
                if j + nums[j] >= i:
                    mini = min(dp[j] + 1, mini)
            dp[i] = mini
        
        return dp[-1]


#   BFS的方法最优,找到最远的到达的位置,下一层的起点则是当前层最远位置 + 1,返回层数 
    def jump(self, nums: List[int]) -> int:
        
        l = len(nums)
        
        # 树的每层起点start,终点end,高度height
#         start = 0
#         end = 0
#         reach = 0
#         height = 0
        
#         while end < l - 1:
#             height += 1
#             for i in range(start, end+1):
#                 if nums[i] + i >= l-1:
#                     return height
#                 reach = max(reach, nums[i] + i)
#             start = end + 1
#             end = reach
            

#         return height

经典题目

300. Longest Increasing Subsequence

Given an unsorted array of integers, find the length of longest increasing subsequence.

找最长的,不可改变顺序,暴力破解是阶乘,只能向高处移动,考虑用坐标动态规划

不同的是:结果是到达所有坐标的最大值。

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        
        l = len(nums)
        dp = [1] * l
        # dp[i] 跳到 第i个木桩 的最大步数
        for i in range(1, l):
            # 不能是负无穷,最短也会是1
            long = 1
            for j in range(i):
                if nums[i] > nums[j]:
                    long = max(long, dp[j] + 1)
            dp[i] = long
        
        return max(dp)

Follow up: Could you improve it to O(n log n) time complexity?

看到时间复杂度,则考虑排序 or 二分搜索法?

排序并没有卵用,排完序之后问题规模并没有缩小。

二分搜索法要求有序性,所以去构造一个有序的序列,又因为我们要求一个最长子序列,必须将序列的长度作为构造序列的一部分,那么是index还是值去表示长度?

如果用值表示长度,那么index只能是前index的个序列,就和上面的DP一样了。

所以用index表示子序列的长度,最后结果是返回构造列表的长度。

只剩下值的构造,可以考虑的有长度为index + 1的子序列的相关参数,比如个数,比如末尾数。再进行排除,个数对于解题没有帮助,所以只剩下末尾数。讲该列表称作是tail数组,将所有子序列的末尾的最小值放到相应的位置,那么每从nums得到一个新的值x,只需要判断和每位末尾数的大小关系就可以知道,以该x为末尾所得到的最长子序列的长度。

所以关键部分就是,每次取到一个新的值,找到他在tail数组中的位置(第一个比该值大的index),然后更新tail列表

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        
        l = len(nums)
    
        def binarysearch(nums, target):
            # 返回第一个大于target的坐标
            start = 0
            end = len(nums) - 1
            while start < end:
                mid = (start + end)//2
                if nums[mid] == target:
                    return mid
                elif nums[mid] < target:
                    start = mid + 1
                else:
                    end = mid
            
            if nums[start] >= target:
                return start
            if start == len(nums) - 1:
                return len(nums)


        tails = [nums[0], ]
        # tail[i] 表示 长度为 i + 1的最长子序列的   末尾的最小值
        for i in range(1, l):
            index = binarysearch(tails, nums[i])
            if index == len(tails):
                tails.append(nums[i])
            else:
                tails[index] = nums[i]
        
        return len(tails)

2)序列型

state: dp[i] 表示前i个

function: dp[i] = f[1 ... i-1 ]

init: dp[0]

answer: dp[n]

例子:

139. Word Break

Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, determine if s can be segmented into a space-separated sequence of one or more dictionary words.

Note: The same word in the dictionary may be reused multiple times in the segmentation.

可以利用的小技巧,word的平均长度是5.7,先求出worddict中最长的单词长度

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        dp = [False] * (len(s) + 1)
        # dp[i]表示 前i个(不包含i)dp[0] 前0个, 是空串
        dp[0] = True
        
        words = set()
        for word in wordDict:
            words.add(word)
        
        for i in range(len(s) + 1):
            for j in range(i)[::-1]:
                if dp[j] and s[j: i] in words:
                    dp[i] = True
                    break
        
        return dp[-1]

140. Word Break II

Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, add spaces in s to construct a sentence where each word is a valid dictionary word. Return all such possible sentences.

要求返回所有的结果,非典型DP,需要使用搜索算法,DFS去得到所有的结果。考察的是DFS + 记忆化搜索

132. Palindrome Partitioning II

Given a string s, partition s such that every substring of the partition is a palindrome.

Return the minimum cuts needed for a palindrome partitioning of s.

求最小,不可交换顺序,暴力罗列是指数级别,区间型的dp,考虑单序列dp

如果用def去判断回文串,那么时间复杂度是O(n^3), 用另外一个DP去做判断回文串。

class Solution:
    def minCut(self, s: str) -> int:
        # def ispalindrome(string):
        #     left = 0
        #     right = len(string) - 1
        #     while left < right:
        #         if string[left] == string[right]:
        #             left += 1
        #             right -= 1
        #         else:
        #             return False
        #     return True
        if not s:
            return 0
        
        l = len(s)
        ispalindrome = [[False for j in range(l)] for i in range(l)]
        
        for i in range(l):
            ispalindrome[i][i] = True  
        
        for i in range(l - 1):
            ispalindrome[i][i + 1] = (s[i] == s[i + 1])
            
        for length in range(2, l):
            for start in range(0, l - length):
                if ispalindrome[start + 1][start + length - 1] and s[start] == s[start + length]:    
                    ispalindrome[start][start + length] = True
                
        dp = [0] * (l + 1)
        for i in range(len(s) + 1):
            dp[i] = i - 1
            
        # dp[i] 表示 第一个字符到第i个字符的子串 最少的回文串个数 可以得到所有子串都是 回文串
        for j in range(1, l + 1):
            for i in range(j):
                if ispalindrome[i][j - 1]:
                    dp[j] = min(dp[j], dp[i] + 1)
                    
        return dp[-1]

3)双序型

state: dp[i][j] 表示第一个序列的前i个字符,和第二个序列前j个字符

function: 研究第一个序列 前i个 和 第二个序列前j个字符

initialize: dp[i][0] 和 dp[0][i]

answer: dp[n][m]

例子

72. Edit Distance

Given two words word1 and word2, find the minimum number of operations required to convert word1 to word2.

You have the following 3 operations permitted on a word:

  1. Insert a character
  2. Delete a character
  3. Replace a character

找最小,可以小状态求大状态,双序列问题,双序列DP

字符串问题,会有空串问题,所以dp初始化为 len(word1) + 1 * len(word2) + 1   

dp[i][j]表示word1的前i个字符 转化成word2 的前j个字符 的最少操作数。前0个就是空串,前1的就是第一个字符,以此类推。

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        if not word1 and not word2:
            return 0
        if not word1:
            return len(word2)
        if not word2:
            return len(word1)
        
        # 初始化,空串变到另一个字符串就是不断的insert。字符串变到空串就是不断的delete
        l1 = len(word1)
        l2 = len(word2)
        
        dp = [[0 for j in range(l2 + 1)] for i in range(l1 + 1)]
        
        for i in range(l1 + 1):
            dp[i][0] = i
        
        for j in range(l2 + 1):
            dp[0][j] = j
        
        
        # 构建dp表
        for i in range(1, l1 + 1):
            for j in range(1, l2 + 1):
                if word1[i - 1] == word2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1]
                else:
                    # 如果不等的话前i - 1到 前j的变化,会多出第i个字符,需要delete
                    # 前i到 前j - 1的变化,会少一个第j个字符,需要insert
                    # 前i - 1 到 j - 1的变化, 末尾不同,需要replace
                    dp[i][j] = min(dp[i - 1][j] + 1,
                                   dp[i][j - 1] + 1,
                                dp[i - 1][j - 1] + 1)
        
        return dp[-1][-1]

递归的做法:

比较直观的想法就是暴力的是尝试所有可能,每次会有三种做法insert,delete,replace,显然DFS搜索 + 记忆化 是最佳选择。

  1. base case: word1 = "" or word2 = "" => return length of other string
  2. recursive case: word1[0] == word2[0] => recurse on word1[1:] and word2[1:]
  3. recursive case: word1[0] != word2[0] => recurse by inserting, deleting, or replacing
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        def MinDistance(word1, word2, i, j, memo):
            """Memoized solution"""
            if i == len(word1) and j == len(word2):
                return 0
            if i == len(word1):
                return len(word2) - j
            if j == len(word2):
                return len(word1) - i

            if (i, j) not in memo:
                if word1[i] == word2[j]:
                    ans = MinDistance(word1, word2, i + 1, j + 1, memo)
                else: 
                    insert = 1 + MinDistance(word1, word2, i, j + 1, memo)
                    delete = 1 + MinDistance(word1, word2, i + 1, j, memo)
                    replace = 1 + MinDistance(word1, word2, i + 1, j + 1, memo)
                    ans = min(insert, delete, replace)
                memo[(i, j)] = ans
            return memo[(i, j)]
        
        memo = {}
        return MinDistance(word1, word2, 0, 0, memo)

115. Distinct Subsequences

Given a string S and a string T, count the number of distinct subsequences of S which equals T.

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        l1 = len(s)
        l2 = len(t)
        if l1 < l2 or not s:
            return 0
        
        dp = [[0 for j in range(l2 + 1)] for i in range(l1 + 1)]
        
        for i in range(l1 + 1):
            dp[i][0] = 1
            
        
        for i in range(1, l1 + 1):
            for j in range(1, i + 1):
                if j <= l2:
                    if s[i - 1] == t[j - 1]:
                        # 相等时,两边同时去掉该值 + 第一个string去掉该值包含0-j的字符
                        dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
                    else:
                        dp[i][j] = dp[i - 1][j]
        
        return dp[-1][-1]

优化: 因为每次只用到了上一层的值,所以用一维的DP保存即可

97. Interleaving String

Given s1s2s3, find whether s3 is formed by the interleaving of s1 and s2.

4)划分型

5)背包型

6)区间型

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值