LeetCode: 动态规划
目录
动态规划
斐波那契数
F(0) = 0, F(1) = 1
F(n) = F(n - 1) + F(n - 2), 其中 n > 1
给你 n, 请计算 F(n).
版本1
class Solution:
def fib(self, n: int) -> int:
if n < 2:
return n
dp = [0 for _ in range(n + 1)]
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[-1]
版本2 空间优化, 只需要前后两步
class Solution:
def fib(self, n: int) -> int:
if n < 2:
return n
f0, f1 = 0, 1
for i in range(2, n + 1):
f0, f1 = f1, f0
f1 = f0 + f1
return f1
爬楼梯
假设你正在爬楼梯, 需要 n 阶你才能到达楼顶, 每次你可以爬 1 或 2 个台阶. 你有多少种不同的方法可以爬到楼顶呢? 注意: 给定 n 是一个正整数. 示例:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
class Solution:
def climbStairs(self, n: int) -> int:
# F(1) = 1, F(2) = 2, F(n) = F(n - 1) + F(n - 2), where n > 2.
if n < 3:
return n
f0, f1 = 1, 2
for i in range(3, n + 1):
f0, f1 = f1, f0
f1 = f0 + f1
return f1
使用最小花费爬楼梯
数组的每个下标作为一个阶梯, 第 i 个阶梯对应着一个非负数的体力花费值 cost[i] (下标从 0 开始). 每当你爬上一个阶梯你都要花费对应的体力值, 一旦支付了相应的体力值, 你就可以选择向上爬一个阶梯或者爬两个阶梯. 请你找出达到楼层顶部的最低花费. 在开始时, 你可以选择从下标为 0 或 1 的元素作为初始阶梯. 示例:
输入: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出: 6
解释: 最低花费方式是从 cost[0] 开始, 逐个经过那些 1, 跳过 cost[3], 一共花费 6.
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
# 到达第 i 个下标的 cost, len(cost) 下标为顶部
dp = [0 for _ in range(len(cost) + 1)]
for i in range(2, len(cost) + 1):
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
return dp[-1]
整数拆分
给定一个正整数 n, 将其拆分为至少两个正整数的和, 并使这些整数的乘积最大化, 返回你可以获得的最大乘积. 示例:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36.
版本1 动态规划
class Solution:
def integerBreak(self, n: int) -> int:
dp = [0 for _ in range(n + 1)]
for i in range(2, n + 1):
for j in range(1, i):
dp[i] = max(dp[i], max(dp[i - j] * j, (i - j) * j))
return dp[-1]
版本2 动态规划
class Solution:
def integerBreak(self, n: int) -> int:
dp = [0 for _ in range(n + 1)]
dp[2] = 1
dp[3] = 2
for i in range(3, n + 1):
dp[i] = max(dp[i - 2] * 2, dp[i - 3] * 3, (i - 2) * 2, (i - 3) * 3)
return dp[-1]
版本3
class Solution:
def integerBreak(self, n: int) -> int:
if n == 2:
return 1
if n == 3:
return 2
res = 1
while n > 4:
res *= 3
n -= 3
res *= n
return res
最大子序和
给定一个整数数组 nums, 找到一个具有最大和的连续子数组 (子数组最少包含一个元素), 返回其最大和. 示例:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大, 为 6.
版本1
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
maxsum = nums[0]
# pre, cur: 以当前下标值结尾的最大连续子数和
pre, cur = nums[0], nums[0]
for i in range(1, len(nums)):
maxsum = max(maxsum, nums[i], pre + nums[i])
cur = max(pre + nums[i], nums[i])
pre = cur
return maxsum
版本2 简化改进
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
pre = float('-inf')
maxnum = nums[0]
for i in range(len(nums)):
# 以当前下标值结尾的最大连续子数和
pre = max(pre + nums[i], nums[i])
maxnum = max(maxnum, pre)
return maxnum
乘积最大子数组
给你一个整数数组 nums, 请你找出数组中乘积最大的连续子数组 (该子数组中至少包含一个数字), 并返回该子数组所对应的乘积. 示例:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6.
class Solution:
def maxProduct(self, nums: List[int]) -> int:
res = float('-inf')
maxprod, minprod = 1, 1
for i in range(len(nums)):
mx, mi = maxprod, minprod
maxprod = max(nums[i], mx * nums[i], mi * nums[i])
minprod = min(nums[i], mi * nums[i], mx * nums[i])
res = max(res, maxprod)
return res
打家劫舍
你是一个专业的小偷, 计划偷窃沿街的房屋. 每间房内都藏有一定的现金, 影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统, 如果两间相邻的房屋在同一晚上被小偷闯入, 系统会自动报警. 给定一个代表每个房屋存放金额的非负整数数组, 计算你不触动警报装置的情况下, 一夜之内能够偷窃到的最高金额. 示例:
输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12.
版本1
class Solution:
def rob(self, nums: List[int]) -> int:
# dp[i]: 前 i 间房屋偷窃到的最高金额 (偷窃 i 或不偷窃 i)
dp = [0 for _ in range(len(nums) + 1)]
dp[1] = nums[0]
for i in range(2, len(nums) + 1):
dp[i] = max(nums[i - 1] + dp[i - 2], dp[i - 1])
return dp[-1]
版本2 空间优化
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 0:
return 0
# cur: 在当前下标能偷窃到的最高金额
pre, cur = 0, 0
for i in range(len(nums)):
cur, pre = max(nums[i] + pre, cur), cur
return cur
打家劫舍 II
你是一个专业的小偷, 计划偷窃沿街的房屋, 每间房内都藏有一定的现金. 这个地方所有的房屋都围成一圈, 这意味着第一个房屋和最后一个房屋是紧挨着的. 同时, 相邻的房屋装有相互连通的防盗系统, 如果两间相邻的房屋在同一晚上被小偷闯入, 系统会自动报警. 给定一个代表每个房屋存放金额的非负整数数组, 计算你在不触动警报装置的情况下, 能够偷窃到的最高金额. 示例:
输入: nums = [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋 (金额 = 2), 然后偷窃 3 号房屋 (金额 = 2), 因为他们是相邻的.
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 0:
return 0
if len(nums) == 1:
return nums[0]
# 最后一个不偷
pre, cur = 0, 0
for i in range(len(nums) - 1):
cur, pre = max(nums[i] + pre, cur), cur
res = cur
# 最后一个偷
pre, cur = 0, 0
for i in range(1, len(nums)):
cur, pre = max(nums[i] + pre, cur), cur
res = max(res, cur)
return res
打家劫舍 III
在上次打劫完一条街道之后和一圈房屋后, 小偷又发现了一个新的可行窃的地区. 这个地区只有一个入口, 我们称之为根. 除了根之外, 每栋房子有且只有一个父房子与之相连. 一番侦察之后, 聪明的小偷意识到这个地方的所有房屋的排列类似于一棵二叉树. 如果两个直接相连的房子在同一天晚上被打劫, 房屋将自动报警. 计算在不触动警报的情况下, 小偷一晚能够盗取的最高金额. 示例:
输入: [3,4,5,1,3,null,1]
3
/ \
4 5
/ \ \
1 3 1
输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def rob(self, root: TreeNode) -> int:
def dfs(root):
if not root:
return [0, 0]
# dp[0] 当前node能盗取的最高金额, 该节点不偷
# dp[1] 当前node能盗取的最高金额, 该节点偷
left = dfs(root.left)
right = dfs(root.right)
dp = [0] * 2
dp[0] = max(left[0], left[1]) + max(right[0], right[1])
dp[1] = left[0] + right[0] + root.val
return dp
res = dfs(root)
return max(res[0], res[1])
不同路径
一个机器人位于一个 m * n 网格的左上角, 机器人每次只能向下或者向右移动一步. 机器人试图达到网格的右下角. 问总共有多少条不同的路径? 示例:
输入: m = 3, n = 2
输出: 3
解释: 从左上角开始,总共有 3 条路径可以到达右下角:
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[0 for _ in range(n)] for _ in range(m)]
for i in range(m):
dp[i][0] = 1
for j in range(n):
dp[0][j] = 1
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
return dp[m - 1][n - 1]
不同路径 II
一个机器人位于一个 m * n 网格的左上角. 机器人每次只能向下或者向右移动一步. 机器人试图达到网格的右下角. 现在考虑网格中有障碍物, 那么从左上角到右下角将会有多少条不同的路径? 示例:
输入: obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出: 2
解释: 3 * 3 网格的正中间有一个障碍物, 从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
m, n = len(obstacleGrid), len(obstacleGrid[0])
dp = [[0 for _ in range(n)] for _ in range(m)]
for i in range(m):
if obstacleGrid[i][0] == 1:
break
dp[i][0] = 1
for j in range(n):
if obstacleGrid[0][j] == 1:
break
dp[0][j] = 1
for i in range(1, m):
for j in range(1, n):
if obstacleGrid[i][j] == 1:
dp[i][j] = 0
else:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
return dp[m - 1][n - 1]
最小路径和
给定一个包含非负整数的 m * n 网格 grid, 请找出一条从左上角到右下角的路径, 使得路径上的数字总和为最小. 说明: 每次只能向下或者向右移动一步. 示例:
输入: grid = [[1,3,1],[1,5,1],[4,2,1]]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小.
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
dp = [[0 for _ in range(len(grid[0]))] for _ in range(len(grid))]
dp[0][0] = grid[0][0]
for i in range(1, len(grid)):
dp[i][0] = dp[i - 1][0] + grid[i][0]
for j in range(1, len(grid[0])):
dp[0][j] = dp[0][j - 1] + grid[0][j]
for i in range(1, len(grid)):
for j in range(1, len(grid[0])):
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
return dp[len(grid) - 1][len(grid[0]) - 1]
三角形最小路径和
给定一个三角形 triangle, 找出自顶向下的最小路径和. 每一步只能移动到下一行中相邻的结点上. 相邻的结点 在这里指的是下标与上一层结点下标相同或者等于上一层结点下标 +1 的两个结点. 也就是说, 如果正位于当前行的下标 i, 那么下一步可以移动到下一行的下标 i 或 i + 1. 示例:
输入: triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出: 11
解释: 如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11 (即, 2 + 3 + 5 + 1 = 11).
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
dp = [0 for _ in range(len(triangle))]
dp[0] = triangle[0][0]
for i in range(1, len(triangle)):
dp[i] = dp[i - 1] + triangle[i][i]
for j in range(i - 1, 0, -1):
dp[j] = min(dp[j - 1], dp[j]) + triangle[i][j]
dp[0] = dp[0] + triangle[i][0]
return min(dp)
解码方法
一条包含字母 A-Z 的消息通过以下映射进行了编码:
'A' -> 1
'B' -> 2
...
'Z' -> 26
要解码已编码的消息, 所有数字必须基于上述映射的方法, 反向映射回字母 (可能有多种方法). 给你一个只含数字的非空字符串 num, 请计算并返回解码方法的总数. 题目数据保证答案肯定是一个 32 位 的整数. 示例:
输入: s = "226"
输出: 3
解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6).
class Solution:
def numDecodings(self, s: str) -> int:
if s[0] == '0':
return 0
dp = [0 for _ in range(len(s))]
dp[0] = 1
for i in range(1, len(s)):
if s[i] != '0':
dp[i] += dp[i - 1]
num = 10 * (ord(s[i - 1]) - ord('0')) + (ord(s[i]) - ord('0'))
if 10 <= num <= 26:
if i == 1:
dp[i] += 1
else:
dp[i] += dp[i - 2]
return dp[-1]
最长回文子串
给你一个字符串 s, 找到 s 中最长的回文子串. 示例:
输入: s = "babad"
输出: "bab" ("aba" 同样是符合题意的答案)
版本1 动态规划, 易超时
class Solution:
def longestPalindrome(self, s: str) -> str:
res = ''
# dp[i][j]: s[i:j+1]是否为回文子串
# dp[i][j]=dp[i+1][j-1] and s[i]==s[j]
dp = [[False for _ in range(len(s))] for _ in range(len(s))]
for j in range(len(s)):
for i in range(j + 1):
if j - i < 2: # 不能i+1>j-1: [i,i],[i,i+1]
dp[i][j] = s[i] == s[j]
else:
dp[i][j] = dp[i + 1][j - 1] and s[i] == s[j]
if dp[i][j] and j - i + 1 > len(res):
res = s[i:j + 1]
return res
版本2 中心扩散
class Solution:
def longestPalindrome(self, s: str) -> str:
def expand(s, left, right):
i, j = left, right
while i >= 0 and j < len(s) and s[i] == s[j]:
i -= 1
j += 1
return i + 1, j - 1
res = ''
for i in range(len(s)):
left1, right1 = expand(s, i, i)
left2, right2 = expand(s, i, i + 1)
if right1 - left1 + 1 > len(res):
res = s[left1:right1 + 1]
if right2 - left2 + 1 > len(res):
res = s[left2:right2 + 1]
return res
最长回文子序列
给定一个字符串 s, 找到其中最长的回文子序列, 并返回该序列的长度, 可以假设 s 的最大长度为 1000. 注意最长回文子序列可以不是连续的, 而最长回文子串需要连续. 示例:
输入:
"cabbeaf"
输出:
4 ("abba")
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
dp = [[0 for _ in range(len(s))] for _ in range(len(s))]
for i in range(len(s)):
dp[i][i] = 1
for i in range(len(s) - 1, -1, -1):
for j in range(i + 1, len(s)):
if s[i] == s[j]:
dp[i][j] = dp[i + 1][j - 1] + 2
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
return dp[0][len(s) - 1]
分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串, 返回 s 所有可能的分割方案. 回文串 是正着读和反着读都一样的字符串. 示例:
输入: s = "aab"
输出: [["a","a","b"],["aa","b"]]
class Solution:
def partition(self, s: str) -> List[List[str]]:
def dfs(s, dp, res, path, begin):
if begin == len(s):
res.append(path[:])
for i in range(begin, len(s)):
if dp[begin][i]:
path.append(s[begin:i + 1])
dfs(s, dp, res, path, i + 1)
path.pop()
dp = [[False for _ in range(len(s))] for _ in range(len(s))]
for j in range(len(s)):
for i in range(j + 1):
if j - i < 2:
dp[i][j] = s[i] == s[j]
else:
dp[i][j] = dp[i + 1][j - 1] and s[i] == s[j]
res = []
path = []
dfs(s, dp, res, path, 0)
return res
分割回文串 II
给你一个字符串 s, 请你将 s 分割成一些子串, 使每个子串都是回文. 返回符合要求的 最少分割次数. 示例:
输入: s = "aab"
输出: 1
解释: 只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串.
class Solution:
def minCut(self, s: str) -> int:
palindrome = [[False for _ in range(len(s))] for _ in range(len(s))]
for j in range(len(s)):
for i in range(j + 1):
if j - i < 2:
palindrome[i][j] = s[i] == s[j]
else:
palindrome[i][j] = palindrome[i + 1][j - 1] and s[i] == s[j]
dp = [i for i in range(len(s))]
for i in range(1, len(s)):
if palindrome[0][i]:
dp[i] = 0
continue
for j in range(i):
if palindrome[j + 1][i]:
dp[i] = min(dp[j] + 1, dp[i])
return dp[-1]
单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict, 判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词. 说明: 拆分时可以重复使用字典中的单词, 你可以假设字典中没有重复的单词. 示例:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple".
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
dp = [False for _ in range(len(s) + 1)]
dp[0] = True
for i in range(1, len(s) + 1):
for j in range(i):
dp[i] = dp[i] or (dp[j] and s[j:i] in wordDict)
return dp[-1]
单词拆分 II
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict, 在字符串中增加空格来构建一个句子, 使得句子中所有的单词都在词典中. 返回所有这些可能的句子. 说明: 分隔时可以重复使用字典中的单词, 你可以假设字典中没有重复的单词. 示例:
输入:
s = "pineapplepenapple"
wordDict = ["apple", "pen", "applepen", "pine", "pineapple"]
输出:
[
"pine apple pen apple",
"pineapple pen apple",
"pine applepen apple"
]
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
def hasBreak(s, wordDict):
dp = [False for _ in range(len(s) + 1)]
dp[0] = True
for i in range(1, len(s) + 1):
for j in range(i):
dp[i] = dp[i] or (dp[j] and s[j:i] in wordDict)
return dp[-1]
def dfs(s, wordDict, path, res, begin):
if begin == len(s):
res.append(' '.join(path[:]))
for word in wordDict:
if begin + len(word) > len(s) or s[begin:begin + len(word)] != word:
continue
path.append(word)
dfs(s, wordDict, path, res, begin + len(word))
path.pop()
res = []
path = []
if hasBreak(s, wordDict):
dfs(s, wordDict, path, res, 0)
return res
不同的二叉搜索树 II
给定一个整数 n, 生成所有由 1 ... n 为节点所组成的 二叉搜索树. 示例:
输入: 3
输出:
[
[1,null,3,2],
[3,2,null,1],
[3,1,null,null,2],
[2,1,3],
[1,null,2,null,3]
]
解释:
以上的输出对应以下 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
版本1 动态规划
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def generateTrees(self, n: int) -> List[TreeNode]:
if n == 0:
return []
# dp[i][j] 为 [(i+1)...(j+1)] 所组成二叉搜索树的根节点的列表
dp = [[[] for _ in range(n)] for _ in range(n)]
for i in range(n - 1, -1, -1):
for j in range(i, n):
if i == j:
dp[i][j].append(TreeNode(i + 1))
else:
for right in dp[i + 1][j]:
root = TreeNode(i + 1)
root.right = right
dp[i][j].append(root)
for left in dp[i][j - 1]:
root = TreeNode(j + 1)
root.left = left
dp[i][j].append(root)
for root_id in range(i + 1, j):
for left in dp[i][root_id - 1]:
for right in dp[root_id + 1][j]:
root = TreeNode(root_id + 1)
root.left = left
root.right = right
dp[i][j].append(root)
return dp[0][n - 1]
版本2 递归
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def generateTrees(self, n: int) -> List[TreeNode]:
def dfs(start, end):
if start > end:
return [None]
tree_list = []
for i in range(start, end + 1):
left_list = dfs(start, i - 1)
right_list = dfs(i + 1, end)
for left in left_list:
for right in right_list:
cur_root = TreeNode(i)
cur_root.left = left
cur_root.right = right
tree_list.append(cur_root)
return tree_list
return dfs(1, n) if n != 0 else []
不同的二叉搜索树
给定一个整数 n, 求以 1 ... n 为节点组成的二叉搜索树有多少种? 示例:
输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
class Solution:
def numTrees(self, n: int) -> int:
dp = [0 for _ in range(n + 1)]
dp[0], dp[1] = 1, 1
for i in range(2, n + 1):
for r in range(1, i + 1): # root
dp[i] += dp[r - 1] * dp[i - r]
return dp[-1]
分割等和子集
给定一个只包含正整数的非空数组. 是否可以将这个数组分割成两个子集, 使得两个子集的元素和相等. 示例:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
class Solution:
def canPartition(self, nums: List[int]) -> bool:
# 01背包-要求恰好取到背包容量
num_sum = sum(nums)
if num_sum % 2 == 1:
return False
target = int(num_sum / 2)
# dp[i][j]: 下标[0:i](包含)中是否存在和为 j 的子集
dp = [[False for _ in range(target + 1)] for _ in range(len(nums))]
if nums[0] <= target:
dp[0][nums[0]] = True
for i in range(1, len(nums)):
for j in range(0, target + 1):
dp[i][j] = dp[i - 1][j]
if nums[i] == j:
dp[i][j] = True
continue
if nums[i] < j:
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]
return dp[len(nums) - 1][target]
目标和
给定一个非负整数数组, a1, a2, ..., an, 和一个目标数 S. 现在你有两个符号 + 和 -. 对于数组中的任意一个整数, 你都可以从 + 或 - 中选择一个符号添加在前面. 返回可以使最终数组和为目标数 S 的所有添加符号的方法数. 示例:
输入: 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.
class Solution:
def findTargetSumWays(self, nums: List[int], S: int) -> int:
# 01背包-求方案数
num_sum = sum(nums)
if abs(S) > num_sum:
return 0
# 下标为[0:i](包含)的正负组合等于 j + num_sum 的方案数
dp = [[0 for _ in range(2 * num_sum + 1)] for _ in range(len(nums))]
dp[0][num_sum - nums[0]] = 1
dp[0][num_sum + nums[0]] += 1
for i in range(1, len(nums)):
for j in range(2 * num_sum + 1):
if j - nums[i] >= 0:
dp[i][j] += dp[i - 1][j - nums[i]]
if j + nums[i] <= 2 * num_sum:
dp[i][j] += dp[i - 1][j + nums[i]]
return dp[len(nums) - 1][num_sum + S]
零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount. 编写一个函数来计算可以凑成总金额所需的最少的硬币个数. 如果没有任何一种硬币组合能组成总金额, 返回 -1. 你可以认为每种硬币的数量是无限的. 示例:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
# 完全背包
dp = [float('inf') for _ in range(amount + 1)]
dp[0] = 0
for coin in coins:
for i in range(coin, amount + 1):
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[-1] if dp[-1] != float('inf') else -1
零钱兑换 II
给定不同面额的硬币和一个总金额. 写出函数来计算可以凑成总金额的硬币组合数. 假设每一种面额的硬币有无限个. 示例:
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
# 完全背包-求方案数
dp = [0 for _ in range(amount + 1)]
dp[0] = 1
for coin in coins:
for i in range(coin, amount + 1):
dp[i] += dp[i - coin]
return dp[-1]
一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n, 请找出并返回 strs 的最大子集的大小, 该子集中最多有 m 个 0 和 n 个 1. 如果 x 的所有元素也是 y 的元素, 集合 x 是集合 y 的 子集. 示例:
输入: strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出: 4
解释: 最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4. 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"}. {"111001"} 不满足题意, 因为它含 4 个 1, 大于 n 的值 3.
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
# 二维费用背包
dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
# dp[i][j]: 使用 i 个 0, j 个 1 的最大子集数
for s in strs:
zeros = s.count("0")
ones = s.count("1")
for i in range(m, zeros - 1, -1):
for j in range(n, ones - 1, -1):
dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1)
return dp[m][n]
最长公共子序列
给定两个字符串 text1 和 text2, 返回这两个字符串的最长公共子序列的长度. 如果不存在公共子序列, 返回 0. 一个字符串的子序列是指这样一个新的字符串: 它是由原字符串在不改变字符的相对顺序的情况下删除某些字符 (也可以不删除任何字符) 后组成的新字符串. 示例:
输入: text1 = "abcde", text2 = "ace"
输出: 3
解释: 最长公共子序列是 "ace", 它的长度为 3.
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m = min(len(text1), len(text2))
# dp[i][j]: text1[0:i-1](包含) 与 text2[0:j-1] 中公共子序列最长值
dp = [[0 for _ in range(len(text2) + 1)] for _ in range(len(text1) + 1)]
for i in range(1, len(text1) + 1):
for j in range(1, len(text2) + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[-1][-1]
正则表达式匹配
给你一个字符串 s 和一个字符规律 p, 请你来实现一个支持 '.' 和 '*' 的正则表达式匹配. 所谓匹配, 是要涵盖整个字符串 s 的, 而不是部分字符串.
'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素
示例:
输入: s = "aa" p = "a*"
输出: true
解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'. 因此, 字符串 "aa" 可被视为 'a' 重复了一次.
class Solution:
def isMatch(self, s: str, p: str) -> bool:
dp = [[False for _ in range(len(p) + 1)] for _ in range(len(s) + 1)]
dp[0][0] = True
# s='' 与 p='#*#*' 边界情况
for j in range(2, len(p) + 1, 2):
if p[j - 1] == '*':
dp[0][j] = True
else:
break
for i in range(1, len(s) + 1):
for j in range(1, len(p) + 1):
if s[i - 1] == p[j - 1] or p[j - 1] == '.':
dp[i][j] = dp[i - 1][j - 1]
elif p[j - 1] == '*':
# p[j-2] 能匹配 s[i-1]
if s[i - 1] == p[j - 2] or p[j - 2] == '.':
# p='a*' 匹配多字符 or 匹配单字符 or 匹配空字符
# dp[i][j - 1] 可省略, 转换为多字符与空字符形式:
# s='a',p='a*' -> s='',p='a*'
dp[i][j] = dp[i - 1][j] or dp[i][j - 2]
# p[j-2] 不能匹配 s[i-1], p[j-2] 取 0 个
elif s[i - 1] != p[j - 2]:
dp[i][j] = dp[i][j - 2]
return dp[-1][-1]
通配符匹配
给定一个字符串 (s) 和一个字符模式 (p), 实现一个支持 '?' 和 '*' 的通配符匹配. 两个字符串完全匹配才算匹配成功.
'?' 可以匹配任何单个字符.
'*' 可以匹配任意字符串 (包括空字符串).
说明:
s 可能为空, 且只包含从 a-z 的小写字母.
p 可能为空, 且只包含从 a-z 的小写字母, 以及字符 ? 和 *.
示例:
输入:
s = "adceb"
p = "*a*b"
输出: true
解释: 第一个 '*' 可以匹配空字符串, 第二个 '*' 可以匹配字符串 "dce".
class Solution:
def isMatch(self, s: str, p: str) -> bool:
dp = [[False for _ in range(len(p) + 1)] for _ in range(len(s) + 1)]
dp[0][0] = True
for i in range(1, len(p) + 1):
if p[i - 1] == '*':
dp[0][i] = True
else:
break
for i in range(1, len(s) + 1):
for j in range(1, len(p) + 1):
if s[i - 1] == p[j - 1] or p[j - 1] == '?':
dp[i][j] = dp[i - 1][j - 1]
elif p[j - 1] == '*':
dp[i][j] = dp[i - 1][j] or dp[i][j - 1]
return dp[-1][-1]
最长有效括号
给你一个只包含 '(' 和 ')' 的字符串, 找出最长有效 (格式正确且连续) 括号子串的长度. 示例:
输入: s = ")()())"
输出: 4
解释: 最长有效括号子串是 "()()"
版本1 动态规划
class Solution:
def longestValidParentheses(self, s: str) -> int:
dp = [0 for _ in range(len(s))]
res = 0
for i in range(1, len(s)):
if s[i] == ')' and s[i - 1] == '(':
dp[i] = dp[i - 2] + 2
elif s[i] == ')' and s[i - 1] == ')':
if i - dp[i - 1] - 1 >= 0 and s[i - dp[i - 1] - 1] == '(':
dp[i] = dp[i - 1] + dp[i - dp[i - 1] - 2] + 2
res = max(res, dp[i])
return res
版本2 栈
class Solution:
def longestValidParentheses(self, s: str) -> int:
res = 0
# 始终保证栈顶为当前遍历的最后一个没有被匹配的右括号
stack = [-1]
for i in range(len(s)):
if s[i] == '(':
stack.append(i)
else:
stack.pop()
if stack:
res = max(res, i - stack[-1])
else:
stack.append(i)
return res
接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图, 计算按此排列的柱子, 下雨之后能接多少雨水. 示例:
输入: height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
class Solution:
def trap(self, height: List[int]) -> int:
# 下标为 i 左/右最高高度
left_max = [0 for _ in range(len(height))]
right_max = [0 for _ in range(len(height))]
res = 0
for i in range(1, len(height)):
left_max[i] = max(left_max[i - 1], height[i - 1])
for i in range(len(height) - 2, -1, -1):
right_max[i] = max(right_max[i + 1], height[i + 1])
for i in range(len(height)):
h = min(left_max[i], right_max[i])
res += (h - height[i]) if h > height[i] else 0
return res
编辑距离
给你两个单词 word1 和 word2, 请你计算出将 word1 转换成 word2 所使用的最少操作数. 你可以对一个单词进行如下三种操作: 插入一个字符, 删除一个字符, 替换一个字符. 示例:
输入: word1 = "horse", word2 = "ros"
输出: 3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
dp = [[0 for _ in range(len(word2) + 1)] for _ in range(len(word1) + 1)]
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):
oper1 = dp[i][j - 1] + 1
oper2 = dp[i - 1][j] + 1
oper3 = dp[i - 1][j - 1] + 1 if word1[i - 1] != word2[j - 1] else dp[i - 1][j - 1]
dp[i][j] = min(oper1, oper2, oper3)
return dp[-1][-1]
扰乱字符串
使用下面描述的算法可以扰乱字符串 s 得到字符串 t:
如果字符串的长度为 1, 算法停止,
如果字符串的长度 > 1, 执行下述步骤:
在一个随机下标处将字符串分割成两个非空的子字符串. 即, 如果已知字符串 s, 则可以将其分成两个子字符串 x 和 y, 且满足 s = x + y.
随机决定是要交换两个子字符串还是要保持这两个子字符串的顺序不变. 即, 在执行这一步骤之后, s 可能是 s = x + y 或者 s = y + x.
在 x 和 y 这两个子字符串上继续从步骤 1 开始递归执行此算法.
给你两个长度相等的字符串 s1 和 s2, 判断 s2 是否是 s1 的扰乱字符串. 如果是, 返回 true, 否则返回 false. 示例:
输入: s1 = "great", s2 = "rgeat"
输出: true
解释: s1 上可能发生的一种情形是:
"great" --> "gr/eat" // 在一个随机下标处分割得到两个子字符串
"gr/eat" --> "gr/eat" // 随机决定: 保持这两个子字符串的顺序不变
"gr/eat" --> "g/r / e/at" // 在子字符串上递归执行此算法. 两个子字符串分别在随机下标处进行一轮分割
"g/r / e/at" --> "r/g / e/at" // 随机决定: 第一组交换两个子字符串, 第二组保持这两个子字符串的顺序不变
"r/g / e/at" --> "r/g / e/ a/t" // 继续递归执行此算法, 将 "at" 分割得到 "a/t"
"r/g / e/ a/t" --> "r/g / e/ a/t" // 随机决定: 保持这两个子字符串的顺序不变
算法终止, 结果字符串和 s2 相同, 都是 "rgeat", 这是一种能够扰乱 s1 得到 s2 的情形, 可以认为 s2 是 s1 的扰乱字符串, 返回 true.
版本1 动态规划
class Solution:
def isScramble(self, s1: str, s2: str) -> bool:
if len(s1) != len(s2):
return False
n = len(s1)
# dp[i][j][h]: s1以i开始长度为k的字符串是否可以变换为s2中以j开始长度为h的字符串
dp = [[[False for _ in range(n + 1)] for _ in range(n)] for _ in range(n)]
for i in range(n):
for j in range(n):
dp[i][j][1] = s1[i] == s2[j]
for h in range(2, n + 1):
for i in range(n - h + 1):
for j in range(n - h + 1):
for k in range(1, h):
# 保持顺序
if dp[i][j][k] and dp[i + k][j + k][h - k]:
dp[i][j][h] = True
break
# 交换顺序
if dp[i][j + h - k][k] and dp[i + k][j][h - k]:
dp[i][j][h] = True
break
return dp[0][0][-1]
版本2 递归
class Solution:
def isScramble(self, s1: str, s2: str) -> bool:
if len(s1) != len(s2):
return False
if s1 == s2:
return True
if sorted(s1) != sorted(s2):
return False
for i in range(1, len(s1)):
if (self.isScramble(s1[:i], s2[:i]) and self.isScramble(s1[i:], s2[i:])) or \
(self.isScramble(s1[:i], s2[-i:]) and self.isScramble(s1[i:], s2[:-i])):
return True
return False
交错字符串
给定三个字符串 s1, s2, s3, 请你帮忙验证 s3 是否是由 s1 和 s2 交错组成的. 两个字符串 s 和 t 交错的定义与过程如下, 其中每个字符串都会被分割成若干非空子字符串:
s = s1 + s2 + ... + sn
t = t1 + t2 + ... + tm
|n - m| <= 1
交错是 s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ...
示例:
输入: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出: true
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
if len(s1) + len(s2) != len(s3):
return False
dp = [[False for _ in range(len(s2) + 1)] for _ in range(len(s1) + 1)]
# dp[i][j]: s1[:i] 与 s2[:j] 交错组成 s3[:i+j] 项
dp[0][0] = True
for i in range(1, len(s1) + 1):
if s1[i - 1] == s3[i - 1]:
dp[i][0] = True
else:
break
for j in range(1, len(s2) + 1):
if s2[j - 1] == s3[j - 1]:
dp[0][j] = True
else:
break
for i in range(1, len(s1) + 1):
for j in range(1, len(s2) + 1):
dp[i][j] = (dp[i - 1][j] and s1[i - 1] == s3[i + j - 1]) or \
(dp[i][j - 1] and s2[j - 1] == s3[i + j - 1])
return dp[-1][-1]
不同的子序列
给定一个字符串 s 和一个字符串 t, 计算在 s 的子序列中 t 出现的个数. 字符串的一个子序列是指, 通过删除一些 (也可以不删除) 字符且不干扰剩余字符相对位置所组成的新字符串. 示例:
输入: s = "rabbbit", t = "rabbit"
输出: 3
解释:
有 3 种可以从 s 中得到 "rabbit" 的方案, 上箭头符号 ^ 表示选取的字母,
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^
class Solution:
def numDistinct(self, s: str, t: str) -> int:
dp = [[0 for _ in range(len(s) + 1)] for _ in range(len(t) + 1)]
# dp[i][j]: t[:i] 在 s[:j] 中不同获取方案数
for j in range(len(s) + 1):
dp[0][j] = 1
for i in range(1, len(t) + 1):
for j in range(i, len(s) + 1):
if t[i - 1] == s[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1]
else:
dp[i][j] = dp[i][j - 1]
return dp[-1][-1]

浙公网安备 33010602011771号