C++图论:从基础到实战应用

一、图论基础概念

  1. 图的定义

    一个图 GGG 由两个集合组成:

    • 顶点集合 VVV (Vertices):代表图中的节点或实体。
    • 边集合 EEE (Edges):代表顶点之间的连接或关系。每条边连接两个顶点(或自身,形成自环)。
  2. 图的分类

    • 无向图 (Undirected Graph) :边没有方向。如果顶点 uuu 和 vvv 之间有边,则 uuu 和 vvv 是相邻的,边表示为 (u,v)(u, v)(u,v) 或 (v,u)(v, u)(v,u)。
    • 有向图 (Directed Graph / Digraph) :边有方向。从顶点 uuu 指向顶点 vvv 的边表示为 <u,v><u, v><u,v> 或 u→vu \to vu→v。uuu 是尾 (tail),vvv 是头 (head)。
    • 加权图 (Weighted Graph):边(有时也包括顶点)被赋予一个数值(权重),表示距离、成本、容量等属性。
    • 无权图 (Unweighted Graph):边没有权重,通常认为权重为 1。
  3. 基本术语

    • 邻接 (Adjacency):如果两个顶点之间有边直接相连,则它们是邻接的。
    • 度 (Degree)
      • 无向图中,一个顶点的度是其邻接边的数量。
      • 有向图中:
        • 出度 (Out-degree):从该顶点出发的边的数量。
        • 入度 (In-degree):指向该顶点的边的数量。
    • 路径 (Path) :顶点序列 v0,v1,v2,...,vkv_0, v_1, v_2, \dots, v_kv0,v1,v2,...,vk,其中每条边 (vi,vi+1)(v_i, v_{i+1})(vi,vi+1)(无向图)或 <vi,vi+1><v_i, v_{i+1}><vi,vi+1>(有向图)都存在。
    • 环/圈 (Cycle):起点和终点相同的路径(且路径长度至少为 1)。
    • 连通性 (Connectivity)
      • 无向图:如果任意两个顶点之间都存在路径,则该图是连通的 (Connected)。一个图可能有多个连通分量 (Connected Components)。
      • 有向图:强连通 (Strongly Connected) 指任意两个顶点 uuu 和 vvv 之间都存在从 uuu 到 vvv 和从 vvv 到 uuu 的路径。弱连通 (Weakly Connected) 指忽略边方向后对应的无向图是连通的。

二、图的表示方法 (C++ 实现)

在 C++ 中,常用的图表示方法有:

  1. 邻接矩阵 (Adjacency Matrix)

    • 原理 :使用一个二维数组 matrix[V][V] 表示图。对于无权图:

      • matrix[u][v] = 1 表示存在边 (u,v)(u, v)(u,v) (无向图) 或 <u,v><u, v><u,v> (有向图)。
      • matrix[u][v] = 0 表示无边。
    • 加权图matrix[u][v] 存储边的权重。若无边,可用特定值(如 INT_MAX)或 0(需注意区分)表示。

    • C++ 代码示例

      cpp 复制代码
      #include <iostream>
      #include <vector>
      using namespace std;
      
      class Graph {
      private:
          int V; // 顶点数
          vector<vector<int>> adjMatrix; // 邻接矩阵
      
      public:
          Graph(int vertices) : V(vertices) {
              adjMatrix.resize(V, vector<int>(V, 0)); // 初始化全0
          }
      
          // 添加无向图边 (无权)
          void addEdgeUndirected(int u, int v) {
              adjMatrix[u][v] = 1;
              adjMatrix[v][u] = 1; // 无向图是对称的
          }
      
          // 添加有向图边 (无权)
          void addEdgeDirected(int u, int v) {
              adjMatrix[u][v] = 1;
          }
      
          // 添加带权有向边
          void addWeightedEdge(int u, int v, int weight) {
              adjMatrix[u][v] = weight;
          }
      
          // 打印邻接矩阵
          void print() {
              for (int i = 0; i < V; ++i) {
                  for (int j = 0; j < V; ++j) {
                      cout << adjMatrix[i][j] << " ";
                  }
                  cout << endl;
              }
          }
      };
    • 优缺点

      • 优点 :查询边是否存在很快 O(1)O(1)O(1)。
      • 缺点 :空间复杂度高 O(V2)O(V^2)O(V2),不适用于稀疏图。添加/删除顶点开销大。
  2. 邻接表 (Adjacency List)

    • 原理:为每个顶点维护一个列表(链表、动态数组等),存储与该顶点直接相邻的所有顶点(对于加权图,还需存储权重)。

    • C++ 代码示例 (使用 vector 存储列表):

      cpp 复制代码
      #include <iostream>
      #include <vector>
      using namespace std;
      
      // 加权图的边结构体
      struct Edge {
          int dest; // 目标顶点
          int weight; // 权重
          Edge(int d, int w) : dest(d), weight(w) {}
      };
      
      class Graph {
      private:
          int V; // 顶点数
          vector<vector<Edge>> adjList; // 邻接表 (存储边)
      
      public:
          Graph(int vertices) : V(vertices) {
              adjList.resize(V);
          }
      
          // 添加无向图边 (无权)
          void addEdgeUndirected(int u, int v) {
              adjList[u].push_back(Edge(v, 1)); // 权重设为1
              adjList[v].push_back(Edge(u, 1));
          }
      
          // 添加有向图边 (无权)
          void addEdgeDirected(int u, int v) {
              adjList[u].push_back(Edge(v, 1));
          }
      
          // 添加带权有向边
          void addWeightedEdge(int u, int v, int weight) {
              adjList[u].push_back(Edge(v, weight));
          }
      
          // 打印邻接表
          void print() {
              for (int i = 0; i < V; ++i) {
                  cout << "Vertex " << i << ": ";
                  for (const Edge& edge : adjList[i]) {
                      cout << "-> (" << edge.dest << ", " << edge.weight << ") ";
                  }
                  cout << endl;
              }
          }
      };
    • 优缺点

      • 优点 :空间复杂度低 O(V+E)O(V + E)O(V+E),适用于稀疏图。易于遍历一个顶点的所有邻居。
      • 缺点 :查询任意两点间是否有边稍慢 O(deg(u))O(\text{deg}(u))O(deg(u))(平均)。

选择哪种表示方法? 取决于图的稀疏程度和需要频繁执行的操作。稀疏图优先考虑邻接表。

三、图遍历算法

遍历是图算法的基础,用于访问图中所有顶点(或连通分量)。

  1. 广度优先搜索 (BFS - Breadth-First Search)

    • 原理:从源点开始,逐层向外探索,先访问所有距离为 1 的邻居,再访问距离为 2 的邻居,以此类推。使用队列 (Queue) 管理待访问顶点。

    • 应用:无权图的最短路径(最少边数)、连通分量、社交网络中的好友层级。

    • C++ 实现 (邻接表,无权图)

      cpp 复制代码
      #include <iostream>
      #include <vector>
      #include <queue>
      using namespace std;
      
      void BFS(const vector<vector<int>>& graph, int start) {
          int V = graph.size();
          vector<bool> visited(V, false); // 标记是否访问过
          queue<int> q;
      
          visited[start] = true;
          q.push(start);
      
          while (!q.empty()) {
              int u = q.front();
              q.pop();
              cout << u << " "; // 访问当前顶点
      
              // 遍历 u 的所有邻居
              for (int v : graph[u]) {
                  if (!visited[v]) {
                      visited[v] = true;
                      q.push(v);
                  }
              }
          }
      }
  2. 深度优先搜索 (DFS - Depth-First Search)

    • 原理:沿着一条路径尽可能深入地探索,直到尽头,然后回溯。使用递归或栈 (Stack) 实现。

    • 应用:查找连通分量、拓扑排序、查找环、路径探索(迷宫求解)。

    • C++ 递归实现 (邻接表)

      cpp 复制代码
      #include <iostream>
      #include <vector>
      using namespace std;
      
      void DFSRecursive(const vector<vector<int>>& graph, int u, vector<bool>& visited) {
          visited[u] = true;
          cout << u << " "; // 访问当前顶点
      
          for (int v : graph[u]) {
              if (!visited[v]) {
                  DFSRecursive(graph, v, visited);
              }
          }
      }
      
      void DFS(const vector<vector<int>>& graph, int start) {
          int V = graph.size();
          vector<bool> visited(V, false);
          DFSRecursive(graph, start, visited);
      }

四、实际应用与算法

  1. 最短路径问题

    • 问题描述:找到图中两个顶点之间权重总和最小的路径。
    • 算法
      • 无权图:BFS 即可。
      • 带权图 (无负权边)Dijkstra 算法。使用优先队列(最小堆)选择当前距离最小的未访问顶点。
      • 带权图 (允许负权边)Bellman-Ford 算法 。进行 V−1V-1V−1 轮松弛操作。
      • 所有点对间最短路径Floyd-Warshall 算法。动态规划。
  2. 最小生成树 (MST - Minimum Spanning Tree)

    • 问题描述:在连通的无向加权图中,找到一棵树,连接所有顶点,且边的权重总和最小。
    • 算法
      • Prim 算法:从任意顶点开始,每次选择连接当前树与树外顶点且权重最小的边。使用优先队列。
      • Kruskal 算法:将所有边按权重排序,从小到大依次选择,若加入该边不会形成环(使用并查集判断)则加入 MST。
  3. 拓扑排序 (Topological Sorting)

    • 问题描述 :对有向无环图 (DAG) 的所有顶点进行线性排序,使得对于每条有向边 <u,v><u, v><u,v>,uuu 在排序中都出现在 vvv 之前。
    • 应用:任务调度、课程安排、编译依赖。
    • 算法:基于 DFS 或 BFS (Kahn's 算法 - 基于入度)。
  4. 网络流问题

    • 问题描述:建模网络中的流量传输(如水管、交通、数据传输),计算最大流量或最小割。
    • 核心算法Ford-Fulkerson 方法 ,其核心是 Edmonds-Karp 算法(使用 BFS 寻找增广路径)。

五、总结

图论提供了强大的工具来建模复杂关系和解决优化问题。在 C++ 中,邻接矩阵和邻接表是两种核心的存储结构。BFS 和 DFS 是遍历的基础。Dijkstra、Bellman-Ford、Prim、Kruskal、拓扑排序和网络流算法等则解决了图论中的经典问题。理解这些基础理论和掌握它们的 C++ 实现,对于解决涉及网络、关系、路径和优化的实际问题至关重要。

相关推荐
乌萨奇也要立志学C++1 小时前
【Linux】信号量 信号量详解与应用和基于环形队列实现单 / 多生产消费模型
linux·c++
小码过河.1 小时前
设计模式——享元模式
java·设计模式·享元模式
J_liaty1 小时前
深入理解Java反射:原理、应用与最佳实践
java·开发语言·反射
小冷coding1 小时前
【面试】围绕‌服务注册与发现、配置中心、熔断限流、API网关路由‌四大核心组件会面临哪些问题?
java·面试·职场和发展
oioihoii1 小时前
博客与短视频谁更能成就你的个人品牌?
c++
张彦峰ZYF1 小时前
Java+Python双语言开发AI工具全景分析与选型指南
java·人工智能·python
可儿·四系桜1 小时前
Kafka从入门到精通:分布式消息队列实战指南(Zookeeper 模式)
java·开发语言·zookeeper·kafka
仰泳的熊猫1 小时前
题目1109:Hanoi双塔问题
数据结构·c++·算法·蓝桥杯
七夜zippoe1 小时前
Cython终极性能优化指南:从Python到C++的混合编程实战
c++·python·macos·cython·类型系统·内存视图