9.8 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-06-10 11:11 |
图搜索与NP复杂性
概述
图搜索算法(BFS和DFS)是图论中最基础的遍历方法,也是许多高级算法的基石。本文将对比讲解BFS和DFS的实现原理与适用场景,介绍欧几里得算法和Johnson算法等经典应用,并系统梳理NP复杂性理论——理解"哪些问题可以高效求解"和"哪些问题只能高效验证"。
正文
一、DFS(深度优先搜索)
DFS 沿着一条路径尽可能深地搜索,走不通再回退。
实现方式:使用栈(Stack)或递归调用栈
#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);
}
}
}
}
递归版本更直观:
#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)
#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(最大深度) |
| 适用场景 | 最短路径、层序遍历 | 连通性检测、拓扑排序 |
| 非连通图 | 需要多次调用 | 需要多次调用 |
| 图类型 | 有向图和无向图均可 | 有向图和无向图均可 |
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 即为答案。
// 欧几里得算法(辗转相除法)
// 求两个正整数的最大公约数
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上:将该工件排在最后面
- 删除该工件,重复步骤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 基本概念
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
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关注的是"顶点的顺序"(依赖关系)。