动态规划经典题目python_python动态规划刷题记录

先言:所有权利归花花酱所有,部分题解参考leetcode官方题解,,本人仅通过此篇博客记录,在此表示感谢!

一、O(n), S = O(n), T = O(n)

模板如下:爬楼梯 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution(object):

def climbStairs(self, n):

""":type n: int:rtype: int"""

if n < 0:

return 0

dp = [0] * max((n+1), 3)

dp[0] = 0

dp[1] = 1

dp[2] = 2

for i in range(3, n+1):

dp[i] = dp[i-1] + dp[i-2]

return dp[n]使用最小花费爬楼梯 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution(object):

def minCostClimbingStairs(self, cost):

""":type cost: List[int]:rtype: int"""

dp = [0] * (len(cost) + 1)

for i in range(len(dp)):

j = i - 1

k = i - 2

v_j = dp[j] if j >= 0 else 0

v_k = dp[k] if k >= 0 else 0

c_j = cost[j] if j >= 0 else 0

c_k = cost[k] if k >= 0 else 0

dp[i] = min(v_j + c_j, v_k + c_k)

return dp[-1]区域和检索 - 数组不可变 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class NumArray(object):

def __init__(self, nums):

""":type nums: List[int]"""

self.nums = [0] * (len(nums)+1)

total = 0

for index, v in enumerate(nums):

total += v

self.nums[index+1] = total

def sumRange(self, i, j):

""":type i: int:type j: int:rtype: int"""

return self.nums[j+1] - self.nums[i]最大子序和 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution(object):

def maxSubArray(self, nums):

""":type nums: List[int]:rtype: int"""

# 从后往前想,如果某一状态是当前子序列最大值,

# 那么之前两种状态会转移到当前状态。

# 要么是sum[i-1] + A[i], 要么是 A[i]本身

maxvalue = -float('inf')

dp = -float('inf')

for num in nums:

dp = max(dp + num, num)

maxvalue = max(maxvalue, dp)

return maxvalue买卖股票的最佳时机 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution(object):

def maxProfit(self, prices):

""":type prices: List[int]:rtype: int"""

if len(prices) in [0, 1]:

return 0

else:

minvalue = prices[0]

maxvalue = 0

for p in prices[1: ]:

maxvalue = max(p - minvalue, maxvalue)

minvalue = min(minvalue, p)

return maxvalue

二、 O(n), S = O(3n), T = O(3n)

这类题的典型特点是存在多个状态。申请的dp空间 是 状态数 * n打家劫舍 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution(object):

def rob(self, nums):

""":type nums: List[int]:rtype: int"""

# 0 表示不抢, 1 代表抢当前的屋子

dp = [[0, 0] for _ in range(len(nums)+1)]

for i, n in enumerate(nums):

i += 1

# 前一状态抢或者不抢,当前都可以不抢

dp[i][0] = max(dp[i-1][0], dp[i-1][1])

# 当前抢,那么前一状态只能是不抢。

dp[i][1] = (dp[i-1][0]) + n # 别忘加上当前抢的值

return max(dp[-1])

# 根据状态转移方程所得

class Solution(object):

def rob(self, nums):

s0 = 0

s1 = - float('inf')

for n in nums:

t = s0

s0 = max(s0, s1)

s1 = t + n

return max(s0, s1)打家劫舍 II - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution(object):

def rob(self, nums):

""":type nums: List[int]:rtype: int"""

if len(nums) == 1:

return nums[0]

return max(self.baserob(nums[1:]), self.baserob(nums[:-1]))

def baserob(self, nums):

""":type nums: List[int]:rtype: int"""

# 0 表示不抢, 1 代表抢当前的屋子

dp = [[0, 0] for _ in range(len(nums)+1)]

for i, n in enumerate(nums):

i += 1

# 前一状态抢或者不抢,当前都可以不抢

dp[i][0] = max(dp[i-1][0], dp[i-1][1])

# 当前抢,那么前一状态只能是不抢。

dp[i][1] = (dp[i-1][0]) + n # 别忘加上当前抢的值

return max(dp[-1])最佳买卖股票时机含冷冻期 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

# 这道题让我学会了状态转移相关的内容

# 根据状态转移进行建模。

class Solution(object):

def maxProfit(self, prices):

""":type prices: List[int]:rtype: int"""

# 我们设定三个状态, rest,hold,sold状态。

# 为什么不直接rest 和 hold状态呢?

# 当然可以,不过 这个时候 0, 1 两种状态

# 依存要依存 i - 2的状态

# 故引入一个sold状态,简化操作

# 不可能的状态初始化为 正无穷(min) 负无穷(max)

rest = 0

hold = -float('inf')

sold = -float('inf')

for p in prices:

pre_sold = sold

sold = hold + p

hold = max(hold, rest - p)

rest = max(rest, pre_sold)

return max(sold, hold, rest)删除与获得点数 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution(object):

def deleteAndEarn(self, nums):

""":type nums: List[int]:rtype: int"""

if len(nums) == 0: return 0

data = [0] * (max(nums)+1)

# 桶排序

for n in nums:

data[n] += 1

# 打家劫舍

s0 = 0

s1 = -float('inf')

for index, d in enumerate(data):

t = s0

s0 = max(s0, s1 )

s1 = t + d * index # 唯一不同的地方

return max(s0, s1)多米诺和托米诺平铺 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

使序列递增的最小交换次数 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

# 深度优先搜索 超时

# TODO()可以试一试记忆化搜索,如果有时间的话

class Solution(object):

ans = 1e14

def minSwap(self, A, B):

""":type A: List[int]:type B: List[int]:rtype: int"""

Solution.ans = 1e14

self.dfs(1,0, A, B)

return Solution.ans

def dfs(self, i, c, A, B):

# 剪枝

if c > Solution.ans:

return

if i >= len(A):

Solution.ans =min(Solution.ans, c)

return

# 先前判断,能否不交换。

if A[i-1] < A[i] and B[i-1] < B[i]:

self.dfs(i+1, c, A, B)

# 添加这个判断后可以由通过64 -> 84

# 为什么呢?因为满足有效的交换。

if A[i]>B[i-1] and B[i] > A[i-1]:

A[i], B[i] = B[i], A[i]

self.dfs(i + 1, c + 1, A, B)

A[i], B[i] = B[i], A[i]

# huahua的解法

"""Author: HuahuaRunning time: 60 ms"""

class Solution:

def minSwap(self, A, B):

n = len(A)

dp = [[float('inf'), float('inf')] for _ in range(n)]

dp[0][0], dp[0][1] = 0, 1

for i in range(1, n):

if A[i] > A[i - 1] and B[i] > B[i - 1]:

dp[i][0] = dp[i - 1][0]

dp[i][1] = dp[i - 1][1] + 1

if B[i] > A[i - 1] and A[i] > B[i - 1]:

dp[i][0] = min(dp[i][0], dp[i - 1][1])

dp[i][1] = min(dp[i][1], dp[i - 1][0] + 1)

return min(dp[-1])

三、O(n), S = O(n), T = O(n^2)

这类题的典型特点是,dp[i] 依赖于所有比它小的子问题。

不过也存在模板:单词拆分 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

类型问题是 找到一个分割点。

class Solution(object):

# 从记忆化递归的角度来看,最终状态的值,可能由前面N个状态转移过来。

# 因此是一道很典型的动态规划,依赖于之前所有状态。

def wordBreak(self, s, wordDict):

""":type s: str:type wordDict: List[str]:rtype: bool"""

wordDict = set(wordDict)

dp = [False] * (len(s))

for i in range(1, len(s)+1):

s1 = s[:i]

if s1 in wordDict:

# 表示以结尾为准

dp[i-1] = True

# print(dp)

# 外层控制问题规模

for i in range(len(s)):

if dp[i]:

continue

# 内层查询子问题

for j in range(i):

dp[i] = dp[j] and (s[j+1:i+1] in wordDict)

if dp[i]:

break

# print(dp)

return dp[-1]单词拆分 II - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

from typing import *

class Solution:

def wordBreak(self, s, wordDict):

m = {}

words = set(wordDict)

self.dfs(len(s),s, m, words)

return m[s]

def dfs(self,k, s, m, words):

if s in m:

return m[s]

# 主要是学会了有一个体外变量 ans用于

# 合并子问题的解 + 当前新的解

# 原本一直走在死胡同里,一直想着如果return 出完美结果来

# 但是代码逻辑就很复杂,写不出来

ans = []

if s in words:

ans.append(s)

for can in words:

i = len(s) - len(can)

if i >= 0 and s[i:] == can:

t_lst = self.dfs(i, s[:i], m, words)

t_ans = [w + ' ' + can for w in t_lst]

# 通过一个体外变量ans

# 存储当前所有子问题的答案,合并起来

ans += list(t_ans)

m[s] = list(ans)

return m[s]赛车 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg最长上升子序列 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution:

def lengthOfLIS(self, nums):

if len(nums) <=1:

return len(nums)

dp = [1] * len(nums)

for i in range(1, len(nums)):

for j in range(i):

if nums[i] > nums[j]:

dp[i] = max(dp[i], dp[j] + 1)

return max(dp)最长递增子序列的个数 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution:

def findNumberOfLIS(self, nums: List[int]) -> int:

if len(nums) == 0:return 0

lengths = [1] * len(nums)

counts = [1] * len(nums)

for i in range(1, len(nums)):

for j in range(i):

# 如果后面比前面的值要大

if nums[i] > nums[j]:

# 这是一条转移路径

if lengths[j] + 1 > lengths[i]:

lengths[i] = lengths[j] + 1

counts[i] = counts[j]

# 这是另外一条转移路径。这两条互斥,故是 if elif

elif lengths[i] == lengths[j] + 1:

counts[i] += counts[j]

# 得到最终的数量

maxlength = max(lengths)

total = 0

for i, v in zip(lengths, counts):

if i == maxlength:

total += v

return total

四、O(m+n), S = O(mn), T = O(mn)编辑距离 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

备注:可用递归方法

class Solution:

def minDistance(self, word1: str, word2: str) -> int:

dp = [[0] *(len(word2) + 1) for _ in range(len(word1) + 1)]

# 别忘记初始化,很重要。不能全是0,边界地方要处理

for i in range(len(word1)+1):

dp[i][0] = i

for j in range(len(word2)+1):

dp[0][j] = j

for i in range(1, len(word1)+1):

for j in range(1, len(word2)+1):

if word1[i-1] == word2[j-1]:

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

else:

dp[i][j] = min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j]) + 1

return dp[-1][-1]正则表达式匹配 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

# 尝试了使用DP的算法,很蓝,看了官方的题解之后发现,原来是方向应该从后往前。。?官方题解:正则表达式匹配 - 力扣(LeetCode)​leetcode-cn.comv2-7714b0ca02a234af2b6fd223be87a901_ipico.jpg

class Solution(object):

# 从后往前进行

def isMatch(self, text, pattern):

dp = [[False] * (len(pattern) + 1) for _ in range(len(text) + 1)]

# 设定末尾的空串是为真

dp[-1][-1] = True

for i in range(len(text), -1, -1):

for j in range(len(pattern) - 1, -1, -1):

# 第一个字符是否完全匹配

first_match = i < len(text) and pattern[j] in {text[i], '.'}

# 如果有 * 号 且匹配到类似 x* 这种pattern

if j+1 < len(pattern) and pattern[j+1] == '*':

# 跳两个位置 or ( 第一个匹配到 and 之后的字符串是否匹配)

dp[i][j] = dp[i][j+2] or (first_match and dp[i+1][j])

else:

# 第一个字符是否匹配 and 之后的字符串是否匹配

dp[i][j] = first_match and dp[i+1][j+1]

return dp[0][0]通配符匹配 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

和上一题看样子很类似,都是匹配类的问题。

# 自己做出来的,牛逼坏了(叉腰)

class Solution:

def isMatch(self, s: str, p: str) -> bool:

dp = build_2d_matrix(len(s)+1, len(p)+1, val= False)

dp[-1][-1] = True

for i in range(len(s), -1, -1):

for j in range(len(p)-1, -1, -1):

# 第一个字符是否匹配,如果 i 是空串,则说明没有第一个匹配的串,就为False

first_match = i != len(s) and (s[i] == p[j] or p[j] in ['?', '*'])

# 空串应该如何处理?

# 只有pattern 是 * 时,才会继承之后的值

# 否则说明是不匹配的,包括?是强制一个相等,都应该为False

if i == len(s):

if p[j] == '*':

dp[i][j] = dp[i][j+1]

continue

# bc *bc -- > dp[i][j+1]

# abc *bc --> (first_match and dp[i+1][j+1])

# aa * --> dp[i+1][j] ; 因为 * 可以匹配任何

if p[j] == '*':

dp[i][j] = dp[i][j+1] or dp[i+1][j] or (first_match and dp[i+1][j+1])

else:

dp[i][j] = first_match and dp[i+1][j+1]

return dp[0][0]交错字符串 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution:

def isInterleave(self, s1: str, s2: str, s3: str) -> bool:

dp = build_2d_matrix(len(s1)+1, len(s2)+1, val= False)

if len(s3) != len(s1) + len(s2):

return False

# leetcode官方题解喜欢在 for循环里面进行初始化操作

# 与在函数外进行初始化其实本质相同,但是代码操作略有区别

# 看个人选择。

for i in range(len(dp)):

for j in range(len(dp[0])):

# 空串匹配s3的空串

if i == 0 and j == 0:

dp[i][j] = True

# 某一串 + 空串 是否匹配s3?

elif j == 0:

# 为什么是 i-1的字符串?

# 回答:因为0 预留给了空串,但是s[0] 是第一个字符

dp[i][j] = dp[i-1][j] and s1[i-1] == s3[i+j-1]

# 空串 + 某一串 是否匹配s3?

elif i == 0:

dp[i][j] = dp[i][j-1] and s2[j-1] == s3[i+j-1]

else:

dp[i][j] = (dp[i][j-1] and s2[j-1] == s3[i+j-1]) or (dp[i-1][j] and s1[i-1] == s3[i+j-1])

return dp[-1][-1]不同的子序列 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution:

def numDistinct(self, s: str, t: str) -> int:

dp = build_2d_matrix(len(s)+1, len(t)+1, val=0)

s = '#' +s

t = '#' +t

for i in range(len(dp)):

for j in range(len(dp[0])):

if i == 0 and j == 0:

dp[i][j] = 1

elif i == 0:

pass

# 神奇,默认 t 为空串,s为任意值时都为1

elif j == 0:

dp[i][j] = 1

else:

# 当对应的文字相等时

# 例如 bagg 与 bag 1 + 1

# 例如 bagc 与bag

if s[i] == t[j]:

dp[i][j] = dp[i-1][j] + dp[i-1][j-1]

# 如果不相等,那么就等于子集所能到达

else:

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

return dp[-1][-1]两个字符串的删除操作 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution:

def minDistance(self, word1: str, word2: str) -> int:

dp = build_2d_matrix(len(word1)+1, len(word2)+1)

word1 = '$' + word1

word2 = '$' + word2

for i in range(len(dp)):

for j in range(len(dp[0])):

if i == 0:

dp[i][j] = j

elif j == 0:

dp[i][j] = i

else:

# 如果相等,就继承上一个状态就好

if word1[i] == word2[j]:

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

else:

# 否则就分情况。

# 要么是都不相同,如abc, abd,这样要同时删除 c 和 d, 然后操作数 + 2

# 要么是ab abd,或者abd, ab,这种删除一个字符

dp[i][j] = min(dp[i-1][j-1] +2 , dp[i-1][j] + 1, dp[i][j-1] + 1)

return dp[-1][-1]两个字符串的最小ASCII删除和 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution:

def minimumDeleteSum(self, s1: str, s2: str) -> int:

dp = build_2d_matrix(len(s1)+1, len(s2)+1)

s1 = '%' + s1

s2 = '%' + s2

for i in range(len(dp)):

for j in range(len(dp[0])):

# 和上一题很像

if i== 0 and j == 0:

dp[i][j] = 0

elif i == 0:

dp[i][j] = ord(s2[j]) + dp[i][j-1]

elif j == 0:

dp[i][j] = ord(s1[i]) + dp[i-1][j]

else:

if s1[i] == s2[j]:

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

else:

# 只不过是将 +1 变成了 添加 ord的方式

dp[i][j] = min(dp[i-1][j-1] + ord(s1[i]) + ord(s2[j]),

dp[i-1][j] + ord(s1[i]),

dp[i][j-1] + ord(s2[j]))

return dp[-1][-1]

五、 O(n) + k, S = O(n), T = O(kn)零钱兑换 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg第一种做法参考之前的 单词拆分2,写了 for j inrange(i): 就将复杂度提到了

amount ^ 2的大小。

# 会超时的做法,只通过了32/182

class Solution:

def coinChange(self, coins: List[int], amount: int) -> int:

if amount == 0:

return 0

# 数组表示到达i步的最少钱数

dp = [1e14, ] * (amount+1)

for c in coins:

if c <= amount:

dp[c] = 1

# 这样写,复杂度是 amount ^ 2

# 遇到 coins = [3,7,405,436], amount = 8839就超时了。。。。

for i in range(1, amount+1):

for j in range(i):

right = i - j

if right in coins:

dp[i] = min(dp[i], dp[j] + 1)

if dp[-1] == 1e14:

return -1

return int(dp[-1])

第二种做法:i 是 0 ~ amount, j 是 0 ~ i

变为:

i 是 0 ~ amount, j 是 0 ~ len(coins) 这样复杂度就从 amount^2 将为 amount * len(coins)

class Solution:

def coinChange(self, coins: List[int], amount: int) -> int:

if amount == 0:

return 0

# 数组表示到达i步的最少钱数

dp = [1e14, ] * (amount+1)

for c in coins:

if c <= amount:

dp[c] = 1

# 这样写,复杂度是 amount * len(coins)

# 并没有超时

for i in range(1, amount+1):

# for j in range(i):

# right = i - j

# if right in coins:

# dp[i] = min(dp[i], dp[j] + 1)

for c in coins:

j = i - c

if j >= 0:

dp[i] = min(dp[i], dp[j]+1)

if dp[-1] == 1e14:

return -1

return int(dp[-1])

组合总和 Ⅳ - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

第一反应就是递归呀,但是严重超时

# 通过了 9/17 发生了严重的超时

class Solution:

ans = 0

def combinationSum4(self, nums: List[int], target: int) -> int:

Solution.ans = 0

self.func(nums, 0, target)

return Solution.ans

def func(self, nums, cur, target):

if cur > target:

return

if cur == target:

Solution.ans += 1

return

for v in nums:

self.func( nums, cur + v, target)题解:动态规划(Python 代码、Java 代码) - 力扣(LeetCode)​leetcode-cn.comv2-213893f96a0dd858566f001d9a7a9198_180x120.jpg

看了题解后豁然开朗~ 有时间补dp算法

# 使用了记忆化递归实现了

class Solution:

m = {}

def combinationSum4(self, nums: List[int], target: int) -> int:

Solution.ans = 0

Solution.m = {}

a = self.func(nums, target)

return a

def func(self, nums, target):

if target == 0:

return 1

if target < 0:

return 0

if target not in Solution.m:

ans = 0

for num in nums:

ans += self.func(nums, target - num)

Solution.m[target] = ans

return Solution.m[target]分割等和子集 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

刚开始想的时候没有思路,不清楚是针对target进行dp还是对输入的序列进行dp。

但是看了题解之后!!豁然开朗!!!

可以把问题看成从数组中只选择部分的元素,使元素之和恰好等于总数值的一半!!!题解:0-1 背包问题 + 针对本题的优化(Python 代码、Java 代码) - 力扣(LeetCode)​leetcode-cn.comv2-2fbbba5aad2fc010dbfa9b45c9afc4ab_180x120.jpg

# 超时了,这种背包问题算法很容易超时 74/106

class Solution(object):

def canPartition(self, nums):

""":type nums: List[int]:rtype: bool"""

if len(nums) == 0: return False

target = sum(nums)

if target % 2 == 1:

return False

target = target // 2

dp = build_2d(len(nums) + 1, target + 1, False)

nums = [0, ] + nums

for i in range(len(dp)):

for j in range(len(dp[0])):

if i == 0 and j == 0:

dp[i][j] = True

elif i == 0:

dp[i][j] = False

elif j == 0:

dp[i][j] = True

else:

# 第一次写的转移方程是 dp[i][j] = dp[i-1][j] or (dp[i][j - nums[i]])

# 这样意味着同样的一件物品可以使用多次

# 因为使用了 j - nums[i] ,故要判断是否大于0

if j - nums[i] >= 0:

dp[i][j] = dp[i-1][j] or (dp[i-1][j - nums[i]])

else:

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

return dp[-1][-1]目标和 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

这道题很奇怪,搜索范围是 - sum(nums) ~ +sum(nums)

# 记忆化递归

class Solution:

def findTargetSumWays(self, nums: List[int], S: int) -> int:

memory = {}

return self.func(nums, 0 , S, memory)

def func(self, nums, index, target, memory):

if index == len(nums):

if target == 0:

return 1

else:

return 0

# 这个就好比是 dp里的状态

# 一个是当前的下标值,一个是目标值

tmp = (index, target)

if tmp in memory:

return memory[tmp]

memory[tmp] = self.func(nums, index + 1, target - nums[index], memory) + \

self.func(nums,index + 1, target + nums[index], memory)

return memory[tmp]

#

六、O(n) + k, S = O(n), T = O(kn^2)最大平均值和的分组 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg看到输入的数据长度是1~100,大概就知道是使用O(n^3)的算法。

dp[k][i] 其中的k表示分割成k组,i代表输入的规模。

例如初始化dp[1][i]代表的是将前i个输入分割成1组,所能够得到的平均值。

class Solution:

def largestSumOfAverages(self, A: List[int], K: int) -> float:

dp = [[0]*(len(A)+1) for _ in range(K+1)]

sums = [0, ]

for i in range(len(A)):

i += 1

sums.append(sums[-1] + A[i-1])

dp[1][i] = \

sums[-1] / i

# k 代表分成几部分,k = 2 表示分成两部分

for k in range(2, K+1):

# i 代表问题规模,分成k部分,就要从k开始

for i in range(k, len(A)+1):

# j代表二分割的分割点的位置

for j in range(k-1, i):

dp[k][i] = max(dp[k][i], dp[k-1][j] + (sums[i] - sums[j]) / ( i - j) )

return dp[-1][-1]

TODO()尝试用记忆化搜索的方式

七、O(n), S = O(n^2), T = O(n^3)

与四、编辑距离类型题相区分。此类型题的通用点是:只有求解所有的子数组的解,才能去构成原问题的解。

而且这里没有顺序:类似前面的单词的题,都会假定

最后一个词是候选词的情况下,去搜索前面的可能性。而本类型题,是不清楚最后一个取值状态是在哪个位置取出。比如戳气球,有可能最后是在中间戳破的。(但是这不是本质,其实两者都一样。找到候选词也可以通过for循环来找分割点,只是时间复杂度会很高)戳气球 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution:

def maxCoins(self, nums: List[int]) -> int:

n = len(nums)

nums.append(1)

nums.insert(0,1)

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

for l in range(1, n + 1):

for i in range(1, len(nums)-l):

j = i + l -1

for k in range(i, j+1):

dp[i][j] = max(dp[i][j],

nums[i-1]*nums[j+1]*nums[k] + dp[i][k-1] + dp[k+1][j])

return dp[1][n]奇怪的打印机 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

八、O(n^2), S = O(n^3), T = O(n^3)摘樱桃 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg使用两遍DP算法是不行的。

可以视作是两个人同时在走,注意如果走在了同一个格子上,就只能有一个人吃到樱桃。

from typing import *

# 美妙的记忆化搜索~!!

from collections import defaultdict

class Solution:

grid = None

def cherryPickup(self, grid: List[List[int]]) -> int:

n = len(grid)

m = defaultdict(int)

Solution.m = m

Solution.grid = grid

Solution.n = n

self.dfs(n-1, n-1, n-1, m)

return max(0, Solution.m[(n-1, n-1, n-1)])

def dfs(self, x1, y1, x2, m):

y2 = x1 + y1 - x2

# 边界值

if x1 < 0 or y1 < 0 or x2 < 0 or y2 < 0:

return -1

# 任何一个人走到了 -1 的格子上 ,就表示没有解

if Solution.grid[x1][y1] < 0 or Solution.grid[x2][y2] < 0:

return -1

# 当两个人同时走到了 (0,0)这个点,只有一个人可以取

if x1 == 0 and y1 == 0 and x2 == 0 :

m[(x1, y1, x2)] = Solution.grid[x1][y1]

return Solution.grid[x1][y1]

if (x1, y1, x2) in m:

return m[(x1, y1, x2)]

ans = max(

self.dfs(x1-1, y1, x2-1, m),

self.dfs(x1, y1-1, x2-1, m),

self.dfs(x1, y1-1, x2, m),

self.dfs(x1-1, y1, x2, m),

)

# 如果子问题求解是无解的

if ans< 0:

m[(x1, y1, x2)] = -1

return -1

# 子问题有解,加上当前的值

ans += Solution.grid[x1][y1]

# 如果并非是在同一个子问题上重叠的话

if x1 != x2:

ans += Solution.grid[x2][y2]

m[(x1,y1,x2)] = ans

return max(0, m[(x1,y1,x2)])

九、O(n), S = O(n^3), T = O(n^4)移除盒子 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

十、O(n), S = O(n*2^n), T = (n^2*2^n)最短超级串 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg不同路径 II - 力扣(LeetCode)​leetcode-cn.comv2-50ee09bddc06507fc3d53fa1e5986ca6_180x120.jpg

class Solution:

def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:

dp = build_2d_matrix(len(obstacleGrid), len(obstacleGrid[0]))

for i in range(len(dp)):

if obstacleGrid[i][0] == 1:

break

dp[i][0] = 1

for j in range(len(dp[0])):

if obstacleGrid[0][j] == 1:

break

dp[0][j] = 1

for i in range(1,len(dp)):

for j in range(1, len(dp[0])):

if obstacleGrid[i][j] == 1:

dp[i][j] = 0

else:

dp[i][j] = dp[i-1][j] + dp[i][j-1]

return dp[-1][-1]不同路径 III - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution:

def uniquePathsIII(self, grid: List[List[int]]) -> int:

count = 0

sx = -1

sy = -1

for i in range(len(grid)):

for j in range(len(grid[0])):

if grid[i][j] == 0:

count += 1

elif grid[i][j] == 1:

sx, sy = i, j

return self.dfs(grid, sx, sy, count+1)

def dfs(self, grid, x, y, n):

if x < 0 or x >= len(grid) or y < 0 or y >= len(grid[0]):

return 0

if grid[x][y] == -1:

return 0

if grid[x][y] == 2 :

if n == 0:

return 1

else:

return 0

# 回溯法

grid[x][y] = -1

# 朝四个方向搜索

paths = self.dfs(grid, x+1, y, n-1) +\

self.dfs(grid, x-1, y, n - 1) +\

self.dfs(grid, x, y+1, n - 1) +\

self.dfs(grid, x, y-1, n - 1)

# 回溯完成后别忘记修复

grid[x][y] = 0

return paths正方形数组的数目 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

这里推荐官方题解官方题解:正方形数组的数目 - 力扣(LeetCode)​leetcode-cn.com

class Solution:

# 这道题构建成了一个图的方式,通过遍历图来解决

def numSquarefulPerms(self, A: List[int]) -> int:

# 得到节点的个数

counter = collections.Counter(A)

# 构建图

graph = collections.defaultdict(list)

for x in counter:

for y in counter:

if x + y == int(math.sqrt(x + y)) ** 2:

graph[x].append(y)

ans = 0

for x in counter:

# 还要遍历除了当前节点以外的 len(A) - 1个节点

ans += self.dfs(counter,graph, x, len(A)-1)

return ans

def dfs(self, counter, graph, x, todo):

# 遍历到的当前节点 个数 -1

counter[x] -= 1

if todo == 0:

ans = 1

else:

ans = 0

# 对所有相邻的邻居节点而言

for y in graph[x]:

# 如果还有节点可以相邻

if counter[y] != 0:

ans += self.dfs(counter,graph, y, todo-1)

# 注意回溯法要恢复

counter[x] += 1

return ans

学到了一个新的方法,@functools.lru_cache(maxsize=None)

十一、O(mn), S = O(mn), T = O(mn)不同路径 - 力扣(LeetCode)​leetcode-cn.comv2-50ee09bddc06507fc3d53fa1e5986ca6_180x120.jpg

class Solution:

def uniquePaths(self, m: int, n: int) -> int:

dp = [[0] * (n) for _ in range(m)]

for i in range(len(dp[0])):

dp[0][i] = 1

for j in range(len(dp)):

dp[j][0] = 1

for i in range(1, len(dp)):

for j in range(1, len(dp[0])):

dp[i][j] = max(dp[i][j], dp[i-1][j] + dp[i][j-1])

return dp[-1][-1]最小路径和 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

MAXINF = float('inf')

import functools

class Solution:

grid = None

def minPathSum(self, grid: List[List[int]]) -> int:

m, n = len(grid), len(grid[0])

Solution.grid = grid

return self.dfs(m-1, n-1)

@functools.lru_cache(maxsize=None)

def dfs(self, x, y):

if x == 0 and y == 0:

return Solution.grid[0][0]

if x < 0 or y < 0 or x >= len(Solution.grid) or y >= len(Solution.grid[0]):

return MAXINF

return min(self.dfs(x-1, y), self.dfs(x, y-1)) + Solution.grid[x][y]三角形最小路径和 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

MAXINF = float('inf')

import functools

class Solution:

grid = None

def minPathSum(self, grid: List[List[int]]) -> int:

m, n = len(grid), len(grid[0])

Solution.grid = grid

return self.dfs(m-1, n-1)

@functools.lru_cache(maxsize=None)

def dfs(self, x, y):

if x == 0 and y == 0:

return Solution.grid[0][0]

if x < 0 or y < 0 or x >= len(Solution.grid) or y >= len(Solution.grid[0]):

return MAXINF

return min(self.dfs(x-1, y), self.dfs(x, y-1)) + Solution.grid[x][y]地下城游戏 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

import functools

MAXINF = float('inf')

MININF = -float('inf')

class Solution:

grid = None

m = None

n = None

ret = None

def calculateMinimumHP(self, dungeon: List[List[int]]) -> int:

Solution.grid = dungeon

Solution.m = len(dungeon)

Solution.n = len(dungeon[0])

Solution.ans = MININF

ans = self.dfs(0, 0)

return ans

@functools.lru_cache(maxsize=None)

def dfs(self, x, y):

if x < 0 or y < 0 or x >= len(Solution.grid) or y >= len(Solution.grid[0]):

return MAXINF

if x == Solution.m - 1 and y == Solution.n - 1:

return max(1, 1 - Solution.grid[x][y])

# max (1 , -?) 代表当前 奖励很大,其实就可以视作是1,表示能走的最小需求

ans = max(1, min(self.dfs(x+1, y), self.dfs(x, y+1)) - Solution.grid[x][y])

return ans最大矩形 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution:

def maximalRectangle(self, matrix: List[List[str]]) -> int:

r = len(matrix)

if r == 0:

return 0

c = len(matrix[0])

dp = [[0]*c for _ in range(r)]

for i in range(r):

for j in range(c):

if matrix[i][j] == '1':

if j == 0:

dp[i][j] = 1

else:

dp[i][j] = dp[i][j-1] + 1

else:

dp[i][j] = 0

# 这个解法不是动态规划了吧?

ans = 0

for i in range(r):

for j in range(c):

length = float('inf')

for k in range(i, r):

length = min(length, dp[k][j])

if length == 0:

break

ans = max(length * (k - i + 1), ans)

return ans下降路径最小和 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

import functools

MAXINF = float('inf')

MININF = -float('inf')

class Solution:

grid = None

def minFallingPathSum(self, A: List[List[int]]) -> int:

Solution.grid = A

ans = []

for j in range(len(A[0])):

ans.append(self.dfs(len(A)-1, j))

return min(ans)

@functools.lru_cache(maxsize=None)

def dfs(self, x, y):

if x < 0:

return 0

if x < 0 or y < 0 or x >= len(Solution.grid) or y >= len(Solution.grid[0]):

return MAXINF

return min(self.dfs(x-1, y-1), self.dfs(x-1, y), self.dfs(x-1, y+1)) + Solution.grid[x][y]

十二、O(mn) + k, S = O(kmn) T = O(kmn)“马”在棋盘上的概率 - 力扣(LeetCode)​leetcode-cn.comv2-9935353910e0dc20d9d92b563ccc2a49_ipico.jpg

from copy import deepcopy

class Solution:

def knightProbability(self, N: int, K: int, r: int, c: int) -> float:

a = [[0] * N for _ in range(N)]

dp= [deepcopy(a) for _ in range(K+1)]

dp[0][r][c] = 1

directions = [(1,2), (-1,-2), (1,-2), (-1,2),

(2,1), (-2,-1), (2,-1), (-2,1)]

for k in range(1,K+1):

for i in range(N):

for j in range(N):

for (a,b) in directions:

x = i + a

y = j + b

if x < 0 or y < 0 or x >= N or y >= N:

continue

# print(k,x,y)

dp[k][i][j] += dp[k-1][x][y]

total = 0

m = dp[-1]

for i in range(N):

for j in range(N):

total += m[i][j]

return total / 8 ** K

附录:股票类题目求解

参考:题解:一个方法团灭 6 道股票问题 - 力扣(LeetCode)​leetcode-cn.comv2-e0346ff56b7133eee6ab31983f2352ad_ipico.jpg思想都是设置 i, k, state,分别表示,天数、还可以几次操作、状态数(0,1分别表示不持有、持有)

第一道题:买卖股票的最佳时机 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg因为只买卖一次,k = 1

注意,这里的持有状态,是dp[i][1]=max(dp[i-1][1],-prices[i])

代表今天买入 与 过去买入时所花费的最小值。状态初始化时,利用到了 -inf注意这里的状态转移方程:过去买过了,那就继承下;过去没有买,那么就今天买入

class Solution:

def maxProfit(self, prices: List[int]) -> int:

n = len(prices)

if n == 0:

return 0

dp = build_2d_matrix(len(prices), 2)

dp[0][0] = 0

dp[0][1] = -prices[0]

for i in range(1, len(prices)):

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])

# 因为只能买一次, 因此 第二个参数是, 每一天都是买入

dp[i][1] = max(dp[i-1][1], -prices[i])

return dp[n-1][0]

第二道题:买卖股票的最佳时机 II - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg无限次买卖。

只需要改动如下一行代码便可。

dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])

总结一下就是:因为斜线只能走一次,故只能从1变成0 一次。走到0之后,无法再走到1,故限制了只能交易一次。从1可以变成0,变成0之后又可以变成1,故相当于是无限次交易。

第三道题:最佳买卖股票时机含冷冻期 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg手里有股票,依赖于 i-2那天的状态

class Solution:

def maxProfit(self, prices: List[int]) -> int:

n = len(prices)

if n == 0:

return 0

dp = build_2d_matrix(len(prices)+2, 2)

dp[0][0] = 0

dp[0][1] = -float('inf')

dp[1][1] = -float('inf')

for i in range(2, len(dp)):

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i-2])

dp[i][1] = max(dp[i-1][1], dp[i-2][0]-prices[i-2])

return dp[-1][0]思考:

为什么是i-2?如果从冷冻期来看,确实第i天购买,需要i-2天未持有时的状态。但是如果一个人到i-1天都不购买,那么第i天时可以购买的。因此第i天持有的状态是与i-1天有关喽?如果有关说明就有一条边。但是答案里却没有考虑这种情况,为什么?

回答:

因为如果到第i-1天都不购买。那么说明第i-2天也是不购买,dp[i-1][0] == dp[i-2][0]。那么实际上这种状态是答案那种解法的子集。

第四道题:买卖股票的最佳时机含手续费 - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution:

def maxProfit(self, prices: List[int], fee: int) -> int:

n = len(prices)

if n == 0:

return 0

dp = build_2d_matrix(len(prices)+1, 2)

dp[0][0] = 0

dp[0][1] = -float('inf')

for i in range(1, len(dp)):

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i-1])

dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i-1] - fee)

return dp[-1][0]

第五道题:买卖股票的最佳时机 III - 力扣(LeetCode)​leetcode-cn.comv2-5aef94ed53c22bbb9cf77c6a92ef921a_ipico.jpg

class Solution:

def maxProfit(self, prices: List[int]) -> int:

n = len(prices)

if n == 0:

return 0

# dp [0] 是用于初始化不可能的

# dp[1] 是用与初始化交易 卖出0 次的状态

# dp[2] 是指已经卖出了一次的状态

# dp[3] 是指已经卖出了二次的状态

dp = build_3dmatrix(4, n+1, 2)

for k in range(0, 4):

for i in range(0,n+1):

if k == 0:

dp[0][i][1] = -float('inf')

continue

if i == 0:

dp[k][0][1] = -float('inf')

continue

# 规定卖出的时候,k += 1

# 注意卖出的时候,必然交易次数 + 1, 因为没有dp[k][i-1][1] + prices[i-1]

# 这个状态

dp[k][i][0] = max(dp[k][i-1][0], dp[k-1][i-1][1] + prices[i-1])

dp[k][i][1] = max(dp[k][i-1][1], dp[k][i-1][0] - prices[i-1])

return dp[-1][-1][0]

# 超时了, 通过209 / 211

class Solution:

def maxProfit(self, K: int, prices: List[int]) -> int:

n = len(prices)

if n == 0 :

return 0

# dp [0] 是用于初始化不可能的

# dp[1] 是用与初始化交易 卖出0 次的状态

# dp[2] 是指已经卖出了一次的状态

# dp[3] 是指已经卖出了二次的状态

dp = build_3dmatrix(K+2, n+1, 2)

for k in range(0, K+2):

for i in range(0,n+1):

if k == 0:

dp[0][i][1] = -float('inf')

continue

if i == 0:

dp[k][0][1] = -float('inf')

continue

# 规定卖出的时候,k += 1

# 注意卖出的时候,必然交易次数 + 1, 因为没有dp[k][i-1][1] + prices[i-1]

# 这个状态

dp[k][i][0] = max(dp[k][i-1][0], dp[k-1][i-1][1] + prices[i-1])

dp[k][i][1] = max(dp[k][i-1][1], dp[k][i-1][0] - prices[i-1])

return dp[-1][-1][0]

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值