Leetcode动态规划(python)

九章算法动态规划总结

动态规划分类:

  • 坐标型动态规划
  • 序列型动态规划
  • 划分型动态规划
  • 最长上升子序列
  • 背包型动态规划
  • 区间型动态规划
  • 综合型动态规划

思路:

  1. 定义状态(根据最后一步和子问题)
  2. 写出状态(根据最后一步和子问题)
  3. 初始化和界内处理
  4. 计算顺序、计算结果(判断是否可以用滑动数组节省空间)

一、坐标型动态规划(最简单的)

方法:

典型特点是以坐标所在的意义作为状态。如一维和二维数组:dp=[m][n] or dp=[m]

  • 给定输入为序列或者矩阵
  • 状态序列下标为下标i或者(i,j);以第i个元素结尾的性质或者以i,j结尾的路径的性质
  • 初始化设置为f[0]或f[0][0...n-1]
  • 二维空间优化:如果f[i][j]的值只依赖于当前行和前一行,则可以用滚动数组节省时间。

与序列型动态规划的不同之处在于,坐标型动态规划dp[i]是以i下标结尾,也就是优化决策里到这一步必含这个元素,而序列型动态规划不是。

 

1、不同路径(无障碍)【leetcode62】

分析过程:

  1. f[i][j]表示到达终点的路径的个数
  2. f[i][j] = f[i-1][j] + f[i][j](注意越界)
  3. f[0][0] = 1 ;越界处理即对f[i-1][j] + f[i][j]中ij的约束
  4. 计算顺序:从左到右,从上到下;计算结果:f[m-1][n-1]
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        if m == 0 or n == 0:
            return 0
        
        dp = [[0 for _ in range(n)] for _ in range(m)]
        #初始化放在for循环里了
        for i in range(0, m):#这个循环怎么走,应该根据状态转移方程来
            for j in range(0, n):
                if i == 0 or j == 0:
                    dp[i][j] = 1
                else:
                    dp[i][j] = dp[i-1][j] + dp[i][j-1]
        return dp[m-1][n-1]

2、不同路径(有障碍)【leetcode63】

分析过程和第1题类似,但是需要注意的是有路障的地方dp[i][j]= 0,分成五种情况考虑

坑:如果start就有路障,那么return 0(因此不能一开始就初始化dp[0][0] = 1)

  1. 如果start就有路障,那么return 0(因此不能一开始就初始化dp[0][0] = 1)
  2. 初始化dp[0][0] = 1
  3. 如果有路障,那么dp[i][j]=0 则continue
  4. f[i][j] = f[i-1][j] + f[i][j](注意越界)区分i==0和j==0
#动态规划问题
class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        m = len(obstacleGrid)
        n = len(obstacleGrid[0])
        nums = obstacleGrid
        if m == 0 or n == 0:
            return 0
        
        dp = [[0 for _ in range(n)] for _ in range(m)]

        #初始化放在for循环中
        for i in range(m):
            for j in range(n):
                if nums[i][j] == 1: #考虑障碍的位置,来分情况讨论(初始化)
                    continue
                if i == 0 and j == 0: #初始化
                    dp[i][j] = 1
                if i > 0:
                    dp[i][j] += dp[i-1][j]
                if j > 0:
                    dp[i][j] += dp[i][j-1]                
        return dp[m-1][n-1]

3、跳跃游戏【leetcode55】

思路:

  1. 最后一步可能能跳到i,也有可能调不到i,因此定义f(i)为能否跳到位置i
  2. 状态转移方程:f[i] = f[j] (f[j] is true and nums[j] >= i-j)!!!!!!!!这个我经常很糊涂,不知道从哪直接能跳到最后一步,因此要枚举!
  3. 初始化f[0]=0,无越界
  4. 计算顺序从左到右,返回f[size-1]
class Solution:
    def canJump(self, nums) -> bool:
        size = len(nums)
        if size == 0:
            return True

        dp = [False for _ in range(size)]

        dp[0] = True

        for i in range(1, size):
            for j in range(i):
                if nums[j] >= i-j and dp[j]:
                    dp[i] = True
                    break
        return dp[size-1]
s = Solution()
print(s.canJump([2,2,1,0,4]))

4、最长连续上升子序列【lintcode397】

题目:

给定一个整数数组(下标从 0 到 n-1, n 表示整个数组的规模),请找出该数组中的最长上升连续子序列。(最长上升连续子序列可以定义为从右到左或从左到右的序列。)

样例

样例 1:

输入:[5, 4, 2, 1, 3]
输出:4
解释:
给定 [5, 4, 2, 1, 3],其最长上升连续子序列(LICS)为 [5, 4, 2, 1],返回 4。

样例 2:

输入:[5, 1, 2, 3, 4]
输出:4
解释:
给定 [5, 1, 2, 3, 4],其最长上升连续子序列(LICS)为 [1, 2, 3, 4],返回 4。

挑战

使用 O(n) 时间和 O(1) 额外空间来解决

  1. f[i]表示以i为结尾的最大连续子序列的长度
  2. f[i] = f[i-1]+1 or 1
  3. f[0]=nums[0]
  4. 计算顺序从左到右,结果是max(f)
class Solution:
    """
    @param A: An array of Integer
    @return: an integer
    """
    def longestIncreasingContinuousSubsequence(self, A):
        size = len(A)
        nums = A
        if size == 0:
            return 0
        
        dp = [0 for _ in range(size)]
        
        dp[0] = 1
        for i in range(1, size):
            if nums[i] > nums[i-1]:
                dp[i] = dp[i-1] + 1
            else:
                dp[i] = 1
        
        max_value = max(dp)
        for i in range(1, size):
            if nums[i] < nums[i-1]:
                dp[i] = dp[i-1] + 1
            else:
                dp[i] = 1
        return max(max_value, max(dp))

优化空间:

滑动数组法节省空间:

old, now = 0, 0
for i in range(m):
    old = now
    now = now - 1
    #接下来就是old表示i-1,now表示i即可

代码 

class Solution:
    """
    @param A: An array of Integer
    @return: an integer
    """

    def longestIncreasingContinuousSubsequence(self, A):
        size = len(A)
        nums = A
        if size == 0:
            return 0

        dp = [0 for _ in range(2)]

        dp[0] = 1 #注意这个初始为1才对
        old, now = 0, 0
        max_value = dp[0]
        for i in range(1, size):
            old = now
            now = 1 - now
            if nums[i] > nums[i - 1]:
                dp[now] = dp[old] + 1
            else:
                dp[now] = 1
            max_value = max(max_value, dp[now])
        dp[0] = 1
        old, now = 0, 0
        for i in range(1, size):
            old = now
            now = 1 - now
            if nums[i] < nums[i - 1]:
                dp[now] = dp[old] + 1
            else:
                dp[now] = 1
            max_value = max(max_value, dp[now])

        return max_value

5、最长上升子序列【Leetcode300】

最后一步:以i为结尾的子序列的最大长度,那序列i前面的值是j,j不是i-1,因此要枚举j找出以i结尾最大的子序列的长度。

#状态表示的意义是:dp[i]表示以nums[i]为结尾的升序列长度,最后返回最大值
# class Solution:
#     def lengthOfLIS(self, nums: List[int]) -> int:
#         size = len(nums)
#         if size == 0:
#             return 0
        
#         dp = [0 for _ in range(size)]
#         #初始化
#         dp[0] = 1

#         for i in range(1, size):
#             dp[i] = 1
#             for j in range(i):
#                 if nums[i] > nums[j]:
#                     dp[i] = max(dp[i], dp[j] + 1)
#         return max(dp)

补充:如果打印出这个最长上升子序列

法一:

思路:定义一个数组pi,记录令当前dp[i]最大的那个j,也就是记录前一个数的索引,然后从后往前打印路径即可

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        #dp[i]表示以i为结尾的最长上升子序列的长度
        #dp[i] = max(dp[j]+1, 1)
        if len(nums) == 0:
            return 0
        dp = [0 for _ in range(len(nums))]

        # dp[0] = 1
        pi = [0 for _ in range(len(nums))] #记录下来上一个是什么
        res = 0
        p = 0
        for i in range(len(nums)):
            dp[i] = 1
            pi[i] = -1
            for j in range(i):
                if nums[j] < nums[i]:
                    dp[i] = max(dp[i], dp[j] + 1)
                    if dp[j] + 1 == dp[i]:
                        pi[i] = j
            res = max(res, dp[i]) #记录下来最大值res极其索引p
            if dp[i] == res: 
                p = i
        seq = [0 for _ in range(res)]
        i = res - 1
        while i >= 0:
            seq[i] = nums[p]
            p = pi[p]
            i -= 1
        print(seq)

法二: 

nums = [2,1,5,3,6,4,8,9,7]
n = len(nums)
dp = [1] * n
for i in range(1,n):
    for j in range(i):
        if nums[j] < nums[i]:
            dp[i] = max(dp[i],dp[j] + 1)
temp = max(dp)
res = []
for i in range(n - 1,-1,-1):
    if dp[i] == temp:
        res.append(str(nums[i]))
        temp -= 1
res.reverse()
print(res)

6、俄罗斯套娃【leetcode354】

和第5题一样,典型的最长上升子序列,唯一的不同是需要排序。

坑:

注意我要求j里面的最大值,那么dp[i]应该放在循环外面,才能真正求到最大值。

#这个题用排序算法无法通过
class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        envelopes.sort()
        # envelopes.sort(key=lambda x: x[0])
        size = len(envelopes)
        if size == 0:
            return 0
         
        dp = [0 for _ in range(size)]
        
        dp[0] = 1

        for i in range(1, size):
            dp[i] = 1
            for j in range(i):
                if envelopes[i][0] > envelopes[j][0] and envelopes[i][1] > envelopes[j][1]:
                    dp[i] = max(dp[i], dp[j] + 1)
        
        return max(dp)

7.【leetcode152】乘积最大子序列

思路:这题和连续上升子序列的题简直看起来一模一样,但是[-2, 3, -4]用之前的解题思路对付不了有负数的情况。那怎么办,考虑到dp[i]为以i为结尾的序列乘积最大,那么设置一个当前最大值和当前最小值,因为如果i-1元素时负值可能乘以当前最小值就是最大值。

  • dp[i] = max(max_value * nums[i], min_value * nums[i], nums[i])
  • 完全可以不用dp数组
#用动态规划做,表明dp[i]表示以i结尾的连续序列的最大值。以i结尾时此刻不是最小就是最大值,因此维护max_value,min_value。
class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if len(nums) == 0:
            return
        dp = [0 for _ in range(len(nums))]
        dp[0] = nums[0]
        max_value = nums[0]
        min_value = nums[0]
        for i in range(1, len(nums)):
            dp[i] = max(max_value * nums[i], min_value * nums[i], nums[i])
            min_value = min(max_value * nums[i], min_value * nums[i], nums[i])
            max_value = dp[i]
        # print(dp)
        return max(dp)
class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if len(nums) == 0:
            return
        res = nums[0]
        pre_max = nums[0]
        pre_min = nums[0]
        for i in range(1, len(nums)):
            cur_max = max(pre_max * nums[i], pre_min * nums[i], nums[i]) 
            cur_min = min(pre_max * nums[i], pre_min * nums[i], nums[i])
            res = max(res, cur_max)
            pre_max = cur_max
            pre_min = cur_min
        return res

 

7.【leetcode64】最小路径和 

空间优化

注意必须是从第一行开始的,才能套用那个滑动数组的模板。

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 = len(grid)
        n = len(grid[0])
        if m == 0:
            return 0
        
        dp = [[0 for _ in range(n)] for _ in range(2)]
        old, now = 0, 0
        dp[0][0] = grid[0][0]
        for i in range(1, n):
            dp[0][i] = dp[0][i-1] + grid[0][i]
            
        for i in range(1, m):
            old = now
            now = 1 - now
            for j in range(n):
                dp[now][j] = float('inf')
                if i > 0:
                    dp[now][j] = min(dp[now][j], dp[old][j]+grid[i][j])
                if j > 0:
                    dp[now][j] = min(dp[now][j], dp[now][j-1]+grid[i][j])
        print(dp)
        return dp[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 = len(grid)
        n = len(grid[0])
        if m == 0:
            return 0
        
        dp = [[0 for _ in range(n)] for _ in range(m)]
        # dp[0][0] = grid[0][0]
        for i in range(m):
            for j in range(n):
                if i == 0 and j == 0:
                    dp[i][j] = grid[i][j]
                    continue
                dp[i][j] = float('inf')
                if i > 0:
                    dp[i][j] = min(dp[i][j], dp[i-1][j]+grid[i][j])
                if j > 0:
                    dp[i][j] = min(dp[i][j], dp[i][j-1]+grid[i][j])
        return dp[m-1][n-1]

 6、炸弹袭击【leetcode553】

描述

给定一个二维矩阵, 每一个格子可能是一堵墙 W,或者 一个敌人 E 或者空 0 (数字 '0'), 返回你可以用一个炸弹杀死的最大敌人数. 炸弹会杀死所有在同一行和同一列没有墙阻隔的敌人。 由于墙比较坚固,所以墙不会被摧毁.

你只能在空的地方放置炸弹.

您在真实的面试中是否遇到过这个题?  是

题目纠错

样例

样例1

输入:
grid =[
     "0E00",
     "E0WE",
     "0E00"
]
输出: 3
解释:
把炸弹放在 (1,1) 能杀3个敌人

思路:

  1. 分成上下左右四个方向就容易了,这个题的难点就是没想到分成上下左右四个方向
  2. 注意在分方向的时候计算顺序也会改变 
class Solution:
    """
    @param grid: Given a 2D grid, each cell is either 'W', 'E' or '0'
    @return: an integer, the maximum enemies you can kill using one bomb
    """
    def maxKilledEnemies(self, grid):
        # write your code here
        m = len(grid)
        if m == 0:
            return 0
        n = len(grid[0])
        if n == 0:
            return 0
        
        
        up = [[0 for _ in range(n)] for _ in range(m)]
        for i in range(m):
            for j in range(n):
                if i == 0 and grid[i][j] == 'E':
                    up[i][j] = 1
                    continue
                if grid[i][j] == 'W':
                    up[i][j] = 0
                    continue
                if i > 0:
                    up[i][j] = up[i-1][j]
                    if grid[i][j] == 'E':
                        up[i][j] += 1
        down = [[0 for _ in range(n)] for _ in range(m)]
        for i in range(m-1, -1, -1):
            for j in range(n):
                if i == m-1 and grid[i][j] == 'E':
                    down[i][j] = 1
                    continue
                if grid[i][j] == 'W':
                    down[i][j] = 0
                    continue
                if i < m-1:
                    down[i][j] = down[i+1][j]
                    if grid[i][j] == 'E':
                        down[i][j] += 1
        left = [[0 for _ in range(n)] for _ in range(m)]
        for i in range(m):
            for j in range(n):
                if j == 0 and grid[i][j] == 'E':
                    left[i][j] = 1
                    continue
                if grid[i][j] == 'W':
                    left[i][j] = 0
                    continue
                if j > 0:
                    left[i][j] = left[i][j-1]
                    if grid[i][j] == 'E':
                        left[i][j] += 1
        right = [[0 for _ in range(n)] for _ in range(m)]
        for i in range(m):
            for j in range(n-1, -1, -1):
                if j == n-1 and grid[i][j] == 'E':
                    right[i][j] = 1
                    continue
                if grid[i][j] == 'W':
                    right[i][j] = 0
                    continue
                if j < n-1:
                    right[i][j] = right[i][j+1]
                    if grid[i][j] == 'E':
                        right[i][j] += 1
        max_value = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == '0':
                    max_value = max(max_value, up[i][j]+down[i][j]+right[i][j]+left[i][j])
        return max_value

 

位操作动态规划

和位操作有关的动态规划题一般用值作为状态。

此题难点在于状态转换方程 f[i] = f[i>>1] + i mod 2

#动态规划
# class Solution:
#     def countBits(self, num: int) -> List[int]:

#         dp = [0 for _ in range(num + 1)] #最后要返回这个数组
#         dp[0] = 0

#         for i in range(1, num + 1):
#             dp[i] = dp[i>>1] + i % 2
        
#         return dp

class Solution:
    def countBits(self, num: int) -> List[int]:

        dp = [0 for _ in range(num + 1)] #最后要返回这个数组
        dp[0] = 0

        for i in range(1, num + 1):
            dp[i] = dp[i>>1] + (i & 1)
        
        return dp

二、序列型动态规划

当思考动态规划最后一步时,这一步的选择依赖于前一步的某种状态

初始化f[0]表示前0天的性质

计算时,f[i]表示前i个元素(0...i-1)的某种性质。

套路:(序列+状态)

  • 给定一个序列
  • 序列型动态规划f[i]的下标表示前i个元素(A0, ... , Ai-1)具有某种性质,而坐标型动态规划f[i]表示以Ai结尾的某种性质
  • 序列型初始化时f[0]具有空序列的性质, 而坐标型动态规划f[0]表示以A0为结尾的某种性质

1、粉刷房子1(三种颜色)【lintcode515】

思路:

  1. 最后一步:由于题目说房子只能刷红蓝绿三种颜色,那么最后一个房子不能和上一个房子颜色一样,上一个房子可能是红色、蓝色或绿色(分情况讨论,找出最小的)。如果上一个房子是红色,那么最后一个房子只能刷绿色或者蓝色,求出来最小的那个就是上一个房子刷红色的花费值。由于fi和颜色有关,我们将颜色加入f[i][j],表示前i个房子(第i-1刷j色)的花费
  2. f[i][j] = min(k!=j)(f[i-1][k] + costs[i-1][j]) 开数组(dp[i][j] = [m+1][n])
  3. 初始化第一行为0
  4. 计算顺序从左到右从上到下,结果就是min(dp[m])
  5. 关键在于最后一个房子和上一个房子的颜色有关,因此把颜色也放在dp里面。
class Solution:
    """
    @param costs: n x 3 cost matrix
    @return: An integer, the minimum cost to paint all houses
    """
    def minCost(self, costs):
        # write your code here
        m = len(costs)
        if m == 0:
            return 0
        n = len(costs[0])
        if n == 0:
            return 0
        
        dp = [[0 for _ in range(n)] for _ in range(m+1)]
        
        for i in range(m+1):
            for j in range(n):
                if i == 0 :
                    dp[i][j] = 0
                    continue
                dp[i][j] = float('inf')
                for k in range(n):
                    if k != j:
                        dp[i][j] = min(dp[i][j], dp[i-1][k]+costs[i-1][j])
        return min(dp[m])

2、粉刷房子(有k种颜色)【lintcode516】

难点:将复杂度从O(mn2)减到O(mn)

方法:

先把dp[i-1][j]的最小值和次小值求出来,然后再计算dp[i][j]

[先计算前一行的最小值和次小值并记录最小值和次小值的索引,如果当前行的颜色和最小值的颜色相同,那么当前行这个颜色的代价最小即为次小值+cost,除此之外当前行的这个颜色的代价应该是最小值+cost

优化时间代码如下:【模板】

        for i in range(1, m+1):
            min_v = float('inf')
            sec_v = float('inf')
            min_i = 0
            sec_i = 0
            for j in range(n):
                if dp[i-1][j] < min_v:
                    sec_v = min_v
                    sec_i = min_i
                    min_v = dp[i-1][j]
                    min_i = j
                    continue
                if dp[i-1][j] < sec_v:
                    sec_v = dp[i-1][j]
                    sec_i = j

总体代码如下:

class Solution:
    """
    @param costs: n x k cost matrix
    @return: an integer, the minimum cost to paint all houses
    """
    def minCostII(self, costs):
        # write your code here
        m = len(costs)
        if m == 0:
            return 0
        n = len(costs[0])
        if n == 0:
            return 0
        
        dp = [[0 for _ in range(n)] for _ in range(m+1)]
        for i in range(n):
            dp[0][i] = 0
        
        for i in range(1, m+1):
            min_v = float('inf')
            sec_v = float('inf')
            min_i = 0
            sec_i = 0
            #先计算dp[i-1]的最小值和次小值
            for j in range(n):
                if dp[i-1][j] < min_v:
                    sec_v = min_v
                    sec_i = min_i
                    min_v = dp[i-1][j]
                    min_i = j
                    continue
                if dp[i-1][j] < sec_v:
                    sec_v = dp[i-1][j]
                    sec_i = j
            #再计算dp[i][j]
            for j in range(n):
                if j != min_i:
                    dp[i][j] = min_v + costs[i-1][j]
                if j == min_i:
                    dp[i][j] = sec_v + costs[i-1][j]
        
        return min(dp[m])

3.打家劫舍1【leetcode198】

思路:

  1. 最后一步是偷房子i-1还是不偷呢,如果偷房子i,那么和它相邻的前一个房子一定不能偷,因此用f[i]表示前i个房子最大偷钱值。
  2. f[i] = max(f[i-1], f[i-2]+A[i-1]
  3. 初始化f[0],f[1]
  4. 计算顺序从左到右,结果是f[size]

空间优化:

        old, now = 0, 0
        
        
        for i in range(2, len(nums1)+1):
            old = now
            now = 1 - now
            dp1[old] = max(dp1[now], dp1[old]+nums1[i-1])

上述代码最后一句,注意old是当前还是now是当前即可

代码:空间优化

#动态规划时间复杂度O(n),空间复杂度O(n)
class Solution:
    """
    @param A: An array of non-negative integers
    @return: The maximum amount of money you can rob tonight
    """
    def houseRobber(self, A):
        # write your code here
        size = len(A)
        if size == 0:
            return 0
        
        dp = [0 for _ in range(2)]
        
        dp[0] = 0
        dp[1] = A[0]
        old, now = 0, 1
        
        for i in range(2, size+1):
            old = now
            now = 1 - now
            dp[now] = max(dp[now]+A[i-1], dp[old])
            
        
        return dp[now]
#时间复杂度O(n),空间复杂度O(1)

 没有空间优化:

#动态规划时间复杂度O(n),空间复杂度O(n)
# class Solution:
#     def rob(self, nums: List[int]) -> int:
#         size = len(nums)
#         if size == 0:
#             return 0
        
#         dp = [0 for _ in range(size+1)]

#         dp[0] = 0

#         for i in range(1, size+1):
#             if i == 1:
#                 dp[i] = nums[i-1]
#             else:
#                 dp[i] = max(dp[i-1], dp[i-2]+nums[i-1])
#         # print(dp)

#         return dp[-1]

4.打家劫舍2【leetcode213】

思路:这个房子围成一圈了,因此我们把这一圈拆成两个序列,一个含首不含尾,一个含尾不含首。拆的时候注意如果[1]一个元素,会发生越界。其余思路同打家劫舍1

class Solution:
    """
    @param nums: An array of non-negative integers.
    @return: The maximum amount of money you can rob tonight
    """
    def houseRobber2(self, nums):
        # write your code here
        size = len(nums)
        nums1 = nums[0:size-1]
        if size == 0:
            return 0
        if size == 1:
            return nums[0]
        
        dp1 = [0 for _ in range(2)]
        
        dp1[0] = 0
        dp1[1] = nums1[0]
        old, now = 0, 0
        
        
        for i in range(2, len(nums1)+1):
            old = now
            now = 1 - now
            dp1[old] = max(dp1[now], dp1[old]+nums1[i-1])
        max_value = dp1[old]
        
        
        nums2 = nums[1:size]
        
        dp2 = [0 for _ in range(2)]
        
        dp2[0] = 0
        dp2[1] = nums2[0]
        old, now = 0, 0
        
        for i in range(2, len(nums2)+1):
            old = now
            now = 1 - now
            dp2[old] = max(dp2[now], dp2[old]+nums2[i-1])
        max_value = max(max_value, dp2[old])  
        
        return max_value
            
            

无空间优化:

# class Solution:
#     def rob(self, nums) -> int:
#         size = len(nums)
#         if size == 0:
#             return 0
#         if size == 1:
#             return nums[0]

#         dp = [0 for i in range(size)] 

#         dp[0] = 0
#         nums1 = nums[:size-1]
#         for i in range(1, size):
#             if i == 1:
#                 dp[i] = nums1[0]
#             else:
#                 dp[i] = max(dp[i - 1], dp[i - 2] + nums1[i - 1])
#         max_value = dp[-1]
#         nums2 = nums[1:]
#         for i in range(1, size):
#             if i == 1:
#                 dp[i] = nums2[0]
#             else:
#                 dp[i] = max(dp[i - 1], dp[i - 2] + nums2[i - 1])

#         max_value = max(max_value, dp[-1])
#         return max_value

打家劫舍 三:递归问题

题意:盗贼只能从根节点(二叉树)进入,并且不能连续偷两个挨着的房屋。

思路:

思路1:用递归的思路,分成有顶点和无顶点两种情况,分别递归,求最大,但是会导致超时。

思路2:也是递归的思想,但是返回值不只是当前偷和不偷的最大值,还有不包括当前顶点的值(不偷)的值,等待下一次偷。减少了重复。

思路1代码:

class Solution:
    def rob(self, root: TreeNode) -> int:
        #这道题用递归来做,分两次递归,有顶点和无顶点
        # res = []
        def dfs(root):
            if root is None:
                return 0
            #有顶点
            res1 = root.val
            if root.left:
                res1 += dfs(root.left.left) + dfs(root.left.right)
            if root.right:
                res1 += dfs(root.right.left) + dfs(root.right.right)
            #无顶点
            res2 = dfs(root.left) + dfs(root.right)
            return max(res1, res2)
        return dfs(root)

思路2代码:

# 这道题在递归返回二个值, 一个值是偷这家或者不偷这家最大值,一个值不偷的(为了使偷下一家准备的)       
class Solution:
    def rob(self, root: TreeNode) -> int:

        def helper(root): 
            if not root:
                return 0, 0
            left, prev1 = helper(root.left)
            right, prev2 = helper(root.right)
            return max(prev1 + prev2 + root.val,  left + right), left + right
        
        return helper(root)[0]

 

5.股票问题1【leetcode121】

题意:只能买卖一次股票,求获利最大是多少。

思路:

  1. 最后一步:如果最后一步之前已经卖了,那么f[i]=f[i-1],如果最后一步卖f[i]=price[i-1]-min_val f[i]表示前i天获利
  2. 转换方程就是上述最大
  3. dp[0]= 0
  4. 计算顺序从左到右,计算结果最后一天。
class Solution:
    """
    @param prices: Given an integer array
    @return: Maximum profit
    """
    def maxProfit(self, prices):
        # write your code here
        size = len(prices)
        if size == 0:
            return 0
        dp = [0 for _ in range(size+1)]
        
        dp[0] = 0
        min_value = float('inf')
        for i in range(1, size+1):
            dp[i] = dp[i-1]
            min_value = min(min_value, prices[i-1])
            if min_value < prices[i-1]:
                dp[i] = max(dp[i], prices[i-1]-min_value)
            
        
        return dp[-1]

空间优化:滚动数组

class Solution:
    """
    @param prices: Given an integer array
    @return: Maximum profit
    """
    def maxProfit(self, prices):
        # write your code here
        size = len(prices)
        if size == 0:
            return 0
        dp = [0 for _ in range(size+1)]
        
        dp[0] = 0
        min_value = float('inf')
        old, now = 0, 0
        for i in range(1, size+1):
            old = now
            now = 1 - now
            dp[now] = dp[old]
            min_value = min(min_value, prices[i-1])
            if min_value < prices[i-1]:
                dp[now] = max(dp[now], prices[i-1]-min_value)
            
        
        return dp[now]

股票问题2【leetcode122】

题意:可以无数次交易

典型的贪心问题:只要当前比之前大就卖出去

class Solution:
    """
    @param prices: Given an integer array
    @return: Maximum profit
    """
    def maxProfit(self, prices):
        # write your code here
        res = 0
        size = len(prices)
        if size == 0:
            return res
        
        for i in range(1, size):
            if prices[i] > prices[i-1]:
                res += prices[i] - prices[i-1]
        
        return res

用动态规划来做:

# class Solution:
#     def maxProfit(self, prices: List[int]) -> int:
#         size = len(prices)
#         if size == 0:
#             return 0
        
#         dp = [0 for _ in range(size+1)]
#         dp[0] = 0
#         dp[1] = 0
#         min_value = float('inf')
#         for i in range(2, size+1):
#             min_value = min(min_value, prices[i-2])
#             if prices[i-1] >= min_value:
#                 dp[i] = prices[i-1] - min_value
#                 min_value = prices[i-1]
#         return sum(dp)

 

股票问题3【leetcode123】

题意:只能买卖两次

如果序列+状态才能解决问题,如果有五个状态最好0, 1, 2, 3, 4这样表示,防止越界。

思路:思考最后一步是第一次卖之后还是第二次卖之后还是没买过之后,不知道,因此把状态写入动态规划

  1. f[i][j]表示前i天处在j状态的获利情况。j分成五个状态,分别为0:第一次买之前;1:第一次买之后(持股);2:第一次卖之后第一次买之前;3:第二次买之后(持股);4:第二次卖之后。最后一步只能是处于0, 2,4状态,因此答案只能是在这三个状态下求最大值即max(f[size][0],f[size][2],f[size][4])
  2. 状态转移方程对于状态0, 2, 4来说f[i][j] = max(f[i-1][j], f[i-1][j-1]+prices[i-1]-prices[i-2](买的当天不赚钱,持股或者卖那天才会赚钱)状态转移:可以保持当前状态,可以在前一状态转到这个状态,因此有两种情况;对于1,3来说,本身是持股,但是在动态规划中也是需要状态转移的,要不是一直是持股状态,要不是前一天是没股状态,因此f[i][j]=max(f[i-1][j]+prices[i-1][i-2], f[i-1][j-1])
  3. 初始化dp[0][0,4]=0,注意j的出界处理。
  4. 计算顺序从左到右,从上到下,结果为max(f[size][0],f[size][2],f[size][4])
class Solution:
    """
    @param prices: Given an integer array
    @return: Maximum profit
    """
    def maxProfit(self, prices):
        # write your code here
        size = len(prices)
        if size == 0:
            return 0
        
        dp = [[0 for _ in range(5)] for _ in range(size+1)]
        
        #初始化都是第一行第一列都是0
        for i in range(1, size+1):
            for j in range(5):
                if j == 0 or j == 2 or j == 4:
                    dp[i][j] = dp[i-1][j]
                    if i > 1 and j > 0:#注意j>1这个边界条件
                        dp[i][j] = max(dp[i][j], dp[i-1][j-1]+prices[i-1]-prices[i-2])
                if j == 1 or j == 3:
                    dp[i][j] = dp[i-1][j-1]
                    if i > 1:
                        dp[i][j] = max(dp[i][j], dp[i-1][j] + prices[i-1]-prices[i-2])
        # print(dp)
        return max(dp[size][0], dp[size][2], dp[size][4])

空间优化:

class Solution:
    """
    @param prices: Given an integer array
    @return: Maximum profit
    """
    def maxProfit(self, prices):
        # write your code here
        size = len(prices)
        if size == 0:
            return 0
        
        dp = [[0 for _ in range(5)] for _ in range(size+1)]
        old, now = 0, 0
        #初始化都是第一行第一列都是0
        for i in range(1, size+1):
            old = now
            now = 1 - now
            for j in range(5):
                if j == 0 or j == 2 or j == 4:
                    dp[now][j] = dp[old][j]
                    if i > 1 and j > 0:#注意j>1这个边界条件
                        dp[now][j] = max(dp[now][j], dp[old][j-1]+prices[i-1]-prices[i-2])
                if j == 1 or j == 3:
                    dp[now][j] = dp[old][j-1]
                    if i > 1:
                        dp[now][j] = max(dp[now][j], dp[old][j] + prices[i-1]-prices[i-2])
        # print(dp)
        return max(dp[now][0], dp[now][2], dp[now][4])

股票问题4【leetcode188】

题意:规定只能进行k次交易

思路:

  1. 如果K>=N/2,那么就是相当于进行了无初次股票交易,即贪心问题
  2. 如果K < N/2,那么相当于股票问题3,分成2k+1个状态。偶数状态为卖出状态,奇数为持股状态。

因为当前状态只与上一状态有关,因此可以用滚动数组来节省空间。

class Solution:
    """
    @param K: An integer
    @param prices: An integer array
    @return: Maximum profit
    """
    def maxProfit(self, K, prices):
        # write your code here
        size = len(prices)
        if size == 0:
            return 0
        if K >= size:
            res = 0
            for i in range(1, size):
                if prices[i] > prices[i-1]:
                    res += prices[i] - prices[i-1]
            return res
        dp = [[0 for _ in range(2*K+1)] for _ in range(size+1)]
        
        for i in range(2*K+1):
            dp[0][i] = 0
        
        for i in range(1, size+1):
            for j in range(2*K+1):
                if j % 2 == 0:
                    dp[i][j] = dp[i-1][j]
                    if i > 1 and j > 0:
                        dp[i][j] = max(dp[i][j], dp[i-1][j-1]+prices[i-1]-prices[i-2])
                if j % 2 == 1:
                    dp[i][j] = dp[i-1][j-1] #不越界因为j一直大于等于1
                    if i > 1:
                        dp[i][j] = max(dp[i][j], dp[i-1][j]+prices[i-1]-prices[i-2])
        max_value = 0
        for i in range(2*K+1):
            if i % 2 == 0:
                max_value = max(dp[size][i], max_value)
        return max_value

 如果分成五个状态【0 1 2 3 4 5】要注意状态的有效性,得从1状态开始,0是无效的一定要记住。因此这个界要处理好j-1>=1

#滑动数组优化记住模板
class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        size = len(prices)
        if size == 0:
            return 0
        if k > size // 2: #注意这个举措是如果k很大,相当于prices不限制购买次数。
            dp = 0
            # min_value = float('inf')
            for i in range(1, size):
                if prices[i] > prices[i-1]:
                    dp += prices[i] - prices[i-1]
            return dp

        dp = [[0 for _ in range(2*k+1+1)] for _ in range(2)]
        old, now = 0, 0 #节省空间还不用初始化
        
        for i in range(1, size+1):
            old = now
            now = 1 - now
            for j in range(1, 2*k+1+1):
                if j % 2 == 1:
                    dp[now][j] = dp[old][j]
                    if i > 1 and j > 1:#注意这个边界条件,令j-1>=1,并且i-2>=0 是因为其余的状态都是无效的。
                        dp[now][j] = max(dp[now][j], dp[old][j-1] + prices[i-1]-prices[i-2])
                if j % 2 == 0:
                    if j > 1:#注意边界条件
                        dp[now][j] = dp[old][j-1]
                        if i > 1:
                            dp[now][j] = max(dp[now][j], dp[old][j] + prices[i-1]-prices[i-2])
        max_value = 0
        for j in range(1, 2*k+1+1):
            if j % 2 == 1:
                max_value = max(max_value, dp[now][j])
    
        return max_value

三、划分型动态规划

坑:要注意枚举的边界

如果最后一段可以是0那么j就可以枚举到i

1、完全平方数[leetcode279]

思路:

  1. 根据题意是把i分成最小的平方数断,因此属于划分型动态规划问题。最后一步就看最后一段,最后一段是完全平方数j2,因为不知道最后一段是哪个数,因此要枚举j求最大值。f[i]表示完全平方数组成i的最小值。
  2. f[i] = f[i-j2] + 1 (0<=j2<=i) [注意什么时候取到i要根据题意]
  3. 初始化f[0] = 0
  4. 计算顺序从左到右 结果为f[n]
class Solution:
    """
    @param n: a positive integer
    @return: An integer
    """
    def numSquares(self, n):
        # write your code here
        dp = [0 for _ in range(n+1)]
        dp[0] = 0
        
        for i in range(1, n+1):
            j = 1
            dp[i] = float('inf')
            while j*j <= i:
                dp[i] = min(dp[i], dp[i-j*j] + 1)
                j += 1
        
        return dp[-1]

2、分割回文串2【leetcode132】关于回文串还有【5最长回文子串】【647回文子串】

思路:

  1. 分割回文串一看就是一个划分型动态规划问题。那么最后一步就是最后一段,f[i]表示前i个字符划分成几个回文串。由于最后一段左边界不知道何处划分,因此要枚举,现在我就要问了那枚举范围是什么啊。0<=j
  2. f[i] = f[j] + 1(并且f[j]得是回文串)
  3. 技巧就是先得到一个回文串判断矩阵(中心扩散法)有2n-1个中心点。【奇偶互为补充】
  4. 计算顺序从左到右,计算结果为f[size]-1
class Solution:
    def minCut(self, s: str) -> int:
        size = len(s)
        if size == 0:
            return 0
        def isPalin(s): #先用动态规划算出是不是回文串表
            size = len(s)
            dp = [[False for _ in range(size)] for _ in range(size)]
            for j in range(size):
                i = j
                while i >= 0 and j < size and s[i] == s[j]:
                    dp[i][j] = True
                    i -= 1
                    j += 1
            for j in range(1, size):
                i = j - 1
                while i >= 0 and j < size and s[i] == s[j]:
                    dp[i][j] = True
                    i -= 1
                    j += 1

            return dp
        dp1 = isPalin(s)

        dp2 = [0 for _ in range(size+1)]
        dp2[0] = 0

        for j in range(1, size+1):
            min_value = float('inf')
            for i in range(j):
                if dp1[i][j-1]:#注意要和isPalin得出的矩阵对应
                    min_value = min(min_value, dp2[i] + 1)
            dp2[j] = min_value

        return dp2[size]-1

3、书籍复印【lintcode437】

题意:由于每个人只能连续抄写书,因此是一个分段问题。规定了段数为k,因此k个人同时抄写。状态为f[k][i],表示前i本书用前k个人需要的最少时间

  1. 状态为f[k][i],表示前i本书用前k个人需要的最少时间,最后一段表示最后一个人完成了几本书【枚举】
  2. 转移方程f[k][i] = min(f[k][i], max(f[k-1][j],A[j...i-1]) )
  3. 初始化第0行第0列。边界情况在枚举的时候要注意,可以枚举到i因为最后一段可以是0,所以0<=j<=i
  4. 技巧:枚举j的时候从后往前枚举,这样就不用每次重新计算A[j...i-1]了。
class Solution:
    """
    @param pages: an array of integers
    @param k: An integer
    @return: an integer
    """
    def copyBooks(self, pages, k):
        # write your code here
        size = len(pages)
        if size == 0:
            return 0
        if k >= size:
            k = size
        
        dp = [[0 for _ in range(size)] for _ in range(k+1)]
        dp[0][0] = 0
        for i in range(1, size+1):
            dp[0][i] = float('inf')
        
        for kc in range(1, k+1):#k个人
            for i in range(1, size+1):#i本书
                dp[kc][i] = float('inf')
                s = 0
                j = i
                while j >= 0: #j可以从0取到i
                    dp[kc][i] = min(dp[kc][i], max(dp[kc-1][j], s))
                    if j > 0:#为了防止j-1出界
                        s += pages[j-1]
                    j -= 1
                
        # print(dp)
        return dp[k][size]
                    
                

四、背包型动态规划

  1. 背包问题的最后一步就是数组不包括nums[i - 1]和包括nums[i - 1]两种方案。状态转移方程都是这么来的。
  2. 背包型动态规划一个典型的特点就是,将重量target写入dp表中!具体的状态直接根据题目的优化目标来设定。
  3. 大体分为两类:一类是nums的元素可以使用无数次,而且(1,2,1)和(1, 1, 2)不同时,此时nums的元素就不用写入dp了;如果可以使用无数次但是(1,2,1)和(1, 1, 2)相同时,也要把nums写入dp,dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];如果nums里的元素只能使用一次,此时状态要把nums写进去并且为dp[i][j] = dp[i - 1][j] + dp[i-1][j - coins[i - 1]]
  4. 现在我掌握的0-1背包问题 and 完全背包问题

【lintcode92】【0-1背包问题】

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

样例

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

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

思路:

  1. 典型的0-1背包问题
  2. 一个物品只能用一次;因此将物品写入dp;
  3. dp[i][j]表示前i个物品能否拼成j
  4. 状态转移方程:dp[i][j] = dp[i - 1][j] or (j - nums[i - 1] >= 0 and dp[i - 1][j - nums[i - 1]])
  5. 初始条件:dp[0][0] = True
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
        #物品只能挑一次,设置dp[i][j]为前i个物品能否拼成重量i
        if m == 0:
            return 0
        #状态转移方程为dp[i][j] = dp[i-1][j] or dp[i-1][j-A[i]
        dp = [[False for _ in range(m + 1)] for _ in range(len(A) + 1)]
        for i in range(len(A) + 1):
            for j in range(m + 1):
                if i == 0:
                    if j == 0:
                        dp[i][j] = True
                        continue
                    continue
                #i>0
                if j == 0:
                    dp[i][j] = True
                #i > 0 
                dp[i][j] = dp[i - 1][j] or (j - A[i - 1] >= 0 and dp[i - 1][j - A[i - 1]])
        # print(dp)
        i = m
        while i >= 0:
            if dp[len(A)][i]:
                return i
            i -= 1
        return i

【leetcode416】【416. 分割等和子集】【0-1背包问题】

给定一个只包含正整数非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:

  1. 每个数组中的元素不会超过 100
  2. 数组的大小不会超过 200

示例 1:

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].

示例 2:

输入: [1, 2, 3, 5]

输出: false

解释: 数组不能分割成两个元素和相等的子集.

思路:如果数组和是奇数一定分不成两个相同的子集;如果数组和为偶数,这道题问能否分成两个相等的子集,转化成能否找到 元素和为数组总和的1/2,即0-1背包问题

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        #转化成这个列表中是否能找到元素和为sum/2——》即0-1背包问题
        #发现背包问题,如果元素只能使用一次,那么就把元素写入表格中
        #dp[i][j]表示前i个元素能否拼成j
        sum1 = sum(nums)
        if sum1 & 1 == 1:
            return False
        target = sum1 // 2
        dp = [[False for _ in range(target + 1)] for _ in range(len(nums) + 1)]

        for i in range(len(nums) + 1):
            for j in range(target + 1):
                if i == 0:
                    if j == 0:
                        dp[i][j] = True
                    continue
                if j == 0:
                    dp[i][j] = True
                    continue
                # i > 0 and j > 0
                dp[i][j] = dp[i - 1][j] or (j - nums[i - 1] >= 0 and dp[i - 1][j - nums[i - 1]])
        if dp[len(nums)][target]:
            return True
        return False

滚动数组优化: 

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        #转化成这个列表中是否能找到元素和为sum/2——》即0-1背包问题
        #发现背包问题,如果元素只能使用一次,那么就把元素写入表格中
        #dp[i][j]表示前i个元素能否拼成j
        sum1 = sum(nums)
        if sum1 & 1 == 1:
            return False
        target = sum1 >> 1
        dp = [[False for _ in range(target + 1)] for _ in range(2)]
        old, now = 0, 0
        dp[0][0] = True
        for i in range(1, len(nums) + 1):
            old = now
            now = 1 - now
            for j in range(target + 1):
                # i > 0 and j > 0
                dp[now][j] = dp[old][j] or (j - nums[i - 1] >= 0 and dp[old][j - nums[i - 1]])
        if dp[now][target]:
            return True
        return False

3、【lintcode】完全背包问题【nums里面的元素只能用一次】

给出 n 个物品, 以及一个数组, nums[i] 代表第i个物品的大小, 保证大小均为正数, 正整数 target 表示背包的大小, 找到能填满背包的方案数。
每一个物品只能使用一次

样例

给出候选物品集合 [1,2,3,3,7] 以及 target 7

结果的集合为:
[7]
[1,3,3]

返回 2

思路:首先nums里面的元素只能使用一次,那么将nums写入状态,f[i][j]表示前i个元素拼成j的方案数,最后一步是有没有nums[i-1]参与,1、没参与:前i-1拼成j的方案2.参与:前i-1拼成j - nums[i - 1]的方案数。因此状态转移方程就是这两个方案相加。

class Solution:
    """
    @param nums: an integer array and all positive numbers
    @param target: An integer
    @return: An integer
    """
    def backPackV(self, nums, target):
        # write your code here
        # dp = [[0 for _ in range(target + 1)] for _ in range(len(nums) + 1)]
        
        # for i in range(len(nums) + 1):
        #     for j in range(target + 1):
        #         if i == 0 and j == 0:
        #             dp[i][j] = 1
        #             continue
        #         dp[i][j] = dp[i - 1][j]
        #         if j - nums[i - 1] >= 0:
        #             dp[i][j] += dp[i - 1][j - nums[i - 1]]
        
        # return dp[len(nums)][target]
        dp = [[0 for _ in range(target + 1)] for _ in range(2)]
        old, now = 0, 0
        dp[0][0] = 1
        for i in range(1, len(nums) + 1):
            old = now
            now = 1 - now
            for j in range(target + 1):
                dp[now][j] = dp[old][j]
                if j - nums[i - 1] >= 0:
                    dp[now][j] += dp[old][j - nums[i - 1]]
        
        return dp[now][target]

4、完全背包问题

leetcode494目标和

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例 1:

输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释: 

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。

思路:相当于有两个数组,P,N,P - N = Target,左右同加 P + N,则为 2P = Target + sum,如果2P为奇数直接返回0, 如果2P为偶数,找和为P的数组个数,从而转化为完全背包问题。

class Solution:
    def findTargetSumWays(self, nums: List[int], S: int) -> int:
        s = sum(nums)
        if S > s:
            return 0
        p_val = (S + s)
        if p_val & 1 == 1:
            return 0
        target = p_val >> 1
        # print(target)
        #转化成了完全背包问题
        dp = [[0 for _ in range(target + 1)] for _ in range(1 + 1)]
        # dp[0] = 1
        old, now = 0, 0
        dp[0][0] = 1
        for i in range(len(nums) + 1):
            old = now
            now = 1 - now
            for j in range(target + 1):
                # if i == 0 and j == 0:
                #     dp[i][j] = 1
                #     continue
                dp[now][j] = dp[old][j]
                if j >= nums[i - 1]:
                    dp[now][j] += dp[old][j - nums[i - 1]]
        print(dp)
        return dp[old][target]

5、【lintcode564 组合总和IV】【完全背包问题】

给出一个都是正整数的数组 nums,其中没有重复的数。从中找出所有的和为 target的组合个数。

样例1

输入: nums = [1, 2, 4] 和 target = 4
输出: 6
解释:
可能的所有组合有:
[1, 1, 1, 1]
[1, 1, 2]
[1, 2, 1]
[2, 1, 1]
[2, 2]
[4]

样例2

输入: nums = [1, 2] 和 target = 4
输出: 5
解释:
可能的所有组合有:
[1, 1, 1, 1]
[1, 1, 2]
[1, 2, 1]
[2, 1, 1]
[2, 2]

注意事项

一个数可以在组合中出现多次。
数的顺序不同则会被认为是不同的组合。

思路:捕捉到题目中的关键字,数组中元素可以重复使用;数的顺序不同则会被认为是不同的组合。因此和上面的背包问题不同的是状态定义时不用把元素放入dp里面。

  1. dp[i]表示可以拼成i的组合个数
  2. dp[i] = dp[i - nums[0]] + dp[i - nums[1]] +... + dp[i - nums[k]]
  3. 初始条件dp[0] = 1
  4. 计算顺序从左到右
class Solution:
    """
    @param nums: an integer array and all positive numbers, no duplicates
    @param target: An integer
    @return: An integer
    """
    def backPackVI(self, nums, target):
        # write your code here
        #既然nums里的数可以重复使用,那么和零钱问题一样,考虑最后一个数是谁
        #dp[i]表示可以拼成i的组合数量,dp[i] = dp[i - nums[0] + dp[i - nums[1] + dp[i - nums[2]
        if target == 0:
            return 1
        if len(nums) == 0:
            return 0
        
        dp = [0 for _ in range(target + 1)]
        
        dp[0] = 1
        
        for i in range(1, target + 1):
            for j in range(len(nums)):
                if i - nums[j] >= 0:
                    dp[i] += dp[i - nums[j]]
        return dp[target]

6、【Leetcode322. 零钱兑换】【完全背包问题】

题目:

给定不同面额的硬币 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] = min( dp[i - coins[k]] + 1)
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        if amount == 0:
            return 0
        if len(coins) == 0:
            return -1
        #最后一步,选哪个硬币 dp[i]为拼成i元所需最小的硬币数,由于硬币可以无限次取,所以考虑最后一步选哪个硬币
        #dp[i] = min(dp[i-coins[j]] + 1)状态转移方程为
        dp = [0 for _ in range(amount + 1)]
        for i in range(1, amount + 1):
            dp[i] = float('inf')
            for j in range(len(coins)):
                if i - coins[j] >= 0:
                    dp[i] = min(dp[i], dp[i - coins[j]] + 1)
        # print(dp)
        return dp[amount] if dp[amount] != float('inf') else -1

7、零钱兑换2【leetcode518】【很细微的区别,需要记住啊!!!虽然我没证明为啥是这样】

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

示例 1:

输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

示例 2:

输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。

示例 3:

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

思路:这题和上一题看起来很相似,仔细一看coins可以使用无数次没错,但是数的顺序不同不能认为是不同的组合,因此状态方程就不能和上一题一样这样定了。

这道题是之前那道 Coin Change 的拓展,那道题问我们最少能用多少个硬币组成给定的钱数,而这道题问的是组成给定钱数总共有多少种不同的方法。还是要使用 DP 来做,首先来考虑最简单的情况,如果只有一个硬币的话,那么给定钱数的组成方式就最多有1种,就看此钱数能否整除该硬币值。当有两个硬币的话,组成某个钱数的方式就可能有多种,比如可能由每种硬币单独来组成,或者是两种硬币同时来组成,怎么量化呢?比如我们有两个硬币 [1,2],钱数为5,那么钱数的5的组成方法是可以看作两部分组成,一种是由硬币1单独组成,那么仅有一种情况 (1+1+1+1+1);另一种是由1和2共同组成,说明组成方法中至少需要有一个2,所以此时先取出一个硬币2,然后只要拼出钱数为3即可,这个3还是可以用硬币1和2来拼,所以就相当于求由硬币 [1,2] 组成的钱数为3的总方法。是不是不太好理解,多想想。这里需要一个二维的 dp 数组,其中 dp[i][j] 表示用前i个硬币组成钱数为j的不同组合方法,怎么算才不会重复,也不会漏掉呢?我们采用的方法是一个硬币一个硬币的增加,每增加一个硬币,都从1遍历到 amount,对于遍历到的当前钱数j,组成方法就是不加上当前硬币的拼法 dp[i-1][j],还要加上,去掉当前硬币值的钱数的组成方法,当然钱数j要大于当前硬币值,状态转移方程也在上面的分析中得到了:

dp[i][j] = dp[i - 1][j] + (j >= coins[i - 1] ? dp[i][j - coins[i - 1]] : 0) 

class Solution:
    def change(self, amount: int, coins) -> int:
        if amount == 0:
            return 1
        size = len(coins)
        if size == 0:
            return 0

        dp = [[0 for _ in range(amount + 1)] for _ in range(size + 1)]
        dp[0][0] = 1
        for i in range(1, size + 1):
            dp[i][0] = 1
            for j in range(1, amount + 1):
                dp[i][j] = dp[i - 1][j]
                if j >= coins[i - 1]:
                    dp[i][j] += dp[i][j - coins[i - 1]]
                
        # print(dp)
        return dp[size][amount]

 

 

 

 

你可能感兴趣的:(Leetcode动态规划(python))