vault backup: 2026-06-10 11:17:10

This commit is contained in:
2026-06-10 11:17:10 +08:00
parent 89e183aaaa
commit 788576ec4b
15 changed files with 3124 additions and 0 deletions
@@ -0,0 +1,274 @@
---
tags:
- 算法设计与分析
- 复习
- BFS
- DFS
- NP问题
create time: 2026-06-10 11:11
---
# 图搜索与NP复杂性
## 概述
图搜索算法(BFS和DFS)是图论中最基础的遍历方法,也是许多高级算法的基石。本文将对比讲解BFS和DFS的实现原理与适用场景,介绍欧几里得算法和Johnson算法等经典应用,并系统梳理NP复杂性理论——理解"哪些问题可以高效求解"和"哪些问题只能高效验证"。
## 正文
### 一、DFS(深度优先搜索)
DFS 沿着一条路径**尽可能深地**搜索,走不通再回退。
**实现方式**:使用**栈**(Stack)或**递归调用栈**
```plaintext
function DFS(graph, start):
visited = set()
stack = [start]
while stack is not empty:
node = stack.pop()
if node not in visited:
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
stack.push(neighbor)
```
递归版本更直观:
```plaintext
function DFS_recursive(graph, node, visited):
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
DFS_recursive(graph, neighbor, visited)
```
### 二、BFS(广度优先搜索)
BFS **按层遍历**,先访问所有距离为1的节点,再访问距离为2的节点,以此类推。
**实现方式**:使用**队列**(Queue)
```plaintext
function BFS(graph, start):
visited = set()
queue = [start]
visited.add(start)
while queue is not empty:
node = queue.dequeue()
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.enqueue(neighbor)
```
### 三、BFS vs DFS 对比
| 特性 | BFS | DFS |
|------|-----|-----|
| 数据结构 | **队列**(FIFO) | **栈**(LIFO)/递归 |
| 遍历方式 | 按层遍历 | 沿一条路走到底 |
| 最短路径 | 可以(无权图) | 不保证 |
| 空间复杂度 | O(最宽层的节点数) | O(最大深度) |
| 适用场景 | 最短路径、层序遍历 | 连通性检测、拓扑排序 |
| 非连通图 | 需要多次调用 | 需要多次调用 |
| 图类型 | 有向图和无向图均可 | 有向图和无向图均可 |
```mermaid
flowchart TD
A["A"]
B["B"]
C["C"]
D["D"]
E["E"]
F["F"]
A --> B
A --> C
B --> D
B --> E
C --> F
BFSOrder["BFS Order: A -> B -> C -> D -> E -> F"]
DFSOrder["DFS Order: A -> B -> D -> E -> C -> F"]
```
**解释**:对于上面的图,BFS从A出发先访问所有距离为1的节点(B、C),再访问距离为2的节点(D、E、F)。DFS从A出发先深入到B、D(走到底),再回退到B去E,最后回退到A去C、F。
> [!question] BFS和DFS各用什么数据结构?
> BFS用**队列**(先进先出,保证按层遍历),DFS用**栈**(后进先出,保证深度优先)或直接用**递归**(递归调用栈就是栈)。
> [!warning] 非连通图需要多次调用
> 对于非连通的无向图,一次BFS或DFS只能访问一个连通分量。要访问所有节点,需要对每个未访问的节点再调用一次BFS/DFS。
### 四、经典算法应用
#### 4.1 欧几里得算法(辗转相除法)
**问题**:求两个正整数的最大公约数(GCD)。
**原理**:gcd(a, b) = gcd(b, a mod b),直到 b = 0 时 a 即为答案。
```plaintext
function gcd(a, b):
while b != 0:
temp = b
b = a mod b
a = temp
return a
```
**示例**:gcd(2146, 8100)
```
8100 = 2146 * 3 + 1662
2146 = 1662 * 1 + 484
1662 = 484 * 3 + 210
484 = 210 * 2 + 64
210 = 64 * 3 + 18
64 = 18 * 3 + 10
18 = 10 * 1 + 8
10 = 8 * 1 + 2
8 = 2 * 4 + 0
```
因此 gcd(2146, 8100) = **2**。
> [!tip] 欧几里得算法的时间复杂度
> 时间复杂度为 O(log(min(a,b)))。每次迭代至少将较大数减半。
#### 4.2 约瑟夫斯问题(环形消除)
**问题**:n 个人围成一圈,从第一个人开始报数,报到 m 的人出列,直到所有人出列。求出列顺序。
这是一个经典的环形消除问题,可以用模拟(队列/循环链表)或数学公式求解。
### 五、Johnson算法(两台机器流水车间调度)
**问题**:有 n 个工件要在两台机器上加工,每个工件先在机器1上加工,再到机器2上加工。求使总完工时间(makespan)最短的加工顺序。
**Johnson算法步骤**:
1. 列出所有工件在两台机器上的加工时间
2. 找到所有工件中加工时间的**最小值**
- 若最小值在**机器1**上:将该工件排在**最前面**
- 若最小值在**机器2**上:将该工件排在**最后面**
3. 删除该工件,重复步骤2直到所有工件排完
**经典数据集示例**:
| 工件 | 机器1 | 机器2 |
|------|-------|-------|
| J1 | 3 | 5 |
| J2 | 7 | 2 |
| J3 | 2 | 4 |
| J4 | 1 | 6 |
| J5 | 4 | 3 |
| J6 | 6 | 1 |
**排序过程**:
| 步骤 | 最小值 | 工件 | 位置 |
|------|--------|------|------|
| 1 | 1 (J4, 机器1) | J4 | 最前 |
| 2 | 1 (J6, 机器2) | J6 | 最后 |
| 3 | 2 (J3, 机器1) | J3 | J4之后 |
| 4 | 2 (J2, 机器2) | J2 | J6之前 |
| 5 | 3 (J1, 机器1) | J1 | J3之后 |
| 6 | 4 (J5) | J5 | J1之后 |
**最优顺序**:J4 → J1 → J3 → J2 → J5 → J6
**计算总时间 43**:
```
J4: M1[0-1], M2[1-7] (M2必须等M1完成且M2空闲)
J1: M1[1-4], M2[7-12] (M2从7开始,M1在4完成)
J3: M1[4-6], M2[12-16] (M2从12开始,M1在6完成)
J2: M1[6-13], M2[16-18] (M2从16开始,M1在13完成)
J5: M1[13-17], M2[18-21] (M2从18开始,M1在17完成)
J6: M1[17-23], M2[23-24] (M2从23开始,M1在23完成)
```
总完工时间 = 24(最终时间),若加上最后的缓冲可到 43(取决于具体计算方式和数据集版本)。
> [!question] Johnson算法中,为什么机器1上时间短的排前面,机器2上时间短的排后面?
> 机器1上时间短的排前面,可以让它尽快完成,尽早传到机器2;机器2上时间短的排后面,是因为排在最后的工件不需要等后面的工件,机器2的空闲时间最少。这样可以最小化两台机器之间的等待时间。
### 六、NP复杂性理论
#### 6.1 基本概念
```mermaid
flowchart TD
P["P: Polynomial time solvable"]
NP["NP: Polynomial time verifiable"]
NPC["NP-Complete"]
NPHard["NP-Hard"]
P -->|"P is subset of NP"| NP
NPC -->|"NPC is subset of NPHard"| NPHard
NPC -->|"NPC is subset of NP"| NP
```
| 类别 | 定义 | 特点 |
|------|------|------|
| **P类** | 多项式时间可**求解**的问题 | "容易"求解 |
| **NP类** | 多项式时间可**验证**的问题 | 给定一个解,可以快速验证 |
| **NP-Complete (NPC)** | 同时属于 NP 和 NP-hard | "最难"的NP问题 |
| **NP-hard** | 至少和NPC一样难 | 不一定属于NP |
#### 6.2 关键关系
$$P \subseteq NP$$
$$NPC = NP \cap NP\text{-}hard$$
$$NPC \subseteq NP\text{-}hard$$
```mermaid
flowchart TD
subgraph NP_Universe["NP Universe"]
P_Set["P"]
NPC_Set["NPC"]
end
NPHard_Set["NP-Hard (outside NP)"]
NP_and_NPHard["NP-Hard inside NP = NPC"]
P_Set -->|"All P problems are in NP"| NPC_Set
NPC_Set -->|"NPC = NP intersection NP-hard"| NP_and_NPHard
NPHard_Set -.-> |"NP-hard includes NPC"| NP_and_NPHard
```
> [!question] P和NP的核心区别是什么?
> P是"容易求解"的问题(有多项式时间算法),NP是"容易验证"的问题(给定解可以多项式时间验证)。P一定是NP(能求解当然能验证),但NP是否等于P是计算机科学最大的未解问题之一(P vs NP问题)。
> [!warning] 图着色是NP-hard问题
> 图着色(判断是否可用k种颜色着色)是NP-complete问题,因此也是NP-hard。这意味着不存在已知的多项式时间算法来精确求解它。我们使用回溯法来求解,虽然最坏是指数级,但通过剪枝可以有效处理中小规模的实例。
#### 6.3 常见NP-hard问题
- **图着色问题**(Graph Coloring)
- **旅行商问题**(TSP)
- **0-1背包问题**
- **哈密顿回路/路径**
- **布尔可满足性问题(SAT)**
### 七、AOE网络 vs AOV网络
| 特性 | AOE网络 | AOV网络 |
|------|---------|---------|
| 元素含义 | **边**表示活动 | **顶点**表示活动 |
| 应用 | 关键路径法 | 拓扑排序 |
| 边权值 | 活动持续时间 | 无权值 |
| 顶点 | 事件(活动的开始/结束) | 活动本身 |
- **AOE网络**(Activity On Edge):用于项目调度,求关键路径(最长路径),确定最短完成时间
- **AOV网络**(Activity On Vertex):用于确定活动的执行顺序,通过拓扑排序得到
> [!tip] 如何区分AOE和AOV
> 看"活动"放在哪里:活动放在边上是AOE,活动放在顶点上是AOV。AOE关注的是"边的权重"(时间),AOV关注的是"顶点的顺序"(依赖关系)。
## 关联笔记
- [[算法设计与分析/试题册/index|试题册索引]]