动态规划标准答案¶
记忆核心:先说 dp[i] 表示什么,再写从更小状态怎么转移过来。背包题先问“每个物品能不能重复用”。
5 最长回文子串¶
- 题目:找字符串中最长的连续回文子串。
- 思路:中心扩展。每个位置都试奇数中心和偶数中心。
- 标准答案:
class Solution:
def longestPalindrome(self, s):
ans = ''
def expand(l, r):
while l >= 0 and r < len(s) and s[l] == s[r]:
l -= 1
r += 1
return s[l + 1:r]
for i in range(len(s)):
a = expand(i, i)
b = expand(i, i + 1)
if len(a) > len(ans):
ans = a
if len(b) > len(ans):
ans = b
return ans
- 小坑:扩展停止后左右各多走了一步,所以返回
s[l + 1:r]。
32 最长有效括号¶
- 题目:给一串括号,找最长的合法连续括号子串长度。
- 思路:栈存未匹配位置。栈底放
-1当哨兵,方便计算长度。 - 标准答案:
class Solution:
def longestValidParentheses(self, s):
stack = [-1]
ans = 0
for i, ch in enumerate(s):
if ch == '(':
stack.append(i)
else:
stack.pop()
if not stack:
stack.append(i)
else:
ans = max(ans, i - stack[-1])
return ans
- 小坑:栈空时把当前右括号下标放进去,作为新的断点。
62 不同路径¶
- 题目:机器人从左上角走到右下角,只能向右或向下,问有多少条路径。
- 思路:
dp[j]表示当前行第j列的路径数,来自上方和左方。 - 标准答案:
class Solution:
def uniquePaths(self, m, n):
dp = [1] * n
for _ in range(1, m):
for j in range(1, n):
dp[j] += dp[j - 1]
return dp[-1]
- 小坑:第一行和第一列都只有一种走法。
64 最小路径和¶
- 题目:从左上到右下,只能向右或向下,路径经过数字之和最小是多少。
- 思路:
dp[j]表示到当前行第j列的最小路径和。 - 标准答案:
class Solution:
def minPathSum(self, grid):
m, n = len(grid), len(grid[0])
dp = [float('inf')] * n
dp[0] = 0
for i in range(m):
for j in range(n):
if j == 0:
dp[j] = dp[j] + grid[i][j]
else:
dp[j] = min(dp[j], dp[j - 1]) + grid[i][j]
return dp[-1]
- 小坑:
dp[j]是从上面来,dp[j-1]是从左边来。
70 爬楼梯¶
- 题目:每次爬 1 或 2 阶,爬到第
n阶有多少种方法。 - 思路:到第
i阶,可以从i-1或i-2来。 - 标准答案:
- 小坑:这是斐波那契;
n=1返回 1,n=2返回 2。
72 编辑距离¶
- 题目:把
word1变成word2,每次可以插入、删除、替换一个字符,求最少操作数。 - 思路:
dp[i][j]表示word1[:i]变成word2[:j]的最少操作。 - 标准答案:
class Solution:
def minDistance(self, word1, word2):
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 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],
dp[i][j - 1],
dp[i - 1][j - 1]
) + 1
return dp[m][n]
- 小坑:空串变长度为
i的串,需要i次操作;下标字符是word1[i-1]。
118 杨辉三角¶
- 题目:生成前
numRows行杨辉三角。 - 思路:每行两边是 1,中间位置等于上一行相邻两个数之和。
- 标准答案:
class Solution:
def generate(self, numRows):
ans = []
for i in range(numRows):
row = [1] * (i + 1)
for j in range(1, i):
row[j] = ans[i - 1][j - 1] + ans[i - 1][j]
ans.append(row)
return ans
- 小坑:中间循环是
range(1, i),两端不用改。
139 单词拆分¶
- 题目:判断字符串能否被字典里的单词拼出来。
- 思路:
dp[i]表示s[:i]能否拼出;枚举最后一刀j。 - 标准答案:
class Solution:
def wordBreak(self, s, wordDict):
words = set(wordDict)
n = len(s)
dp = [False] * (n + 1)
dp[0] = True
for i in range(1, n + 1):
for j in range(i):
if dp[j] and s[j:i] in words:
dp[i] = True
break
return dp[n]
- 小坑:
s[j:i]是切片,不是s[j, i]。
152 乘积最大子数组¶
- 题目:找连续子数组,使乘积最大。
- 思路:负数会让最大变最小、最小变最大,所以同时维护当前位置结尾的最大/最小乘积。
- 标准答案:
class Solution:
def maxProduct(self, nums):
max_prod = min_prod = ans = nums[0]
for x in nums[1:]:
a = max_prod * x
b = min_prod * x
max_prod = max(x, a, b)
min_prod = min(x, a, b)
ans = max(ans, max_prod)
return ans
- 小坑:候选必须包含
x自己,表示从当前位置重新开始。
198 打家劫舍¶
- 题目:一排房子不能偷相邻两家,求最多能偷多少钱。
- 思路:每家只有偷或不偷。滚动维护前两个状态。
- 标准答案:
class Solution:
def rob(self, nums):
prev2 = 0
prev1 = 0
for x in nums:
cur = max(prev1, prev2 + x)
prev2 = prev1
prev1 = cur
return prev1
- 小坑:
prev2 + x是偷当前家,prev1是不偷当前家。
279 完全平方数¶
- 题目:把
n拆成若干完全平方数之和,求最少需要几个。 - 思路:
dp[i]表示凑出i的最少数量;枚举最后一个平方数j*j。 - 标准答案:
class Solution:
def numSquares(self, n):
dp = [float('inf')] * (n + 1)
dp[0] = 0
for i in range(1, n + 1):
j = 1
while j * j <= i:
dp[i] = min(dp[i], dp[i - j * j] + 1)
j += 1
return dp[n]
- 小坑:
dp[0] = 0是所有转移的起点。
300 最长递增子序列¶
- 题目:找数组中最长严格递增子序列的长度,不要求连续。
- 思路:
tails[i]表示长度为i+1的递增子序列的最小尾巴,用二分维护。 - 标准答案:
import bisect
class Solution:
def lengthOfLIS(self, nums):
tails = []
for x in nums:
i = bisect.bisect_left(tails, x)
if i == len(tails):
tails.append(x)
else:
tails[i] = x
return len(tails)
- 小坑:严格递增用
bisect_left,相等时替换原位置。
322 零钱兑换¶
- 题目:给硬币面额和总金额,求凑出金额的最少硬币数;凑不出返回 -1。
- 思路:完全背包。每种硬币可以无限用,所以金额正序更新。
- 标准答案:
class Solution:
def coinChange(self, coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
for j in range(coin, amount + 1):
dp[j] = min(dp[j], dp[j - coin] + 1)
return -1 if dp[amount] == float('inf') else dp[amount]
- 小坑:硬币可重复使用,所以内层金额正序。
416 分割等和子集¶
- 题目:判断数组能不能分成两个子集,使两个子集和相等。
- 思路:总和必须是偶数;问题变成能否用每个数最多一次凑出一半。
- 标准答案:
class Solution:
def canPartition(self, nums):
total = sum(nums)
if total % 2 == 1:
return False
target = total // 2
dp = [False] * (target + 1)
dp[0] = True
for x in nums:
for j in range(target, x - 1, -1):
dp[j] = dp[j] or dp[j - x]
return dp[target]
- 小坑:每个数只能用一次,所以
j必须倒序。
1143 最长公共子序列¶
- 题目:给两个字符串,找最长公共子序列长度;子序列可以不连续,但相对顺序不能变。
- 思路:
dp[i][j]表示text1[:i]和text2[:j]的最长公共子序列长度。 - 标准答案:
class Solution:
def longestCommonSubsequence(self, text1, text2):
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 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[m][n]
- 小坑:
dp下标是长度,字符下标要减一。