算法概述
- 算法概述
-
- 目录
- [1.1 算法与程序](#1.1 算法与程序)
-
- [1.1.1 什么是算法](#1.1.1 什么是算法)
- [1.1.2 算法与程序的区别](#1.1.2 算法与程序的区别)
- [1.1.3 算法设计原则](#1.1.3 算法设计原则)
- [1.2 算法复杂性分析](#1.2 算法复杂性分析)
- [1.3 NP完全性理论](#1.3 NP完全性理论)
-
- [1.3.1 P类与NP类问题](#1.3.1 P类与NP类问题)
-
- [P类(Polynomial Time)](#P类(Polynomial Time))
- [NP类(Nondeterministic Polynomial Time)](#NP类(Nondeterministic Polynomial Time))
- [1.3.2 判定问题 vs 最优化问题](#1.3.2 判定问题 vs 最优化问题)
- [1.3.3 NP完全性概念](#1.3.3 NP完全性概念)
- [1.3.4 常见NP完全问题](#1.3.4 常见NP完全问题)
-
- [1. SAT问题(Boolean Satisfiability)](#1. SAT问题(Boolean Satisfiability))
- [2. 旅行商问题(TSP)](#2. 旅行商问题(TSP))
- [3. 0-1背包问题](#3. 0-1背包问题)
- [4. 图着色问题](#4. 图着色问题)
- [1.3.5 NP完全性的实际意义](#1.3.5 NP完全性的实际意义)
- [1.3.6 P vs NP 问题](#1.3.6 P vs NP 问题)
- 练习题
- 答案
- 本章小结
算法概述
本章学习目标
- 理解算法的定义和五个基本特征
- 掌握算法复杂度分析方法(时间复杂度和空间复杂度)
- 了解P类与NP类问题的基本概念
- 认识常见的NP完全问题
目录
1.1 算法与程序
1.1.1 什么是算法
算法定义:算法是解决特定问题的一系列明确步骤,具有以下五个基本特征:
| 特征 | 英文 | 说明 |
|---|---|---|
| 有穷性 | Finiteness | 算法必须在有限步骤内执行完毕 |
| 确定性 | Definiteness | 每一步指令必须明确无歧义 |
| 可行性 | Effectiveness | 每一步操作都可以在有限时间内完成 |
| 输入 | Input | 算法有零个或多个输入 |
| 输出 | Output | 算法产生一个或多个输出 |
1.1.2 算法与程序的区别
算法和程序是相关但不同的概念:
python
# 示例1:这是一个算法(满足五个特征)
def sum_array(arr: list[int]) -> int:
"""计算数组元素的和"""
total = 0
for num in arr:
total += num
return total
# 示例2:这不是算法(不满足有穷性)
def infinite_server():
"""无限运行的服务器程序"""
while True: # 永不终止!
handle_request()
# 示例3:这不是算法(不满足确定性)
def ambiguous_algorithm(x):
"""步骤不明确的代码"""
# 处理数据...(具体步骤不清晰)
return some_result # 从哪里来?
关键区别:
- 所有算法都可以用程序实现,但并非所有程序都是算法
- 算法强调解决问题的方法和步骤 ,程序强调具体的实现
- 算法必须在有限步内终止,程序可以是持续运行的(如操作系统)
1.1.3 算法设计原则
评价算法质量的四个标准:
| 标准 | 说明 | Python体现 |
|---|---|---|
| 正确性 | 对所有合法输入产生正确输出 | 单元测试覆盖各种边界情况 |
| 可读性 | 代码易于理解和维护 | 有意义的变量名、清晰的注释 |
| 健壮性 | 对非法输入有合理的处理 | 异常处理、输入验证 |
| 效率 | 合理的时间和空间复杂度 | 选择合适的数据结构和算法 |
python
# 好的算法示例:二分查找
def binary_search(arr: list[int], target: int) -> int | None:
"""
在有序数组中二分查找目标值
Args:
arr: 升序排列的整数数组
target: 要查找的目标值
Returns:
目标值的索引,如果不存在则返回None
"""
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return None
# 使用示例
if __name__ == "__main__":
data = [1, 3, 5, 7, 9, 11, 13]
print(binary_search(data, 7)) # 输出: 3
print(binary_search(data, 8)) # 输出: None
python
# 不好的算法示例:同一问题的糟糕实现
def f(a, x):
i = 0
j = len(a) - 1
while i <= j:
m = (i + j) // 2
if a[m] == x:
return m
# 逻辑混乱,难以理解
i = m + 1 if a[m] < x else i
j = m - 1 if a[m] > x else j
return -1 # 返回值不一致(None vs -1)
代码风格对比:
- 使用有意义的变量名(
arr,targetvsa,x) - 添加清晰的docstring文档
- 一致的返回值类型
- 添加边界条件处理
1.2 算法复杂性分析
1.2.1 为什么需要复杂度分析
复杂度分析是算法设计的核心工具,它帮助我们在不实际运行代码的情况下评估算法效率。
问题:为什么不能只靠实际运行时间来评价算法?
- 硬件依赖:同样的代码在不同机器上运行时间不同
- 实现依赖:编程语言、编译器优化会影响结果
- 输入规模:算法效率随数据规模增长的变化趋势更重要
1.2.2 渐进记号
大O记号(上界)
定义:f(n) = O(g(n)) 当且仅当存在正常数 c 和 n₀,使得对所有 n ≥ n₀,有 f(n) ≤ c·g(n)
含义:g(n) 是 f(n) 的上界(增长率不超过 g(n))
python
# 示例:分析函数的时间复杂度
def example1(n: int) -> int:
"""O(1): 常数时间"""
return n * 2 + 1 # 固定数量的操作
def example2(arr: list[int]) -> None:
"""O(n): 线性时间,n = len(arr)"""
for x in arr: # 循环n次
print(x) # O(1)操作
def example3(n: int) -> None:
"""O(n²): 平方时间"""
for i in range(n): # 外层循环n次
for j in range(n): # 内层循环n次
print(i, j) # O(1)操作
# 总操作数:n × n = n²
def example4(n: int) -> int:
"""O(log n): 对数时间"""
while n > 1:
n = n // 2 # 每次n减半
# 循环次数:log₂n
return 0
大Ω记号(下界)
定义:f(n) = Ω(g(n)) 当且仅当存在正常数 c 和 n₀,使得对所有 n ≥ n₀,有 f(n) ≥ c·g(n)
含义:g(n) 是 f(n) 的下界(增长率不低于 g(n))
大θ记号(紧确界)
定义:f(n) = Θ(g(n)) 当且仅当 f(n) = O(g(n)) 且 f(n) = Ω(g(n))
含义:g(n) 是 f(n) 的紧确界(增长率既不高于也不低于 g(n))
三者关系图

上图展示了渐进记号之间的关系:
- 红色虚线:O(g(n)) 表示上界,f(n) 的增长率不超过这个
- 绿色虚线:Ω(g(n)) 表示下界,f(n) 的增长率不低于这个
- 紫色点线:Θ(g(n)) 表示紧确界,是上下界的交集
1.2.3 常见时间复杂度层次

上图展示不同复杂度的效率层次:金字塔越高(操作数越少),算法效率越高。
按效率从高到低排序:
| 复杂度 | 符号 | 典型算法 | n=100时操作数 |
|---|---|---|---|
| 常数 | O(1) | 数组访问、哈希查找 | 1 |
| 对数 | O(log n) | 二分搜索 | ~7 |
| 线性 | O(n) | 顺序搜索 | 100 |
| 线性对数 | O(n log n) | 归并排序、快速排序 | ~700 |
| 平方 | O(n²) | 冒泡排序、插入排序 | 10,000 |
| 立方 | O(n³) | 普通矩阵乘法 | 1,000,000 |
| 指数 | O(2ⁿ) | 汉诺塔、暴力搜索 | 1.27×10³⁰ |
| 阶乘 | O(n!) | 旅行商(暴力) | 9.33×10¹⁵⁷ |
python
# 复杂度实验:对比不同复杂度的实际运行时间
import time
def constant_time(n: int) -> int:
"""O(1): 常数时间"""
return n * 2
def linear_time(n: int) -> int:
"""O(n): 线性时间"""
total = 0
for i in range(n):
total += 1
return total
def quadratic_time(n: int) -> int:
"""O(n²): 平方时间"""
count = 0
for i in range(n):
for j in range(n):
count += 1
return count
def measure_time(func, n, iterations=1000):
"""测量函数平均执行时间"""
start = time.perf_counter()
for _ in range(iterations):
func(n)
end = time.perf_counter()
return (end - start) / iterations * 1000 # 毫秒
# 对比实验
if __name__ == "__main__":
print(f"{'n':<10} {'O(1)(μs)':<12} {'O(n)(μs)':<12} {'O(n²)(μs)':<12}")
print("-" * 50)
for n in [100, 1000, 10000]:
t1 = measure_time(constant_time, n)
t2 = measure_time(linear_time, n)
t3 = measure_time(quadratic_time, n)
print(f"{n:<10} {t1:<12.4f} {t2:<12.4f} {t3:<12.4f}")
# 预期输出示例:
# n O(1)(μs) O(n)(μs) O(n²)(μs)
# --------------------------------------------------
# 100 0.0005 0.0023 0.1234
# 1000 0.0004 0.0198 11.5678
# 10000 0.0006 0.1987 1245.6789
1.2.4 复杂度分析规则
python
# 规则1:顺序结构 - 取最大
def sequential(n: int) -> None:
part1(n) # O(n)
part2(n) # O(n²)
part3(n) # O(log n)
# 总复杂度:O(n²) = max(O(n), O(n²), O(log n))
# 规则2:选择结构 - 取最大
def conditional(n: int) -> None:
if condition:
part1(n) # O(n)
else:
part2(n) # O(n²)
# 总复杂度:O(n²) = max(O(n), O(n²))
# 规则3:循环结构 - 循环次数 × 循环体复杂度
def single_loop(n: int) -> None:
for i in range(n): # 循环n次
O(1)_operation # O(1)操作
# 总复杂度:O(n) = n × O(1)
def nested_loops(n: int) -> None:
for i in range(n): # 外层:n次
for j in range(n): # 内层:n次
O(1)_operation # O(1)操作
# 总复杂度:O(n²) = n × n × O(1)
# 规则4:函数调用 - 代入被调用函数的复杂度
def caller(n: int) -> None:
for i in range(n):
callee(n) # callee的复杂度是O(n)
# 总复杂度:O(n²) = n × O(n)
1.2.5 递归算法复杂度分析
递归树方法
python
# 示例1:归并排序的复杂度分析
def merge_sort(arr: list[int]) -> list[int]:
"""T(n) = 2T(n/2) + O(n) = O(n log n)"""
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid]) # T(n/2)
right = merge_sort(arr[mid:]) # T(n/2)
return merge(left, right) # O(n) - 合并操作
# 递归树分析:
# n <- 第1层:O(n)
# n/2 n/2 <- 第2层:2 × O(n/2) = O(n)
# n/4 n/4 n/4 n/4 <- 第3层:4 × O(n/4) = O(n)
# ... ... ... ...
# 1 1 1 1 1 1 1 1 1 1 <- 第log₂n层:n × O(1) = O(n)
#
# 总复杂度:(log₂n + 1) × O(n) = O(n log n)
# 示例2:二分搜索的复杂度分析
def binary_search(arr: list[int], target: int, left: int, right: int) -> int:
"""T(n) = T(n/2) + O(1) = O(log n)"""
if left > right:
return -1
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return binary_search(arr, target, mid + 1, right) # T(n/2)
else:
return binary_search(arr, target, left, mid - 1) # T(n/2)
# 递归树分析:
# n <- O(1)
# n/2 <- O(1)
# n/4 <- O(1)
# ... <- ...
# 1 <- O(1)
# 高度:log₂n,每层O(1),总计:O(log n)
主定理(Master Theorem)
对于形如 T(n) = aT(n/b) + f(n) 的递归关系,其中 a ≥ 1, b > 1:
令 c = log_b(a),比较 f(n) 与 n^c 的大小:
| 情况 | 条件 | 解 |
|---|---|---|
| 1 | f(n) = O(n^(c-ε)),ε > 0 | T(n) = Θ(n^c) |
| 2 | f(n) = Θ(n^c × log^k n),k ≥ 0 | T(n) = Θ(n^c × log^(k+1) n) |
| 3 | f(n) = Ω(n^(c+ε)),ε > 0,且满足正则条件 | T(n) = Θ(f(n)) |
python
# 主定理应用示例
# 案例1:归并排序
# T(n) = 2T(n/2) + O(n)
# a=2, b=2, f(n)=n
# c = log₂(2) = 1
# f(n) = n = n^1 = Θ(n^c),属于情况2(k=0)
# 解:T(n) = Θ(n log n)
# 案例2:二分搜索
# T(n) = T(n/2) + O(1)
# a=1, b=2, f(n)=1
# c = log₂(1) = 0
# f(n) = 1 = n^0 = Θ(n^c),属于情况2(k=0)
# 解:T(n) = Θ(log n)
# 案例3:二叉树遍历
# T(n) = 2T(n/2) + O(1)
# a=2, b=2, f(n)=1
# c = log₂(2) = 1
# f(n) = 1 = O(n^(1-ε)),ε=1,属于情况1
# 解:T(n) = Θ(n)
1.2.6 空间复杂度分析
空间复杂度衡量算法执行过程中额外使用的存储空间。
python
# 空间复杂度示例
def example1(arr: list[int]) -> int:
"""空间复杂度:O(1) - 原地操作"""
total = 0 # O(1)
for x in arr:
total += x # 使用固定的额外空间
return total
def example2(arr: list[int]) -> list[int]:
"""空间复杂度:O(n) - 创建新数组"""
result = [] # O(n)
for x in arr:
result.append(x * 2) # 存储n个元素
return result
def example3(n: int) -> int:
"""空间复杂度:O(n) - 递归调用栈"""
if n <= 1:
return 1
return n * example3(n - 1)
# 递归深度为n,每层栈帧O(1),总计O(n)
def example4(matrix: list[list[int]]) -> list[list[int]]:
"""空间复杂度:O(n²) - 创建新矩阵"""
n = len(matrix)
result = [[0] * n for _ in range(n)] # n × n 矩阵
for i in range(n):
for j in range(n):
result[i][j] = matrix[i][j] * 2
return result
原地算法(In-place Algorithm):辅助空间为 O(1) 的算法
python
# 原地算法示例
def reverse_array_inplace(arr: list[int]) -> None:
"""原地反转数组 - O(1) 额外空间"""
left, right = 0, len(arr) - 1
while left < right:
arr[left], arr[right] = arr[right], arr[left]
left += 1
right -= 1
# 非原地算法示例
def reverse_array_new(arr: list[int]) -> list[int]:
"""创建新数组存储反转结果 - O(n) 额外空间"""
return arr[::-1] # 创建新的反转数组
1.3 NP完全性理论
1.3.1 P类与NP类问题
计算复杂性理论将问题按照求解难度分类:
P类(Polynomial Time)
定义:可以在多项式时间内求解的判定问题
python
# P类问题示例:排序
def is_sorted(arr: list[int]) -> bool:
"""判断数组是否有序 - O(n) 多项式时间"""
for i in range(len(arr) - 1):
if arr[i] > arr[i + 1]:
return False
return True
def bubble_sort(arr: list[int]) -> list[int]:
"""冒泡排序 - O(n²) 多项式时间"""
n = len(arr)
result = arr.copy()
for i in range(n):
for j in range(n - 1 - i):
if result[j] > result[j + 1]:
result[j], result[j + 1] = result[j + 1], result[j]
return result
NP类(Nondeterministic Polynomial Time)
定义 :可以在多项式时间内验证解的判定问题
python
# NP问题示例:可满足性问题的验证
def verify_sat(
formula: list[list[int]],
assignment: list[bool]
) -> bool:
"""
验证布尔公式是否被赋值满足 - 多项式时间O(m)
Args:
formula: CNF形式的布尔公式,每个子句是文字列表
正数表示变量,负数表示变量的否定
assignment: 变量赋值,True表示真,False表示假
Returns:
该赋值是否满足公式
"""
for clause in formula: # 遍历每个子句
clause_satisfied = False
for var in clause:
if var > 0 and assignment[var - 1]: # 正变量
clause_satisfied = True
break
elif var < 0 and not assignment[-var - 1]: # 负变量
clause_satisfied = True
break
if not clause_satisfied: # 有子句不满足
return False
return True
# 求解SAT问题 - 需要指数时间
def solve_sat_bruteforce(formula: list[list[int]]) -> list[bool] | None:
"""
暴力求解SAT问题 - 指数时间 O(2^n × m)
注意:验证是多项式时间,但求解是指数时间!
这正是NP问题的关键特征。
"""
if not formula:
return []
n_vars = max(abs(var) for clause in formula for var in clause)
# 枚举所有可能的赋值:2^n 种
for i in range(2 ** n_vars):
assignment = [(i >> j) & 1 == 1 for j in range(n_vars)]
if verify_sat(formula, assignment):
return assignment
return None
关键区别:
- P类 :多项式时间求解
- NP类 :多项式时间验证
- P ⊆ NP(能快速求解的问题,肯定也能快速验证)
1.3.2 判定问题 vs 最优化问题
python
# 最优化问题:求最优解
def tsp_optimal(cities: list[tuple[float, float]]) -> tuple[list[int], float]:
"""
旅行商问题(最优化版本):找最短路径
指数时间复杂度
"""
from itertools import permutations
n = len(cities)
min_distance = float('inf')
best_tour = list(range(n))
for tour in permutations(range(n)): # n! 种排列
total = calculate_distance(cities, tour)
if total < min_distance:
min_distance = total
best_tour = list(tour)
return best_tour, min_distance
# 判定问题:是/否答案
def tsp_decision(
cities: list[tuple[float, float]],
max_distance: float
) -> bool:
"""
旅行商问题(判定版本):是否存在长度 ≤ max_distance 的路径?
仍然是NP完全问题
"""
from itertools import permutations
for tour in permutations(range(len(cities))):
if calculate_distance(cities, tour) <= max_distance:
return True
return False
# 最优化问题可以转化为判定问题(二分搜索答案)
def tsp_via_decision(cities: list[tuple[float, float]]) -> float:
"""通过判定问题求解最优化问题"""
# 使用二分搜索 + 判定问题来找最优距离
low, high = 0, float('inf')
while high - low > 1e-6:
mid = (low + high) / 2
if tsp_decision(cities, mid):
high = mid
else:
low = mid
return high
1.3.3 NP完全性概念
规约(Reduction)
直观含义:如果问题A可以规约到问题B,那么"会解B就会解A"

上图展示了规约的完整流程:
- 将问题A的实例转换为问题B的实例(多项式时间)
- 使用问题B的求解器得到B的解
- 将B的解转换回A的解
规约的传递性:如果 A ≤p B 且 B ≤p C,则 A ≤p C

规约的传递性是证明NP完全性的基础:通过将已知的NP完全问题(如SAT)规约到新问题,可以证明新问题也是NP完全的。
NP完全、NP困难、NP的关系

上图清晰地展示了P、NP、NP完全、NP困难之间的包含关系:
| 类别 | 定义 | 例子 |
|---|---|---|
| P | 多项式时间可解 | 排序、最短路径 |
| NP | 多项式时间可验证 | SAT、TSP |
| NP完全 | NP中最难的问题,所有NP问题都可规约到它 | SAT、TSP、背包问题 |
| NP困难 | 至少和NP完全一样难(可能不在NP中) | 停机问题、TSP的最优化版本 |
关键关系:
- P ⊆ NP(能快速求解的问题,肯定也能快速验证)
- NP完全 = NP ∩ NP-hard(既在NP中,又是NP-hard)
- P = NP? 是计算机科学中最重要的未解决问题之一
Cook-Levin定理
内容:SAT(布尔可满足性)问题是第一个NP完全问题
意义:
- 证明了NP完全问题的存在
- 提供了证明其他问题NP完全性的方法:通过从SAT规约
- 不需要掌握证明,但需要理解其重要性
1.3.4 常见NP完全问题
1. SAT问题(Boolean Satisfiability)
python
def sat_example():
"""SAT问题示例"""
# 公式:(x₁ ∨ ¬x₃) ∧ (¬x₁ ∨ x₂ ∨ x₃) ∧ (x₂)
# 用列表表示子句,正数表示变量,负数表示其否定
formula = [
[1, -3], # (x₁ ∨ ¬x₃)
[-1, 2, 3], # (¬x₁ ∨ x₂ ∨ x₃)
[2] # (x₂)
]
# 验证赋值 x₁=True, x₂=True, x₃=False 是否满足
assignment = [True, True, False] # x₁, x₂, x₃
result = verify_sat(formula, assignment)
print(f"赋值 {assignment} 满足公式: {result}") # True
2. 旅行商问题(TSP)
python
def tsp_example():
"""旅行商问题示例"""
import math
cities = [
(0, 0), # 城市0
(1, 1), # 城市1
(2, 0), # 城市2
(1, -1) # 城市3
]
def distance(a, b):
return math.sqrt((a[0] - b[0])**2 + (a[1] - b[1])**2)
# 判定问题:是否存在长度 ≤ 6 的路径?
max_dist = 6.0
exists = tsp_decision(cities, max_dist)
print(f"存在长度 ≤ {max_dist} 的路径: {exists}")
# 验证给定路径的长度
tour = [0, 1, 2, 3] # 一个可能的路径
tour_distance = calculate_distance(cities, tour)
print(f"路径 {tour} 的长度: {tour_distance:.2f}")
3. 0-1背包问题
python
def knapsack_verify(
weights: list[int],
values: list[int],
capacity: int,
min_value: int,
selection: list[bool]
) -> bool:
"""
验证背包问题的解
Args:
weights: 物品重量
values: 物品价值
capacity: 背包容量
min_value: 要求的最小价值
selection: 物品选择列表
Returns:
该选择是否满足约束且达到最小价值
"""
total_weight = sum(w for w, s in zip(weights, selection) if s)
total_value = sum(v for v, s in zip(values, selection) if s)
return total_weight <= capacity and total_value >= min_value
# 示例
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 5
min_value = 6
selection = [True, True, False, False] # 选前两个物品
is_valid = knapsack_verify(weights, values, capacity, min_value, selection)
print(f"选择 {selection} 有效: {is_valid}") # True(重量5,价值7)
4. 图着色问题
python
def graph_coloring_verify(
edges: list[tuple[int, int]],
colors: list[int],
num_colors: int
) -> bool:
"""
验证图着色问题的解
Args:
edges: 边列表
colors: 每个顶点的颜色
num_colors: 使用的颜色数量
Returns:
是否是合法的着色(相邻顶点不同色)
"""
# 检查颜色范围
if max(colors) >= num_colors:
return False
# 检查所有边
for u, v in edges:
if colors[u] == colors[v]:
return False # 相邻顶点同色!
return True
# 示例:3个顶点的三角形图,需要3种颜色
edges = [(0, 1), (1, 2), (2, 0)]
colors = [0, 1, 2] # 每个顶点不同颜色
is_valid = graph_coloring_verify(edges, colors, 3)
print(f"着色 {colors} 有效: {is_valid}") # True
1.3.5 NP完全性的实际意义
面对NP完全问题的策略
python
"""
策略1:接受近似解
使用贪心算法或启发式算法,找到接近最优的解
"""
def tsp_greedy(cities: list[tuple[float, float]]) -> list[int]:
"""TSP的贪心近似算法 - O(n²)"""
import math
n = len(cities)
unvisited = set(range(1, n))
tour = [0] # 从城市0开始
while unvisited:
current = tour[-1]
# 选择最近的未访问城市
nearest = min(unvisited,
key=lambda j: math.dist(cities[current], cities[j]))
tour.append(nearest)
unvisited.remove(nearest)
return tour # 不保证最优,但快速
"""
策略2:特殊结构利用
针对特定输入结构设计高效算法
"""
def knapsack_dp(weights: list[int], values: list[int], capacity: int) -> int:
"""
背包问题的动态规划解法 - O(n × capacity)
当容量较小时是多项式时间(伪多项式时间)
"""
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(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]
"""
策略3:参数化算法
固定某个参数,使算法在该参数上高效
"""
def vertex_cover_parameterized(
edges: list[tuple[int, int]],
k: int
) -> list[int] | None:
"""
顶点覆盖问题的参数化算法 - O(2^k × n)
当k较小时实用
"""
from itertools import combinations
vertices = set()
for u, v in edges:
vertices.add(u)
vertices.add(v)
# 尝试所有大小为k的顶点组合
for cover in combinations(vertices, k):
cover_set = set(cover)
# 检查是否覆盖所有边
if all(u in cover_set or v in cover_set for u, v in edges):
return list(cover)
return None
1.3.6 P vs NP 问题
P = NP? 是计算机科学中最重要的未解决问题之一。
P = NP?
├── 如果 P = NP:
│ └── 所有可快速验证的问题都可快速求解
│ → 密码学体系崩溃
│ → 许多优化问题变得易解
│ → 人工智能可能达到人类水平
│
└── 如果 P ≠ NP(主流观点):
└── 存在本质上难求解的问题
→ 密码学安全有保障
→ 需要近似算法和启发式方法
→ 解释了为什么某些问题 inherently hard
当前状态:
- 克雷数学研究所千禧年七大难题之一,悬赏100万美元
- 学界普遍认为 P ≠ NP,但尚未证明
- 自1971年提出以来,50多年仍未解决
练习题
概念题
-
算法的五个基本特征是什么? 请举例说明一个不满足"有穷性"特征的程序。
-
判断以下函数的时间复杂度:
pythondef func1(n): for i in range(n): for j in range(10): print(i, j) def func2(n): i = 1 while i < n: i *= 2 def func3(n): if n <= 1: return 1 return func3(n-1) + func3(n-2) -
什么是原地算法? 以下哪个函数是原地算法?
pythondef A(arr): return arr[::-1] def B(arr): left, right = 0, len(arr)-1 while left < right: arr[left], arr[right] = arr[right], arr[left] left += 1 right -= 1 -
解释P类、NP类、NP完全、NP困难的区别,并画图说明它们的关系。
-
为什么说"验证容易,求解难"是NP问题的核心特征? 请举例说明。
代码分析题
-
分析以下递归函数的时间复杂度,并画出递归树:
pythondef recursive_func(n): if n <= 1: return 1 return recursive_func(n//2) + recursive_func(n//2) + n提示:使用主定理。
-
以下函数用于计算斐波那契数列,分析其时间复杂度和空间复杂度:
pythondef fib(n): if n <= 1: return n return fib(n-1) + fib(n-2)如何改进使其时间复杂度降到 O(n)?
-
判断以下说法是否正确,并说明理由:
- "如果一个算法是O(n²)的,那么它一定不是O(n)的"
- "如果一个算法是O(n)的,那么它也可能是O(n²)的"
- "O(n)和O(n+1)是不同的复杂度"
编程题
-
实现一个函数判断数组是否有序,要求:
- 时间复杂度 O(n)
- 空间复杂度 O(1)
- 同时支持升序和降序判断
-
实现SAT问题的暴力求解器:
- 输入:CNF形式的布尔公式
- 输出:满足的赋值,或None(无解)
- 分析你的算法的时间复杂度
答案
概念题答案
-
算法的五个基本特征:
- 有穷性:有限步骤内终止
- 确定性:每步明确无歧义
- 可行性:每步可执行
- 输入:零个或多个输入
- 输出:一个或多个输出
不满足有穷性的程序示例:
pythondef infinite_loop(): while True: pass # 永不终止 -
时间复杂度分析:
func1: O(n) - 外层n次,内层固定10次func2: O(log n) - 每次i翻倍func3: O(2^n) - 递归树有2^n个节点
-
原地算法:
- 函数B是原地算法(O(1)额外空间)
- 函数A不是(O(n)额外空间创建新数组)
-
P/NP关系图:

-
验证容易,求解难:
- NP问题可以在多项式时间内验证解的正确性
- 但找到解可能需要指数时间
- 例如:SAT问题验证是O(m),但求解是O(2^n)
代码分析题答案
-
递归函数分析:
- 递归关系:T(n) = 2T(n/2) + O(n)
- a=2, b=2, f(n)=n
- c = log₂(2) = 1
- f(n) = Θ(n^c),属于主定理情况2
- 答案:T(n) = Θ(n log n)
-
斐波那契数列分析:
- 时间复杂度:O(2^n) - 每次调用产生两个新调用
- 空间复杂度:O(n) - 递归深度
改进版本(动态规划):
pythondef fib_improved(n): if n <= 1: return n a, b = 0, 1 for _ in range(2, n + 1): a, b = b, a + b return b # 时间复杂度:O(n) # 空间复杂度:O(1) -
判断正误:
-
"如果一个算法是O(n²)的,那么它一定不是O(n)的"
- 错误:O(n²)是上界,算法可能同时是O(n)和O(n²)
- 正确说法:如果一个算法是Θ(n²)的,那么它不是Θ(n)的
-
"如果一个算法是O(n)的,那么它也可能是O(n²)的"
- 正确:O(n) ⊆ O(n²)
-
"O(n)和O(n+1)是不同的复杂度"
- 错误:渐进记号忽略常数,O(n) = O(n+1)
-
编程题答案
-
判断数组是否有序:
pythondef is_sorted(arr: list[int]) -> str: """ 判断数组是否有序 Returns: "ascending" - 升序 "descending" - 降序 "unsorted" - 无序 """ if len(arr) <= 1: return "ascending" # 单元素数组视为升序 # 检查升序 is_asc = all(arr[i] <= arr[i+1] for i in range(len(arr)-1)) # 检查降序 is_desc = all(arr[i] >= arr[i+1] for i in range(len(arr)-1)) if is_asc: return "ascending" elif is_desc: return "descending" else: return "unsorted" # 测试 print(is_sorted([1, 2, 3, 4])) # ascending print(is_sorted([4, 3, 2, 1])) # descending print(is_sorted([1, 3, 2, 4])) # unsorted -
SAT问题暴力求解器:
pythondef solve_sat(formula: list[list[int]]) -> list[bool] | None: """ SAT问题暴力求解器 时间复杂度:O(2^n × m),n是变量数,m是子句数 空间复杂度:O(n) """ if not formula: return [] # 确定变量数量 n_vars = max(abs(var) for clause in formula for var in clause) # 枚举所有可能的赋值 for i in range(2 ** n_vars): assignment = [(i >> j) & 1 == 1 for j in range(n_vars)] if verify_sat(formula, assignment): return assignment return None # 无解 # 测试 formula = [[1, 2], [-1, 2], [-1, -2]] # 可满足 result = solve_sat(formula) print(f"解: {result}") # 可能输出 [False, True] 或其他满足的赋值
本章小结
核心知识点回顾
-
算法定义:算法是满足五个基本特征(有穷性、确定性、可行性、输入、输出)的问题求解步骤
-
复杂度分析:
- 时间复杂度:大O、大Ω、大θ记号
- 常见层次:O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ)
- 递归分析:递归树方法、主定理
- 空间复杂度:区分输入空间和辅助空间
-
NP完全性理论:
- P类:多项式时间可解
- NP类:多项式时间可验证
- NP完全:NP中最难的问题
- 面对NP完全问题的策略:近似、特殊结构、参数化
与后续章节的关联
| 本章内容 | 后续章节 |
|---|---|
| 算法特征 | 所有章节的算法设计 |
| 复杂度分析 | 每个算法都会分析其复杂度 |
| 递归分析 | 第2章递归与分治策略 |
| NP完全性 | 第5、6章回溯法和分支限界法 |
学习建议
- 掌握复杂度分析是后续学习的基础,务必多做练习
- 不要死记硬背主定理公式,理解递归树分析方法更重要
- NP完全性理论偏重概念,重点在直观理解而非严格证明
- 动手实践:实现每个算法并实际测量其运行时间
下一章预告:递归与分治策略将学习如何将问题分解为更小的子问题,是递归算法和高效算法设计的核心技术。