先言:所有权利归花花酱所有,部分题解参考leetcode官方题解,,本人仅通过此篇博客记录,在此表示感谢!
一、O(n), S = O(n), T = O(n)
模板如下:爬楼梯 - 力扣(LeetCode)leetcode-cn.com
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.com
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.com
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.com
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.com
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.com
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.com
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.com
# 这道题让我学会了状态转移相关的内容
# 根据状态转移进行建模。
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.com
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.com
使序列递增的最小交换次数 - 力扣(LeetCode)leetcode-cn.com
# 深度优先搜索 超时
# 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.com
类型问题是 找到一个分割点。
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.com
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.com最长上升子序列 - 力扣(LeetCode)leetcode-cn.com
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.com
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.com
备注:可用递归方法
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.com
# 尝试了使用DP的算法,很蓝,看了官方的题解之后发现,原来是方向应该从后往前。。?官方题解:正则表达式匹配 - 力扣(LeetCode)leetcode-cn.com
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.com
和上一题看样子很类似,都是匹配类的问题。
# 自己做出来的,牛逼坏了(叉腰)
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.com
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.com
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.com
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.com
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.com第一种做法参考之前的 单词拆分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.com
第一反应就是递归呀,但是严重超时
# 通过了 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.com
看了题解后豁然开朗~ 有时间补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.com
刚开始想的时候没有思路,不清楚是针对target进行dp还是对输入的序列进行dp。
但是看了题解之后!!豁然开朗!!!
可以把问题看成从数组中只选择部分的元素,使元素之和恰好等于总数值的一半!!!题解:0-1 背包问题 + 针对本题的优化(Python 代码、Java 代码) - 力扣(LeetCode)leetcode-cn.com
# 超时了,这种背包问题算法很容易超时 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.com
这道题很奇怪,搜索范围是 - 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.com看到输入的数据长度是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.com
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.com
八、O(n^2), S = O(n^3), T = O(n^3)摘樱桃 - 力扣(LeetCode)leetcode-cn.com使用两遍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.com
十、O(n), S = O(n*2^n), T = (n^2*2^n)最短超级串 - 力扣(LeetCode)leetcode-cn.com不同路径 II - 力扣(LeetCode)leetcode-cn.com
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.com
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.com
这里推荐官方题解官方题解:正方形数组的数目 - 力扣(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.com
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.com
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.com
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.com
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.com
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.com
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.com
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.com思想都是设置 i, k, state,分别表示,天数、还可以几次操作、状态数(0,1分别表示不持有、持有)
第一道题:买卖股票的最佳时机 - 力扣(LeetCode)leetcode-cn.com因为只买卖一次,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.com无限次买卖。
只需要改动如下一行代码便可。
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.com手里有股票,依赖于 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.com
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.com
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]