--- tags: - 算法设计与分析 - 复习 - BFS - DFS - NP问题 create time: 2026-06-10 11:11 --- # 图搜索与NP复杂性 ## 概述 图搜索算法(BFS和DFS)是图论中最基础的遍历方法,也是许多高级算法的基石。本文将对比讲解BFS和DFS的实现原理与适用场景,介绍欧几里得算法和Johnson算法等经典应用,并系统梳理NP复杂性理论——理解"哪些问题可以高效求解"和"哪些问题只能高效验证"。 ## 正文 ### 一、DFS(深度优先搜索) DFS 沿着一条路径**尽可能深地**搜索,走不通再回退。 **实现方式**:使用**栈**(Stack)或**递归调用栈** ```cpp #include #include #include using namespace std; // DFS(栈实现) // graph: 邻接表, start: 起始顶点 void DFS(vector>& graph, int start) { int n = graph.size(); vector visited(n, false); stack 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 #include using namespace std; // DFS(递归版本) // graph: 邻接表, node: 当前顶点, visited: 访问标记数组 void DFS_recursive(vector>& graph, int node, vector& 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 #include #include using namespace std; // BFS(队列实现) // graph: 邻接表, start: 起始顶点 void BFS(vector>& graph, int start) { int n = graph.size(); vector visited(n, false); queue 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|试题册索引]]