1. 为什么动态规划和图遍历是面试必考算法
在技术面试中,动态规划和图遍历算法出现的频率高得惊人。根据我参与过的数百场面试统计,这两类问题占比超过算法题的60%。为什么它们如此受面试官青睐?
动态规划考察的是将复杂问题分解为子问题的能力,这直接反映了工程师解决实际业务问题的思维模式。比如电商平台的库存优化、物流路径规划等场景,都需要这种分阶段决策的思维方式。而图遍历算法(DFS/BFS)则是处理树形结构、网络拓扑等数据的基石,从社交网络的好友关系到分布式系统的节点通信,无处不在。
我清楚地记得去年面试一位候选人时,给他出了一道动态规划的中等难度题。他在白板上画出了状态转移方程后突然停下来说:"等等,这个递推关系有问题..."然后花了5分钟重新推导。最后他不仅给出了正确解法,还详细说明了第一次的错误原因。这种对算法本质的理解,远比死记硬背代码更有价值。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动态规划的系统化解题框架
2.1 识别动态规划问题的三大特征
不是所有问题都适合用动态规划解决。通过这三个特征可以快速判断:
-
最优子结构:问题的最优解包含子问题的最优解。比如背包问题中,前i个物品的最优解必然包含前i-1个物品的最优解。
-
重叠子问题:递归求解时会重复计算相同的子问题。斐波那契数列就是典型例子,fib(5)计算时需要多次计算fib(3)。
-
无后效性:当前状态一旦确定,后续决策不受之前决策影响。股票买卖问题中,第i天的决策只与当前持有状态有关,与之前哪天买入无关。
常见误区:很多初学者会把所有递归问题都当作动态规划,实际上像汉诺塔问题虽然有递归但不具备重叠子问题特征。
2.2 动态规划的四种经典实现方式
2.2.1 自顶向下备忘录法
python复制def fib(n, memo={}):
if n in memo: return memo[n]
if n <= 2: return 1
memo[n] = fib(n-1, memo) + fib(n-2, memo)
return memo[n]
这种方法直接对应递归思路,加入哈希表存储已计算结果。适合问题边界清晰但状态转移复杂的场景。
2.2.2 自底向上迭代法
python复制def fib(n):
if n <= 2: return 1
dp = [0]*(n+1)
dp[1] = dp[2] = 1
for i in range(3, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
更符合动态规划本质的实现,通常效率更高。适合状态转移简单但需要优化空间的情况。
2.2.3 状态压缩技巧
当当前状态只与前几个状态相关时,可以压缩DP数组:
python复制def fib(n):
if n <= 2: return 1
prev, curr = 1, 1
for _ in range(3, n+1):
prev, curr = curr, prev + curr
return curr
将空间复杂度从O(n)降到O(1)。适用于斐波那契、爬楼梯等问题。
2.2.4 带路径记录的DP
有些问题需要还原具体解而不仅是数值:
python复制def longest_increasing_subsequence(nums):
dp = [1]*len(nums)
path = [[] for _ in range(len(nums))]
for i in range(1, len(nums)):
for j in range(i):
if nums[j] < nums[i] and dp[j]+1 > dp[i]:
dp[i] = dp[j]+1
path[i] = path[j] + [nums[j]]
path[i].append(nums[i])
max_len = max(dp)
return path[dp.index(max_len)]
这种实现额外维护了path数组,在面试中能展现更全面的思考。
2.3 高频动态规划题型精讲
2.3.1 背包问题变种
- 0-1背包:物品不可分割
- 完全背包:物品无限取用
- 多维背包:多个约束条件
python复制# 0-1背包标准实现
def knapsack(weights, values, capacity):
n = len(weights)
dp = [[0]*(capacity+1) for _ in range(n+1)]
for i in range(1, n+1):
for w in range(1, capacity+1):
if weights[i-1] <= w:
dp[i][w] = max(dp[i-1][w],
dp[i-1][w-weights[i-1]] + values[i-1])
else:
dp[i][w] = dp[i-1][w]
return dp[n][capacity]
2.3.2 字符串编辑距离
python复制def minDistance(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] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
return dp[m][n]
2.3.3 股票买卖问题
python复制def maxProfit(prices):
n = len(prices)
dp = [[0]*2 for _ in range(n+1)]
dp[0][1] = -float('inf')
for i in range(1, n+1):
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])
return dp[n][0]
3. 图遍历算法的核心要点
3.1 DFS与BFS的选择策略
| 特性 | DFS | BFS |
|---|---|---|
| 数据结构 | 栈 | 队列 |
| 空间复杂度 | O(h) | O(w) |
| 适用场景 | 寻找所有解、连通性 | 最短路径、层级遍历 |
| 实现方式 | 递归/显式栈 | 迭代 |
选择原则:
- 当需要找出所有可能解时(如排列组合),优先DFS
- 当需要找最短路径或最近节点时(如社交网络好友推荐),用BFS
3.2 图的表示方法与转换
3.2.1 邻接矩阵
python复制# 无向图示例
graph = [
[0, 1, 1, 0],
[1, 0, 1, 1],
[1, 1, 0, 0],
[0, 1, 0, 0]
]
适合稠密图,空间复杂度O(V²),查询两点是否相邻只需O(1)。
3.2.2 邻接表
python复制graph = {
0: [1, 2],
1: [0, 2, 3],
2: [0, 1],
3: [1]
}
适合稀疏图,空间复杂度O(V+E),能高效遍历邻接节点。
3.2.3 边列表
python复制edges = [
(0, 1), (0, 2),
(1, 2), (1, 3)
]
某些算法(如Kruskal)需要这种表示,但通常需要转换为其他形式处理。
3.3 经典图算法实现
3.3.1 DFS递归实现
python复制def dfs_recursive(graph, node, visited=None):
if visited is None: visited = set()
visited.add(node)
print(node) # 处理节点
for neighbor in graph[node]:
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited)
3.3.2 DFS迭代实现
python复制def dfs_iterative(graph, start):
visited = set()
stack = [start]
while stack:
node = stack.pop()
if node not in visited:
visited.add(node)
print(node) # 处理节点
# 逆序压栈保证顺序一致
stack.extend(reversed(graph[node]))
3.3.3 BFS标准实现
python复制from collections import deque
def bfs(graph, start):
visited = set()
queue = deque([start])
while queue:
node = queue.popleft()
if node not in visited:
visited.add(node)
print(node) # 处理节点
queue.extend(graph[node])
3.3.4 拓扑排序(Kahn算法)
python复制def topological_sort(graph):
in_degree = {u:0 for u in graph}
for u in graph:
for v in graph[u]:
in_degree[v] += 1
queue = deque([u for u in graph if in_degree[u] == 0])
topo_order = []
while queue:
u = queue.popleft()
topo_order.append(u)
for v in graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
queue.append(v)
if len(topo_order) == len(graph):
return topo_order
else: # 存在环
return []
4. 面试实战技巧与高频考题解析
4.1 动态规划问题拆解五步法
- 定义状态:明确dp[i]或dp[i][j]表示什么
- 建立转移方程:找出状态间的关系式
- 确定初始条件:设置边界情况的初始值
- 计算顺序:确定填表顺序(自底向上/自顶向下)
- 空间优化:考虑是否可压缩状态维度
以"最长递增子序列"为例:
python复制def lengthOfLIS(nums):
if not nums: return 0
dp = [1]*len(nums) # 步骤1:dp[i]表示以nums[i]结尾的LIS长度
for i in range(1, len(nums)): # 步骤4:从左到右计算
for j in range(i): # 步骤2:状态转移
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j]+1)
return max(dp) # 结果是dp数组中的最大值
4.2 图遍历问题的三个思考维度
- 图的构建:如何将问题抽象为图结构
- 节点表示什么?
- 边表示什么关系?
- 遍历方式:DFS/BFS的选择依据
- 是否需要最短路径?
- 是否需要遍历所有可能性?
- 状态记录:避免重复访问的机制
- 使用visited集合
- 对树状结构可不记录(无环)
例题:单词接龙
python复制from collections import deque
def ladderLength(beginWord, endWord, wordList):
wordSet = set(wordList)
if endWord not in wordSet: return 0
queue = deque([(beginWord, 1)])
visited = set()
while queue:
word, length = queue.popleft()
if word == endWord:
return length
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
next_word = word[:i] + c + word[i+1:]
if next_word in wordSet and next_word not in visited:
visited.add(next_word)
queue.append((next_word, length+1))
return 0
4.3 十个必刷高频面试题
-
动态规划:
- 爬楼梯(空间优化版)
- 零钱兑换(完全背包)
- 最长公共子序列
- 编辑距离
- 打家劫舍(状态机DP)
-
图遍历:
- 岛屿数量(连通分量)
- 课程表(拓扑排序)
- 单词搜索(回溯+DFS)
- 克隆图(哈希表+BFS)
- 二叉树右视图(层级BFS)
以"岛屿数量"为例的DFS解法:
python复制def numIslands(grid):
if not grid: return 0
count = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == '1':
count += 1
self.dfs(grid, i, j)
return count
def dfs(grid, i, j):
if i<0 or j<0 or i>=len(grid) or j>=len(grid[0]) or grid[i][j]!='1':
return
grid[i][j] = '0' # 标记为已访问
dfs(grid, i+1, j)
dfs(grid, i-1, j)
dfs(grid, i, j+1)
dfs(grid, i, j-1)
4.4 面试中的五个常见失误
-
过早优化:一上来就考虑状态压缩,反而让代码难以理解。应该先写出清晰的标准DP,再讨论优化。
-
边界处理不当:忘记初始化dp[0]或处理空输入等特殊情况。建议先写出测试用例。
-
混淆遍历顺序:在二维DP中搞错行优先还是列优先遍历。画图辅助理解状态依赖关系。
-
visited记录错误:在图遍历中错误共享visited集合,导致跳过有效路径。每个分支可能需要独立的visited。
-
过度依赖递归:DFS全部用递归实现,遇到大深度图时栈溢出。掌握迭代写法是必要的。
