Leetcode 动态规划 2
题号直达: 198 打家劫舍 | 279 完全平方数 | 322 零钱兑换 | 139 单词拆分
LeetCode 198. 打家劫舍 ↑
题目
你是一个专业的小偷,计划沿着一条街道挨户偷窃。每间房都存放着一定现金;但相邻两间房屋装有相连的防盗系统,若同一晚上偷了两间相邻房屋,系统就会报警。
给定一个非负整数数组 nums,nums[i] 表示第 i 间房屋里的现金。求在这一晚上不触动报警装置的前提下,能偷到的最大金额。
测试样例
- 示例 1:
nums = [1,2,3,1]
输出:4
解释:偷窃 1 号屋(金额 1)与 3 号屋(金额 3),共4。 - 示例 2:
nums = [2,7,9,3,1]
输出:12
解释:偷窃 1、3、5 号屋,金额为2 + 9 + 1 = 12。
提示
1 <= nums.length <= 1000 <= nums[i] <= 400
代码(Python)
class Solution(object):
def rob(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
n = len(nums)
if n == 1:
return nums[0]
if n == 2:
return max(nums[0], nums[1])
dp = [0] * n
max_value = 0
dp[0] = nums[0]
dp[1] = nums[1]
for i in range(2, n):
max_value = max(max_value, dp[i - 2])
dp[i] = nums[i] + max_value
return max(dp[n - 1], dp[n - 2])
思路解析
若决定偷第 i 间,则不能偷第 i - 1 间,但可在 0 … i - 2 的最优结果上再累加 nums[i]。用 dp[i] 表示考虑前 i + 1 间房、且必偷第 i 间时能获得的最大金额,则有:
dp[i] = nums[i] + max(dp[0], … , dp[i - 2])
n = 1、n = 2 直接返回。从 i = 2 起遍历时,用变量 max_value 滚动维护 max(dp[0], …, dp[i - 2]),避免在内层再扫一遍。最终答案不一定偷最后一间,因此返回 max(dp[n - 1], dp[n - 2])。
- 时间复杂度:O(n)
- 空间复杂度:O(n)(
dp数组)
LeetCode 279. 完全平方数 ↑
题目
给你一个整数 n,返回和为 n 的完全平方数的最少个数。
完全平方数:等于某个整数的平方的数(例如 1、4、9、16 是,3、11 不是)。
测试样例
- 示例 1:
n = 12
输出:3
解释:12 = 4 + 4 + 4(三个完全平方数)。 - 示例 2:
n = 13
输出:2
解释:13 = 4 + 9。
提示
1 <= n <= 10^4
代码(Python)解法 1:记忆化 DFS
class Solution(object):
def numSquares(self, n):
"""
:type n: int
:rtype: int
"""
memory = {}
memory[0] = 0
memory[1] = 1
def dfs(k):
if k in memory:
return memory[k]
else:
max_num = int(k ** 0.5)
min_count = float('inf')
for i in range(1, max_num + 1):
min_count = min(min_count, dfs(k - i ** 2) + 1)
memory[k] = min_count
return min_count
return dfs(n)
思路解析(解法 1)
把问题看成:最后一次减去的完全平方数是 i²,则 k = i² + (k - i²),最少个数为 1 +(k - i² 的最少个数)。枚举 i = 1 … ⌊√k⌋ 取最小值即可。
memory[k] 记录 dfs(k) 的结果,避免重复子问题;边界 memory[0]=0、memory[1]=1(与递归终止一致)。
- 时间复杂度:每个
k最多计算一次,每次 O(√k) 枚举;总体约 O(n√n)。 - 空间复杂度:记忆化表与递归栈,O(n)。
代码(Python)解法 2:数学(四平方和 / 三平方定理)
class Solution(object):
def numSquares(self, n):
"""
:type n: int
:rtype: int
"""
m = n
while m % 4 == 0:
m //= 4
if m % 8 == 7:
return 4
r = int(n ** 0.5)
if r * r == n:
return 1
for a in range(1, r + 1):
b = n - a * a
t = int(b ** 0.5)
if t * t == b:
return 2
return 3
思路解析(解法 2)
方法二:数学(与上图一致)
- 四平方和定理(拉格朗日):任意正整数至多可表成四个完全平方数之和,故答案上界为 4。
- 三平方和定理(勒让德):正整数
n能表成至多三个完全平方数之和,当且仅当n不能写成4^k × (8m + 7)的形式(k,m为非负整数)。反之,若经不断除以4后得到的数满足≡ 7 (mod 8),则必须用四个完全平方数,可直接返回 4。 - 其余情况答案只能是 1、2 或 3:
- 1:
n本身就是完全平方数; - 2:存在
a使得n - a²仍为完全平方数(枚举1 ≤ a ≤ √n); - 3:若以上及「必为 4」都不成立,由排除法知答案为 3。
- 1:
实现时先对 n 反复除以 4 做上述 8m+7 判定,再依次判断 1、2,最后返回 3。
- 时间复杂度:O(√n)(除以
4与枚举a均为√n量级)。 - 空间复杂度:O(1)。
LeetCode 322. 零钱兑换 ↑
题目
给定整数数组 coins 表示不同面额的硬币,每种硬币数量无限。另给定整数 amount 表示要凑出的总金额。求凑成该金额所需的最少硬币个数;若不能凑出,返回 -1。
可以认为 amount == 0 时需要 0 枚硬币。
测试样例
- 示例 1:
coins = [1, 2, 5],amount = 11
输出:3
解释:11 = 5 + 5 + 1。 - 示例 2:
coins = [2],amount = 3
输出:-1 - 示例 3:
coins = [1],amount = 0
输出:0
提示
1 <= coins.length <= 121 <= coins[i] <= 2^31 - 10 <= amount <= 10^4
代码(Python):记忆化 DFS
class Solution(object):
def coinChange(self, coins, amount):
"""
:type coins: List[int]
:type amount: int
:rtype: int
"""
if amount == 0:
return 0
memory = {}
memory[0] = 0
def dfs(k):
if k in memory:
return memory[k]
else:
min_count = 10 ** 4 + 1
for i in range(len(coins)):
if k - coins[i] >= 0:
min_count = min(min_count, dfs(k - coins[i]) + 1)
memory[k] = min_count
return min_count
res = dfs(amount)
if res == 10 ** 4 + 1:
return -1
return res
思路解析
dfs(k) 表示凑出面额 k 的最少硬币数,边界 memory[0] = 0。转移时枚举最后一枚硬币 coins[i],则需 1 + dfs(k - coins[i])。题目保证 amount ≤ 10^4,用 10 ** 4 + 1 表示「无解」意义上的无穷大。
边界:必须判断 k - coins[i] >= 0(或先判断 coins[i] <= k)。若不加限制,k - coins[i] 会为负,递归仍可能套用公式得到偏小的错误答案;只有在剩余面额非负时才进入子问题。
最后若 dfs(amount) 仍为哨兵值,说明无法凑出,返回 -1。
- 时间复杂度:O(amount × len(coins))(每个
k至多算一次,内层枚举硬币)。 - 空间复杂度:O(amount)(哈希表与递归栈)。
LeetCode 139. 单词拆分 ↑
题目
给定字符串 s 和字符串数组 wordDict 作为词典。若 s 能拆成一个或多个词典中出现的单词首尾相接(单词之间以隐含的空格概念分界),则返回 true;否则返回 false。
说明:不必用完词典里所有词;同一个词典词可以重复使用。
测试样例
- 示例 1:
s = "leetcode",wordDict = ["leet", "code"]
输出:true
解释:"leet"+"code"。 - 示例 2:
s = "applepenapple",wordDict = ["apple", "pen"]
输出:true
解释:"apple"+"pen"+"apple"("apple"可重复用)。 - 示例 3:
s = "catsandog",wordDict = ["cats", "dog", "sand", "and", "cat"]
输出:false
提示
1 <= s.length <= 3001 <= wordDict.length <= 10001 <= wordDict[i].length <= 20s与wordDict[i]仅由英文小写字母组成
代码(Python)
class Solution(object):
def wordBreak(self, s, wordDict):
"""
:type s: str
:type wordDict: List[str]
:rtype: bool
"""
word_set = 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 word_set:
dp[i] = True
break
return dp[n]
思路解析
dp[i] 表示 s 的前 i 个字符(即 s[0:i])能否被合法拆分。空串 dp[0] = True 作为递推起点。
枚举长度 i 与上一段的切分点 j:若 dp[j] 为真,且 s[j:i] 落在词典里,则 dp[i] = True。一旦发现可行可提前 break。
- 时间复杂度:O(n³) 量级(
n = len(s),双重循环 O(n²),取子串s[j:i]并做集合判断与长度线性相关;本题n ≤ 300可接受)。 - 空间复杂度:O(n)(
dp数组)及词典set的空间。

浙公网安备 33010602011771号