方法:
典型特点是以坐标所在的意义作为状态。如一维和二维数组:dp=[m][n] or dp=[m]
与序列型动态规划的不同之处在于,坐标型动态规划dp[i]是以i下标结尾,也就是优化决策里到这一步必含这个元素,而序列型动态规划不是。
1、不同路径(无障碍)【leetcode62】
分析过程:
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)
#动态规划问题
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】
思路:
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) 额外空间来解决
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]表示以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个敌人
思路:
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)的某种性质。
套路:(序列+状态)
1、粉刷房子1(三种颜色)【lintcode515】
思路:
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】
思路:
空间优化:
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】
题意:只能买卖一次股票,求获利最大是多少。
思路:
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这样表示,防止越界。
思路:思考最后一步是第一次卖之后还是第二次卖之后还是没买过之后,不知道,因此把状态写入动态规划
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次交易
思路:
因为当前状态只与上一状态有关,因此可以用滚动数组来节省空间。
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]
思路:
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回文子串】
思路:
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个人需要的最少时间
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]
四、背包型动态规划
【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
思路:
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:
输入: [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里面。
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
思路:和上一题完全一模一样,数组中元素(零钱)可以重复使用
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]