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

9.8 KiB
Raw Blame History

tags, create time
tags create time
算法设计与分析
复习
BFS
DFS
NP问题
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. 找到所有工件中加工时间的最小值
    • 若最小值在机器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 基本概念

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关注的是"顶点的顺序"(依赖关系)。

关联笔记