背包问题是DP的王冠——理解它,就理解了资源分配问题的本质
背包问题是DP最经典的模型族:给定容量为W的背包和n个物品,选择物品使总价值最大(或满足特定条件)。核心区别在于物品的选取方式。背包问题是理解"资源分配"类问题的钥匙。
# 01背包:n个物品,容量W,重量w[i],价值v[i]
# 一维优化: 逆序遍历容量
def knapsack_01(n, W, w, v):
dp = [0] * (W + 1)
for i in range(n):
for j in range(W, w[i] - 1, -1): # 逆序!
dp[j] = max(dp[j], dp[j - w[i]] + v[i])
return dp[W]
# 二维版本(更直观)
def knapsack_01_2d(n, W, w, v):
dp = [[0] * (W + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(W + 1):
dp[i][j] = dp[i-1][j] # 不选第i个
if j >= w[i-1]:
dp[i][j] = max(dp[i][j], dp[i-1][j-w[i-1]] + v[i-1])
return dp[n][W]
# 完全背包:每种物品可无限选取
def knapsack_complete(n, W, w, v):
dp = [0] * (W + 1)
for i in range(n):
for j in range(w[i], W + 1): # 正序!
dp[j] = max(dp[j], dp[j - w[i]] + v[i])
return dp[W]
给定正整数数组,判断能否分成两个子集使得两个子集的元素和相等。
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total % 2: # 奇数不可能等分
return False
target = total // 2
dp = [False] * (target + 1)
dp[0] = True # 容量0一定可以凑出
for num in nums:
for j in range(target, num - 1, -1): # 逆序!
dp[j] = dp[j] or dp[j - num]
return dp[target]
时间: O(n * target) 空间: O(target)
给定不同面额的硬币和总金额,计算凑成总金额所需的最少硬币数。每种硬币可无限使用。
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
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 dp[amount] if dp[amount] != float('inf') else -1
时间: O(n * amount) 空间: O(amount)
给定不同面额的硬币和总金额,计算凑成总金额的组合数。每种硬币可无限使用。
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
dp = [0] * (amount + 1)
dp[0] = 1
for coin in coins:
for j in range(coin, amount + 1):
dp[j] += dp[j - coin]
return dp[amount]
时间: O(n * amount) 空间: O(amount)
在实际面试中,算法题不仅仅是写代码,更考察沟通、分析和解决问题的能力。以下是一些实战技巧:
# 通用解题模板
class Solution:
def solve(self, input_data):
# 1. 边界检查
if not input_data:
return default_value
# 2. 初始化
result = initial_value
state = initial_state
# 3. 主循环
for item in input_data:
state = transition(state, item)
result = max/min/update(result, state)
# 4. 返回结果
return result
每道经典题目都有丰富的变体。掌握核心模板后,灵活应对变体才是面试的真正挑战。以下列举本课相关的高频变体和扩展思路。
# 面试沟通模板
# 1. 先说思路(1-2分钟)
"这道题我想到两种解法,先说更直观的一种..."
# 2. 分析复杂度(30秒)
"时间复杂度O(n^2),空间可以优化到O(n)"
# 3. 边写边说
"这里我定义dp[i]表示...,转移方程是..."
# 4. 主动验证
"我用示例走一遍:dp[0]=..., dp[1]=..."
# 5. 讨论优化
"空间还可以进一步优化,因为dp[i]只依赖..."
| 检查项 | 常见错误 |
|---|---|
| 初始条件 | dp[0]是否正确?空输入是否处理? |
| 遍历范围 | 循环从0还是1开始?是否越界? |
| 状态转移 | 依赖的状态是否已计算? |
| 返回值 | 是dp[n]还是max(dp)? |
| 边界情况 | 单元素、全相同、空数组 |
背包问题的变体众多,以下速查表帮助你快速定位题目类型和解法。
| 问题 | 背包类型 | dp含义 | 遍历方向 |
|---|---|---|---|
| 分割等和子集 | 01背包 | dp[j]=能否凑出j | 逆序 |
| 目标和 | 01背包 | dp[j]=凑出j的方案数 | 逆序 |
| 最后一块石头II | 01背包 | dp[j]=能否凑出j | 逆序 |
| 零钱兑换 | 完全背包 | dp[j]=最少硬币数 | 正序 |
| 零钱兑换II | 完全背包 | dp[j]=组合数 | 正序 |
| 组合总和IV | 完全背包 | dp[j]=排列数 | 先容量后物品 |
| 完全平方数 | 完全背包 | dp[j]=最少个数 | 正序 |
| 单词拆分 | 完全背包 | dp[j]=能否拼出 | 正序 |