Files
final-exam/算法设计与分析/复习文档/图搜索与NP复杂性.md
T

318 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags:
- 算法设计与分析
- 复习
- BFS
- DFS
- NP问题
create time: 2026-06-10 11:11
---
# 图搜索与NP复杂性
## 概述
图搜索算法(BFS和DFS)是图论中最基础的遍历方法,也是许多高级算法的基石。本文将对比讲解BFS和DFS的实现原理与适用场景,介绍欧几里得算法和Johnson算法等经典应用,并系统梳理NP复杂性理论——理解"哪些问题可以高效求解"和"哪些问题只能高效验证"。
## 正文
### 一、DFS(深度优先搜索)
DFS 沿着一条路径**尽可能深地**搜索,走不通再回退。
**实现方式**:使用**栈**(Stack)或**递归调用栈**
```cpp
#include <vector>
#include <stack>
#include <iostream>
using namespace std;
// DFS(栈实现)
// graph: 邻接表, start: 起始顶点
void DFS(vector<vector<int>>& graph, int start) {
int n = graph.size();
vector<bool> visited(n, false);
stack<int> st;
st.push(start);
while (!st.empty()) {
int node = st.top();
st.pop();
if (!visited[node]) {
visited[node] = true;
cout << node << " ";
for (int neighbor : graph[node]) {
if (!visited[neighbor])
st.push(neighbor);
}
}
}
}
```
递归版本更直观:
```cpp
#include <vector>
#include <iostream>
using namespace std;
// DFS(递归版本)
// graph: 邻接表, node: 当前顶点, visited: 访问标记数组
void DFS_recursive(vector<vector<int>>& graph, int node, vector<bool>& visited) {
visited[node] = true;
cout << node << " ";
for (int neighbor : graph[node]) {
if (!visited[neighbor])
DFS_recursive(graph, neighbor, visited);
}
}
```
### 二、BFS(广度优先搜索)
BFS **按层遍历**,先访问所有距离为1的节点,再访问距离为2的节点,以此类推。
**实现方式**:使用**队列**(Queue)
```cpp
#include <vector>
#include <queue>
#include <iostream>
using namespace std;
// BFS(队列实现)
// graph: 邻接表, start: 起始顶点
void BFS(vector<vector<int>>& graph, int start) {
int n = graph.size();
vector<bool> visited(n, false);
queue<int> q;
q.push(start);
visited[start] = true;
while (!q.empty()) {
int node = q.front();
q.pop();
cout << node << " ";
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
q.push(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 即为答案。
```cpp
// 欧几里得算法(辗转相除法)
// 求两个正整数的最大公约数
int gcd(int a, int b) {
while (b != 0) {
int temp = b;
b = a % 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|试题册索引]]