第8章 高级算法
本章将介绍几种高级算法设计技术,包括贪心算法、回溯算法和分治算法。这些算法思想在解决复杂问题时各有特点,掌握它们可以大大扩展解决问题的能力。
8.1 贪心算法
Activity Selection

Activity Selection Animation
贪心算法是一种简单而高效的算法设计技术。它在每一步选择中都采取当前状态下的最优选择,希望通过局部最优达到全局最优。贪心算法不保证得到最优解,但在很多问题中可以得到最优解或接近最优解的解。
贪心算法适用于具有贪心选择性质的问题。贪心选择性质是指通过局部最优选择可以达到全局最优。贪心算法也要求问题具有最优子结构性质,即问题的最优解包含子问题的最优解。
def activity_selection(starts, ends):
n = len(starts)
activities = list(zip(starts, ends))
activities.sort(key=lambda x: x[1])
selected = [activities[0]]
last_end = activities[0][1]
for i in range(1, n):
if activities[i][0] >= last_end:
selected.append(activities[i])
last_end = activities[i][1]
return selected
starts = [1, 3, 0, 5, 8, 5]
ends = [2, 4, 6, 7, 9, 9]
result = activity_selection(starts, ends)
print("选择的活动:")
for i, (s, e) in enumerate(result):
print(f"活动{i+1}: 开始时间{s}, 结束时间{e}")
活动选择问题是贪心算法的经典应用。给定一系列活动,每个活动有开始时间和结束时间,要求选择尽可能多的活动,使得它们不冲突。贪心策略是每次选择结束时间最早的活动,这样可以为后续活动留下最多的时间。
def fractional_knapsack(weights, values, capacity):
n = len(weights)
items = [(values[i] / weights[i], weights[i], values[i]) for i in range(n)]
items.sort(reverse=True)
total_value = 0
remaining_capacity = capacity
for ratio, weight, value in items:
if remaining_capacity >= weight:
total_value += value
remaining_capacity -= weight
else:
fraction = remaining_capacity / weight
total_value += value * fraction
break
return total_value
weights = [10, 20, 30]
values = [60, 100, 120]
capacity = 50
max_value = fractional_knapsack(weights, values, capacity)
print(f"分数背包问题最大价值: {max_value}")
分数背包问题允许物品分割,可以使用贪心算法求解。每次选择性价比最高的物品,优先装入背包。这与0-1背包问题不同,0-1背包问题需要动态规划求解。
最小生成树问题也可以使用贪心算法求解。Prim算法从一个顶点开始,每次选择连接已选顶点和未选顶点的最小权边。Kruskal算法按边权从小到大依次选择,保证不形成环。
def prim_mst(graph):
n = len(graph)
selected = [False] * n
mst = []
selected[0] = True
edges_in_mst = 0
while edges_in_mst < n - 1:
min_weight = float('inf')
u, v = -1, -1
for i in range(n):
if selected[i]:
for j in range(n):
ifnot selected[j] and graph[i][j] > 0:
if graph[i][j] < min_weight:
min_weight = graph[i][j]
u, v = i, j
if u != -1:
selected[v] = True
mst.append((u, v, min_weight))
edges_in_mst += 1
return mst
8.2 回溯算法
N-Queens

N-Queens Animation
回溯算法是一种系统地搜索问题所有解的方法。它通过探索所有可能的候选解来找出所有解,如果候选解不满足约束条件,则回溯到上一步尝试其他候选。回溯算法适用于组合优化问题,如排列、组合、子集等。
def n_queens(n):
def is_safe(board, row, col):
for i in range(row):
if board[i] == col or \
abs(board[i] - col) == abs(i - row):
returnFalse
returnTrue
def solve(board, row):
if row == n:
solutions.append(board.copy())
return
for col in range(n):
if is_safe(board, row, col):
board[row] = col
solve(board, row + 1)
board[row] = -1
solutions = []
solve([-1] * n, 0)
return solutions
def print_queens_board(solution):
n = len(solution)
for row in range(n):
line = ''
for col in range(n):
if solution[row] == col:
line += 'Q '
else:
line += '. '
print(line)
print()
solutions = n_queens(4)
print(f"4皇后问题共有{len(solutions)}种解法:")
for i, sol in enumerate(solutions[:2]):
print(f"解法{i+1}:")
print_queens_board(sol)
N皇后问题要求在n×n棋盘上放置n个皇后,使得她们互不攻击。皇后可以攻击同一行、同一列和同一对角线上的其他皇后。回溯算法逐行放置皇后,每次放置时检查是否与已放置皇后冲突。
def subset_sum(nums, target):
def backtrack(start, current_sum, path):
if current_sum == target:
result.append(path.copy())
return
for i in range(start, len(nums)):
if current_sum + nums[i] <= target:
path.append(nums[i])
backtrack(i + 1, current_sum + nums[i], path)
path.pop()
result = []
backtrack(0, 0, [])
return result
nums = [2, 3, 6, 7]
target = 7
solutions = subset_sum(nums, target)
print(f"子集和问题: 从{nums}中选择元素和为{target}")
for sol in solutions:
print(sol)
子集和问题要求从数组中选择元素,使得它们的和等于目标值。回溯算法尝试每种可能的组合,当当前和超过目标时剪枝,避免不必要的搜索。
回溯算法的时间复杂度通常是指数级,因为需要探索所有可能的解。但通过剪枝可以大大减少搜索空间。剪枝策略包括约束剪枝和优化剪枝。约束剪枝在候选解不满足约束条件时提前终止。优化剪枝在当前解不可能优于已知最优解时提前终止。
8.3 分治算法
分治算法将复杂问题分解为相似的子问题,递归求解子问题,然后将子问题的解合并为原问题的解。分治算法适用于可以自然分解的问题,如排序、查找、计算等。
分治算法的三个步骤是分解、解决和合并。分解步骤将原问题分解为若干子问题。解决步骤递归求解子问题,如果子问题足够小则直接求解。合并步骤将子问题的解合并为原问题的解。
def merge_sort_divide_conquer(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort_divide_conquer(arr[:mid])
right = merge_sort_divide_conquer(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i, j = 0, 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
data = [38, 27, 43, 3, 9, 82, 10]
sorted_data = merge_sort_divide_conquer(data)
print(f"归并排序: {sorted_data}")
归并排序是分治算法的经典应用。它将数组分成两部分,分别排序,然后合并。归并排序的时间复杂度为O(n log n),因为需要log n层递归,每层合并需要O(n)时间。
快速排序也是分治算法,但它使用不同的分解策略。快速排序选择一个基准元素,将数组分成小于基准和大于基准的两部分,然后递归排序两部分。
def karatsuba_multiply(x, y):
if x < 10or y < 10:
return x * y
n = max(len(str(x)), len(str(y)))
m = n // 2
high1, low1 = x // 10**m, x % 10**m
high2, low2 = y // 10**m, y % 10**m
z0 = karatsuba_multiply(low1, low2)
z1 = karatsuba_multiply((low1 + high1), (low2 + high2))
z2 = karatsuba_multiply(high1, high2)
return (z2 * 10**(2*m)) + ((z1 - z2 - z0) * 10**m) + z0
result = karatsuba_multiply(1234, 5678)
print(f"Karatsuba乘法: 1234 × 5678 = {result}")
Karatsuba乘法使用分治思想加速大数乘法。传统乘法需要O(n²)次乘法运算,Karatsuba算法将其降低到O(n^log3)约O(n^1.585)。这在处理超大数乘法时效率提升显著。
8.4 字符串匹配算法
字符串匹配是文本处理中的基本问题,要求在一个文本串中查找模式串的所有出现位置。朴素匹配算法时间复杂度为O(n×m),其中n是文本长度,m是模式长度。KMP算法通过预处理模式串,将时间复杂度降低到O(n+m)。
def kmp_search(text, pattern):
def build_lps(pattern):
lps = [0] * len(pattern)
length = 0
i = 1
while i < len(pattern):
if pattern[i] == pattern[length]:
length += 1
lps[i] = length
i += 1
else:
if length != 0:
length = lps[length - 1]
else:
lps[i] = 0
i += 1
return lps
lps = build_lps(pattern)
positions = []
i, j = 0, 0
while i < len(text):
if pattern[j] == text[i]:
i += 1
j += 1
if j == len(pattern):
positions.append(i - j)
j = lps[j - 1]
else:
if j != 0:
j = lps[j - 1]
else:
i += 1
return positions
text = "ABABDABACDABABCABAB"
pattern = "ABABCABAB"
positions = kmp_search(text, pattern)
print(f"在文本中找到模式'{pattern}'的位置: {positions}")

KMP Animation
KMP算法的关键是构建部分匹配表(LPS数组)。LPS数组记录模式串中每个位置之前的最长相同前后缀长度。当匹配失败时,模式串可以跳过已经匹配的部分,避免重复比较。
8.5 最小生成树
MST

MST Animation
最小生成树是连通图的一个子图,它包含所有顶点且边数最少,边权总和最小。最小生成树在网络设计、电路设计等场景有重要应用。Prim算法和Kruskal算法是两种经典的构造方法。
def kruskal_mst(edges, n):
def find(parent, i):
if parent[i] != i:
parent[i] = find(parent, parent[i])
return parent[i]
def union(parent, rank, x, y):
root_x = find(parent, x)
root_y = find(parent, y)
if rank[root_x] < rank[root_y]:
parent[root_x] = root_y
elif rank[root_x] > rank[root_y]:
parent[root_y] = root_x
else:
parent[root_y] = root_x
rank[root_x] += 1
edges.sort(key=lambda x: x[2])
parent = [i for i in range(n)]
rank = [0] * n
mst = []
total_weight = 0
for u, v, weight in edges:
root_u = find(parent, u)
root_v = find(parent, v)
if root_u != root_v:
union(parent, rank, root_u, root_v)
mst.append((u, v, weight))
total_weight += weight
return mst, total_weight
edges = [(0, 1, 10), (0, 2, 6), (0, 3, 5),
(1, 3, 15), (2, 3, 4)]
n = 4
mst, weight = kruskal_mst(edges, n)
print("最小生成树的边:")
for u, v, w in mst:
print(f"边({u}, {v}), 权重{w}")
print(f"总权重: {weight}")
Kruskal算法按边权从小到大依次选择边,使用并查集检测是否形成环。Prim算法从一个顶点开始,每次选择连接已选顶点和未选顶点的最小权边。两种算法的时间复杂度都是O(E log E)或O(E log V)。
8.6 算法设计总结
掌握算法设计需要理解多种算法思想,并能够根据问题特点选择合适的方法。表8-1总结了主要算法设计技术的特点和适用场景。
| 算法类型 | 核心思想 | 时间复杂度 | 适用场景 |
|---|---|---|---|
| 贪心算法 | 局部最优选择 | 通常较低 | 活动选择、最小生成树、分数背包 |
| 回溯算法 | 搜索所有可能解 | 指数级 | N皇后、排列组合、子集问题 |
| 分治算法 | 分解解决合并 | O(n log n)常见 | 归并排序、快速排序、大数乘法 |
| 动态规划 | 保存子问题解 | 多项式级 | 背包问题、LCS、最短路径 |
| 图算法 | BFS/DFS遍历 | O(V+E) | 路径搜索、连通性、拓扑排序 |
def algorithm_comparison():
import time
problems = {
'小规模排序': ('插入排序', 'O(n²)', '简单,稳定'),
'大规模排序': ('快速排序', 'O(n log n)', '高效,原地'),
'字符串搜索': ('KMP算法', 'O(n+m)', '避免重复比较'),
'最短路径': ('Dijkstra', 'O(E log V)', '正权图最短路径'),
'连通分量': ('DFS', 'O(V+E)', '检测图连通性'),
'背包问题': ('动态规划', 'O(n×W)', '0-1背包最优解'),
}
print("算法选择指南:")
for problem, (algorithm, complexity, note) in problems.items():
print(f"{problem}: {algorithm} ({complexity}) - {note}")
algorithm_comparison()
选择合适的算法需要考虑多个因素。问题规模决定了算法的时间复杂度要求。数据特点影响算法的适用性,如是否有序、是否有重复等。性能要求包括时间限制、空间限制和是否需要最优解。实现难度权衡了开发成本和维护成本。
算法设计的一般流程是,理解问题明确输入输出和约束条件,分析问题寻找最优子结构或贪心选择性质,设计算法确定状态定义或分解策略,验证正确性证明算法可以得到正确解,优化改进通过剪枝或空间优化提高效率。
通过本教材的学习,你已经掌握了数据结构与算法的核心知识。从基础的线性结构到复杂的图论算法,从简单的排序到高级的动态规划,这些知识将为你解决实际问题提供坚实的基础。继续实践和探索,你将发现算法世界的更多精彩。
贪心算法通过局部最优达到全局最优,适用于活动选择、最小生成树等问题。回溯算法系统地搜索所有可能解,适用于组合优化问题。分治算法将问题分解为子问题求解,适用于排序和计算问题。我们还学习了字符串匹配算法和最小生成树算法。希望大家通过实践巩固所学知识,在算法设计的道路上不断进步。
预览时标签不可点