图论从开始到进阶(C++ 版)

前言

图论是计算机科学与数学的重要交叉领域,广泛应用于网络设计、路径规划、社交网络分析等场景。本文将从图的基础概念讲起,逐步深入核心算法,并提供完整的 C++ 实现代码,帮助读者从零构建图论知识体系。

一、图的基础概念

1.1 什么是图?

图 (Graph) 是由顶点 (Vertex) 和边 (Edge) 组成的一种数据结构,用于描述元素之间的多对多关系。数学上用 G=(V,E) 表示,其中:

  • V 是顶点的有限集合
  • E 是边的有限集合,每条边连接两个顶点

1.2 图的分类

根据边的特性,图可以分为:

  • 无向图:边没有方向,(u,v) 与 (v,u) 表示同一条边
  • 有向图:边有方向,<u,v> 表示从 u 到 v 的有向边
  • 带权图:边具有权重(可以是距离、成本等)
  • 无权图:边没有权重

1.3 基本术语

  • 度 (Degree):顶点关联的边数,有向图中分为入度和出度
  • 路径 (Path):从一个顶点到另一个顶点的边的序列
  • 环 (Cycle):起点和终点相同的路径
  • 连通性:两个顶点之间存在路径则称为连通
  • 连通分量:图中最大的连通子图

二、图的存储方式

选择合适的存储方式对图论算法的效率至关重要,下面介绍三种常用的存储方式:

2.1 邻接矩阵

使用二维数组存储顶点间的连接关系,适用于稠密图。

cpp

const int MAXN = 1005;
int adj_matrix[MAXN][MAXN]; // 邻接矩阵

// 初始化
void init(int n) {
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            adj_matrix[i][j] = (i == j) ? 0 : INT_MAX; // 初始化距离
        }
    }
}

// 添加边
void addEdge(int u, int v, int w = 1) {
    adj_matrix[u][v] = w;
    // 无向图需要添加下面一行
    // adj_matrix[v][u] = w;
}

优点:查询两点间是否有边的时间复杂度为 O (1)缺点:空间复杂度为 O (n²),不适合稀疏图

2.2 邻接表

使用向量数组存储每个顶点的邻接顶点,适用于稀疏图。

cpp

const int MAXN = 100005;
struct Edge {
    int to;    // 目标顶点
    int weight;// 边的权重
    Edge(int t, int w) : to(t), weight(w) {}
};

vector<Edge> adj_list[MAXN]; // 邻接表

// 添加边
void addEdge(int u, int v, int w = 1) {
    adj_list[u].emplace_back(v, w);
    // 无向图需要添加下面一行
    // adj_list[v].emplace_back(u, w);
}

优点:空间复杂度为 O (n+m),适合稀疏图缺点:查询两点间是否有边的时间复杂度为 O (degree (u))

2.3 链式前向星

使用数组模拟链表存储边,适用于边数多的场景,效率高于邻接表。

cpp

const int MAXN = 100005;
const int MAXM = 200005;

struct Edge {
    int to;     // 目标顶点
    int weight; // 边的权重
    int next;   // 下一条边的索引
} edges[MAXM];

int head[MAXN]; // 每个顶点的第一条边的索引
int cnt;        // 边的数量计数器

// 初始化
void init() {
    memset(head, -1, sizeof(head));
    cnt = 0;
}

// 添加边
void addEdge(int u, int v, int w = 1) {
    edges[cnt].to = v;
    edges[cnt].weight = w;
    edges[cnt].next = head[u];
    head[u] = cnt++;
    // 无向图需要添加下面一行
    // addEdge(v, u, w);
}

优点:内存效率高,遍历边速度快缺点:实现相对复杂

三、图的遍历

图的遍历是访问图中所有顶点的过程,主要有两种方式:深度优先搜索 (DFS) 和广度优先搜索 (BFS)。

3.1 深度优先搜索 (DFS)

cpp

const int MAXN = 100005;
vector<int> adj[MAXN]; // 邻接表
bool visited[MAXN];    // 访问标记

// 递归实现
void dfs(int u) {
    visited[u] = true;
    cout << u << " "; // 访问顶点u
    
    for (int v : adj[u]) {
        if (!visited[v]) {
            dfs(v); // 递归访问未访问的邻接顶点
        }
    }
}

// 非递归实现(栈)
void dfs_iterative(int start) {
    stack<int> stk;
    stk.push(start);
    visited[start] = true;
    
    while (!stk.empty()) {
        int u = stk.top();
        stk.pop();
        cout << u << " "; // 访问顶点u
        
        // 注意:为了保持与递归相同的访问顺序,这里逆序入栈
        for (auto it = adj[u].rbegin(); it != adj[u].rend(); ++it) {
            int v = *it;
            if (!visited[v]) {
                visited[v] = true;
                stk.push(v);
            }
        }
    }
}

3.2 广度优先搜索 (BFS)

cpp

const int MAXN = 100005;
vector<int> adj[MAXN]; // 邻接表
bool visited[MAXN];    // 访问标记

void bfs(int start) {
    queue<int> q;
    q.push(start);
    visited[start] = true;
    
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        cout << u << " "; // 访问顶点u
        
        for (int v : adj[u]) {
            if (!visited[v]) {
                visited[v] = true;
                q.push(v);
            }
        }
    }
}

3.3 连通分量查找

cpp

const int MAXN = 100005;
vector<int> adj[MAXN]; // 邻接表
bool visited[MAXN];    // 访问标记

// 使用DFS查找连通分量
void dfs(int u, vector<int>& component) {
    visited[u] = true;
    component.push_back(u);
    
    for (int v : adj[u]) {
        if (!visited[v]) {
            dfs(v, component);
        }
    }
}

// 查找所有连通分量
vector<vector<int>> find_connected_components(int n) {
    fill(visited, visited + n + 1, false);
    vector<vector<int>> components;
    
    for (int i = 1; i <= n; i++) {
        if (!visited[i]) {
            vector<int> component;
            dfs(i, component);
            components.push_back(component);
        }
    }
    
    return components;
}

四、图论进阶算法

4.1 拓扑排序

拓扑排序用于有向无环图 (DAG),得到一个顶点线性序列,使得所有有向边均从序列前部指向后部。

const int MAXN = 100005;
vector<int> adj[MAXN]; // 邻接表
int in_degree[MAXN];   // 入度
int n, m;

// 拓扑排序
vector<int> topological_sort() {
    vector<int> result;
    queue<int> q;
    
    // 初始化队列,将入度为0的顶点入队
    for (int i = 1; i <= n; i++) {
        if (in_degree[i] == 0) {
            q.push(i);
        }
    }
    
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        result.push_back(u);
        
        // 减少邻接顶点的入度
        for (int v : adj[u]) {
            if (--in_degree[v] == 0) {
                q.push(v);
            }
        }
    }
    
    // 如果结果包含所有顶点,则排序成功,否则图中存在环
    if (result.size() != n) {
        return {}; // 存在环,返回空
    }
    return result;
}

4.2 最短路径算法

4.2.1 Dijkstra 算法(单源最短路径,正权图)

cpp

const int MAXN = 100005;
const int INF = INT_MAX;

vector<pair<int, int>> adj[MAXN]; // 邻接表,pair<顶点, 权重>
int dist[MAXN];                   // 距离数组
bool visited[MAXN];               // 访问标记

// Dijkstra算法,返回从start到所有顶点的最短距离
void dijkstra(int start, int n) {
    // 初始化距离数组
    for (int i = 1; i <= n; i++) {
        dist[i] = INF;
        visited[i] = false;
    }
    dist[start] = 0;
    
    // 优先队列,pair<距离, 顶点>,按距离从小到大排序
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
    pq.push({0, start});
    
    while (!pq.empty()) {
        int u = pq.top().second;
        pq.pop();
        
        if (visited[u]) continue;
        visited[u] = true;
        
        // 松弛操作
        for (auto& edge : adj[u]) {
            int v = edge.first;
            int w = edge.second;
            
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
}
4.2.2 SPFA 算法(单源最短路径,可处理负权边)

cpp

const int MAXN = 100005;
const int INF = INT_MAX;

vector<pair<int, int>> adj[MAXN]; // 邻接表,pair<顶点, 权重>
int dist[MAXN];                   // 距离数组
int in_queue[MAXN];               // 记录顶点入队次数
bool in_spfa_queue[MAXN];         // 是否在队列中

// SPFA算法,返回是否存在负环
bool spfa(int start, int n) {
    // 初始化距离数组
    for (int i = 1; i <= n; i++) {
        dist[i] = INF;
        in_queue[i] = 0;
        in_spfa_queue[i] = false;
    }
    dist[start] = 0;
    
    queue<int> q;
    q.push(start);
    in_spfa_queue[start] = true;
    in_queue[start]++;
    
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        in_spfa_queue[u] = false;
        
        // 松弛操作
        for (auto& edge : adj[u]) {
            int v = edge.first;
            int w = edge.second;
            
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                
                if (!in_spfa_queue[v]) {
                    q.push(v);
                    in_spfa_queue[v] = true;
                    in_queue[v]++;
                    
                    // 入队次数超过n,说明存在负环
                    if (in_queue[v] > n) {
                        return true;
                    }
                }
            }
        }
    }
    
    return false; // 不存在负环
}
4.2.3 Floyd 算法(全源最短路径)

cpp

const int MAXN = 505;
const int INF = INT_MAX;

int dist[MAXN][MAXN]; // 距离矩阵
int n, m;

// Floyd算法,计算所有顶点对之间的最短路径
void floyd() {
    // 初始化距离矩阵
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            dist[i][j] = (i == j) ? 0 : INF;
        }
    }
    
    // 读入边
    for (int i = 0; i < m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        dist[u][v] = min(dist[u][v], w);
        // 无向图需要添加下面一行
        // dist[v][u] = min(dist[v][u], w);
    }
    
    // Floyd核心算法
    for (int k = 1; k <= n; k++) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                if (dist[i][k] != INF && dist[k][j] != INF) {
                    dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
                }
            }
        }
    }
}

4.3 最小生成树

4.3.1 Kruskal 算法

cpp

const int MAXN = 100005;
const int MAXM = 200005;

struct Edge {
    int u, v, weight;
    Edge(int u_, int v_, int w_) : u(u_), v(v_), weight(w_) {}
    // 按权重排序
    bool operator<(const Edge& other) const {
        return weight < other.weight;
    }
};

vector<Edge> edges;
int n, m;

// 并查集
struct DSU {
    vector<int> parent;
    vector<int> rank;
    
    DSU(int n) {
        parent.resize(n + 1);
        rank.resize(n + 1, 1);
        for (int i = 1; i <= n; i++) {
            parent[i] = i;
        }
    }
    
    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]); // 路径压缩
        }
        return parent[x];
    }
    
    bool unite(int x, int y) {
        int root_x = find(x);
        int root_y = find(y);
        
        if (root_x == root_y) return false; // 已在同一集合
        
        // 按秩合并
        if (rank[root_x] < rank[root_y]) {
            parent[root_x] = root_y;
        } else {
            parent[root_y] = root_x;
            if (rank[root_x] == rank[root_y]) {
                rank[root_x]++;
            }
        }
        return true;
    }
};

// Kruskal算法,返回最小生成树的总权重,-1表示无法生成
int kruskal() {
    sort(edges.begin(), edges.end()); // 按权重排序
    
    DSU dsu(n);
    int total_weight = 0;
    int edges_used = 0;
    
    for (auto& edge : edges) {
        if (dsu.unite(edge.u, edge.v)) {
            total_weight += edge.weight;
            edges_used++;
            if (edges_used == n - 1) break; // 已找到生成树
        }
    }
    
    return (edges_used == n - 1) ? total_weight : -1;
}
4.3.2 Prim 算法

cpp

const int MAXN = 100005;
const int INF = INT_MAX;

vector<pair<int, int>> adj[MAXN]; // 邻接表,pair<顶点, 权重>
int n, m;

// Prim算法,返回最小生成树的总权重,-1表示无法生成
int prim() {
    vector<bool> in_mst(n + 1, false); // 标记是否已加入MST
    vector<int> dist(n + 1, INF);      // 距离数组
    
    // 优先队列,pair<距离, 顶点>,按距离从小到大排序
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
    
    // 从节点1开始
    dist[1] = 0;
    pq.push({0, 1});
    
    int total_weight = 0;
    int nodes_used = 0;
    
    while (!pq.empty()) {
        auto [d, u] = pq.top();
        pq.pop();
        
        if (in_mst[u]) continue;
        
        in_mst[u] = true;
        total_weight += d;
        nodes_used++;
        
        // 更新邻接顶点的距离
        for (auto& [v, w] : adj[u]) {
            if (!in_mst[v] && w < dist[v]) {
                dist[v] = w;
                pq.push({w, v});
            }
        }
    }
    
    return (nodes_used == n) ? total_weight : -1;
}

4.4 树上算法

4.4.1 最近公共祖先 (LCA) - 倍增算法

cpp

const int MAXN = 100005;
const int LOG = 20; // 2^20 > 1e6

vector<int> adj[MAXN]; // 邻接表
int up[MAXN][LOG];     // up[i][j]表示i的2^j级祖先
int depth[MAXN];       // 深度数组
int n;

// DFS预处理
void dfs_lca(int u, int parent) {
    up[u][0] = parent;
    for (int j = 1; j < LOG; j++) {
        up[u][j] = up[up[u][j-1]][j-1];
    }
    
    for (int v : adj[u]) {
        if (v != parent) {
            depth[v] = depth[u] + 1;
            dfs_lca(v, u);
        }
    }
}

// 初始化
void init_lca(int root) {
    depth[root] = 0;
    memset(up, 0, sizeof(up));
    dfs_lca(root, root); // 根节点的父节点设为自身
}

// 计算u和v的LCA
int lca(int u, int v) {
    if (depth[u] < depth[v]) swap(u, v);
    
    // 将u提升到与v相同的深度
    int diff = depth[u] - depth[v];
    for (int j = 0; j < LOG; j++) {
        if (diff & (1 << j)) {
            u = up[u][j];
        }
    }
    
    if (u == v) return u;
    
    // 共同提升,直到找到LCA
    for (int j = LOG - 1; j >= 0; j--) {
        if (up[u][j] != up[v][j]) {
            u = up[u][j];
            v = up[v][j];
        }
    }
    
    return up[u][0];
}
4.4.2 树的重心

cpp

const int MAXN = 100005;
vector<int> adj[MAXN]; // 邻接表
int size[MAXN];        // 子树大小
int n;
int min_balance;       // 最小不平衡度
int center;            // 重心

// 计算子树大小
void dfs_size(int u, int parent) {
    size[u] = 1;
    for (int v : adj[u]) {
        if (v != parent) {
            dfs_size(v, u);
            size[u] += size[v];
        }
    }
}

// 查找重心
void dfs_center(int u, int parent) {
    int max_subtree = n - size[u]; // 父节点方向的子树大小
    
    for (int v : adj[u]) {
        if (v != parent) {
            dfs_center(v, u);
            max_subtree = max(max_subtree, size[v]);
        }
    }
    
    // 更新重心
    if (max_subtree < min_balance) {
        min_balance = max_subtree;
        center = u;
    }
}

// 查找树的重心
int find_center() {
    dfs_size(1, -1); // 假设根为1
    min_balance = n;
    dfs_center(1, -1);
    return center;
}
4.4.3 树上差分

cpp

const int MAXN = 100005;
vector<int> adj[MAXN]; // 邻接表
int diff[MAXN];        // 差分数组
int res[MAXN];         // 结果数组
int n;

// 计算前缀和
void dfs_diff(int u, int parent) {
    res[u] = diff[u];
    for (int v : adj[u]) {
        if (v != parent) {
            dfs_diff(v, u);
            res[u] += res[v];
        }
    }
}

// 对路径u-v上的所有节点加val
void update_path(int u, int v, int val) {
    int ancestor = lca(u, v); // 使用前面实现的LCA函数
    diff[u] += val;
    diff[v] += val;
    diff[ancestor] -= val;
    if (up[ancestor][0] != ancestor) { // 如果不是根节点
        diff[up[ancestor][0]] -= val;
    }
}

// 初始化并计算结果
void init_diff() {
    memset(diff, 0, sizeof(diff));
    memset(res, 0, sizeof(res));
}

// 获取最终结果
void compute_result() {
    dfs_diff(1, -1); // 假设根为1
}

4.5 Tarjan 算法(割点、割边、强连通分量)

cpp

const int MAXN = 100005;

// 1. 割点和割边
vector<int> adj_cut[MAXN];
int dfn_cut[MAXN], low_cut[MAXN];
bool is_cut_point[MAXN];
vector<pair<int, int>> cut_edges;
int time_cut = 0;

void tarjan_cut(int u, int parent, bool is_root) {
    dfn_cut[u] = low_cut[u] = ++time_cut;
    int children = 0; // 子树数量(根节点用)
    
    for (int v : adj_cut[u]) {
        if (v == parent) continue;
        
        if (!dfn_cut[v]) { // 未访问过
            children++;
            tarjan_cut(v, u, false);
            low_cut[u] = min(low_cut[u], low_cut[v]);
            
            // 判断割点
            if (!is_root && low_cut[v] >= dfn_cut[u]) {
                is_cut_point[u] = true;
            }
            
            // 判断割边
            if (low_cut[v] > dfn_cut[u]) {
                cut_edges.emplace_back(min(u, v), max(u, v)); // 避免重复
            }
        } else { // 回边
            low_cut[u] = min(low_cut[u], dfn_cut[v]);
        }
    }
    
    // 根节点割点判断
    if (is_root && children >= 2) {
        is_cut_point[u] = true;
    }
}

// 2. 强连通分量
vector<int> adj_scc[MAXN];
int dfn_scc[MAXN], low_scc[MAXN];
bool in_stack[MAXN];
stack<int> stk;
vector<vector<int>> sccs;
int time_scc = 0;

void tarjan_scc(int u) {
    dfn_scc[u] = low_scc[u] = ++time_scc;
    stk.push(u);
    in_stack[u] = true;
    
    for (int v : adj_scc[u]) {
        if (!dfn_scc[v]) { // 未访问过
            tarjan_scc(v);
            low_scc[u] = min(low_scc[u], low_scc[v]);
        } else if (in_stack[v]) { // 回边且在栈中
            low_scc[u] = min(low_scc[u], dfn_scc[v]);
        }
    }
    
    // 找到一个SCC
    if (dfn_scc[u] == low_scc[u]) {
        vector<int> scc;
        while (true) {
            int v = stk.top();
            stk.pop();
            in_stack[v] = false;
            scc.push_back(v);
            if (v == u) break;
        }
        sccs.push_back(scc);
    }
}

五、总结与学习建议

图论是算法领域的重要组成部分,本文涵盖了从基础概念到高级算法的主要内容,包括:

  1. 图的基本概念和存储方式
  2. 图的遍历算法(DFS、BFS)
  3. 拓扑排序
  4. 最短路径算法(Dijkstra、SPFA、Floyd)
  5. 最小生成树(Kruskal、Prim)
  6. 树上算法(LCA、树的重心、树上差分)
  7. Tarjan 算法(割点、割边、强连通分量)

学习建议:

  1. 打好基础:先理解图的基本概念和存储方式,这是后续学习的基础
  2. 动手实践:图论算法比较抽象,一定要动手实现,才能真正理解
  3. 由浅入深:从简单的遍历算法开始,逐步学习复杂算法
  4. 多做练习:通过实际问题巩固所学知识,推荐在 OJ 平台上练习相关题目
  5. 理解原理:不仅要记住代码,更要理解算法的核心思想和适用场景

图论算法在实际应用中非常广泛,掌握这些算法将为你的编程能力带来质的提升。祝你学习顺利!

参考资料

  1. 《算法导论》
  2. 《图论及其应用》
  3. 信息学奥赛相关教程和资料
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值