图论相关整理

图论是计算机科学中的一个重要分支,广泛应用于网络设计、路径规划、社交网络分析等领域。图论相关问题有很多,常见的包括最短路径问题、最小生成树问题、图的遍历等。

1. 最短路径问题

单源最短路径(Single Source Shortest Path)

从一个源点到图中其他所有顶点的最短路径。

  • Dijkstra算法
    适用于边权非负的图。使用优先队列实现,可以达到O((V + E)logV)的时间复杂度。
    Dijkstra算法是一种经典的单源最短路径算法,适用于图中边权非负的情况。它的基本思想是通过贪心策略,逐步扩展最短路径集,从源点开始,逐步找到到其他所有顶点的最短路径。
适用条件
  • 图中的边权必须是非负的。如果图中存在负权边,Dijkstra算法将无法保证找到正确的最短路径。
  • 适用于有向图和无向图。
算法步骤
  1. 初始化

    • 创建一个距离数组dist,初始化为无限大,表示从源点到各个顶点的距离。dist[source]设为0,因为源点到自身的距离为0。
    • 使用一个优先队列(最小堆)来选择当前距离最小的顶点。初始时,将源点(距离为0)加入优先队列。
    • 创建一个布尔数组visited,初始为false,表示各个顶点是否已经确定了最短路径。
  2. 松弛操作

    • 每次从优先队列中取出一个顶点u,表示当前已确定最短路径的顶点。标记u为已访问。
    • 遍历u的所有邻接顶点v,如果通过u到达v的路径比当前已知的最短路径更短,则更新v的最短路径,并将v加入优先队列。
  3. 重复上述步骤

    • 重复步骤2,直到优先队列为空,此时所有顶点的最短路径都已确定。
代码示例(C++)
#include <iostream>
#include <vector>
#include <queue>
#include <utility>
#include <functional>

using namespace std;

typedef pair<int, int> P; // first是最短距离,second是顶点编号

void dijkstra(int source, vector<vector<P>>& graph) {
    int n = graph.size();
    vector<int> dist(n, INT_MAX); // 初始化距离数组
    priority_queue<P, vector<P>, greater<P>> pq; // 优先队列(最小堆)
    
    dist[source] = 0;
    pq.push({0, source}); // 将源点加入优先队列
  ```cpp
  #include <iostream>
  #include <vector>
  #include <queue>
  #include <utility>
  #include <functional>
  
  using namespace std;
  
  typedef pair<int, int> P; // first是最短距离,second是顶点编号
  
  void dijkstra(int source, vector<vector<P>>& graph) {
      int n = graph.size();
      vector<int> dist(n, INT_MAX);
      priority_queue<P, vector<P>, greater<P>> pq;
      
      dist[source] = 0;
      pq.push({0, source});
      
      while (!pq.empty()) {
          int d = pq.top().first;
          int u = pq.top().second;
          pq.pop();
          
          if (d > dist[u]) continue;
          
          for (auto edge : graph[u]) {
              int v = edge.first;
              int weight = edge.second;
              
              if (dist[u] + weight < dist[v]) {
                  dist[v] = dist[u] + weight;
                  pq.push({dist[v], v});
              }
          }
      }
      
      // 输出结果
      for (int i = 0; i < n; ++i) {
          cout << "Vertex " << i << " distance from source " << source << ": " << dist[i] << endl;
      }
  }
  • Bellman-Ford算法
    适用于有负权边的图,但不适用于含负权回路的图。时间复杂度为O(VE)。
    是一种用于计算单源最短路径的算法,特别适用于包含负权边的图。与Dijkstra算法不同,Bellman-Ford算法能够处理负权边,但不适用于含有负权回路(即总权重为负的循环路径)的图。如果图中存在负权回路,Bellman-Ford算法可以检测到。
适用条件
  • 图中可以有负权边,但不能有负权回路。
  • 适用于有向图和无向图。
算法步骤
  1. 初始化

    • 创建一个距离数组dist,初始化为无限大(表示从源点到各个顶点的距离)。dist[source]设为0,因为源点到自身的距离为0。
    • 创建一个边列表edges,包含图中所有的边(包括起点、终点和权重)。
  2. 松弛操作

    • 对于每一条边(u, v, w),如果dist[u] + w < dist[v],则更新dist[v] = dist[u] + w
    • 重复上述步骤V-1次,其中V是顶点的数量。这是因为最短路径最多包含V-1条边。
  3. 检测负权回路

    • 再次遍历所有边,如果存在边(u, v, w)使得dist[u] + w < dist[v],则图中存在负权回路。
算法复杂度
  • 时间复杂度:O(VE),其中V是顶点数,E是边数。
  • 空间复杂度:O(V),用于存储距离数组。
代码示例(C++)
#include <iostream>
#include <vector>
#include <limits.h>

using namespace std;

struct Edge
  ```cpp
  #include <iostream>
  #include <vector>
  
  using namespace std;
  
  struct Edge {
      int u, v, weight;
  };
  
  void bellmanFord(int source, int V, vector<Edge>& edges) {
      vector<int> dist(V, INT_MAX);
      dist[source] = 0;
      
      for (int i = 0; i < V - 1; ++i) {
          for (auto edge : edges) {
              if (dist[edge.u] != INT_MAX && dist[edge.u] + edge.weight < dist[edge.v]) {
                  dist[edge.v] = dist[edge.u] + edge.weight;
              }
          }
      }
      
      // 检测负权回路
      for (auto edge : edges) {
          if (dist[edge.u] != INT_MAX && dist[edge.u] + edge.weight < dist[edge.v]) {
              cout << "Graph contains a negative-weight cycle" << endl;
              return;
          }
      }
      
      // 输出结果
      for (int i = 0; i < V; ++i) {
          cout << "Vertex " << i << " distance from source " << source << ": " << dist[i] << endl;
      }
  }
全源最短路径(All Pairs Shortest Path)

计算图中任意两点之间的最短路径。

  • Floyd-Warshall算法
    时间复杂度为O(V^3),适用于稠密图。
    Floyd-Warshall算法是一种用于计算全源最短路径的动态规划算法,能够在多项式时间内找到图中每对顶点之间的最短路径。它适用于稠密图,即边数接近于顶点数平方的图。
适用条件
  • 适用于有向图和无向图。
  • 可以处理负权边,但要求图中没有负权回路。
  • 适用于稠密图,因为其时间复杂度较高。
算法步骤
  1. 初始化

    • 创建一个二维数组dist,其中dist[i][j]表示从顶点i到顶点j的最短路径长度。初始化为图中对应的边权重,如果没有直接边相连则初始化为无穷大(表示不可达)。对角线上的元素dist[i][i]初始化为0。
  2. 动态规划

    • 使用一个三重循环,依次将每个顶点作为中间点来更新最短路径。
    • 具体步骤是,对于每对顶点ij,通过中间点k来更新它们之间的最短路径。如果dist[i][j] > dist[i][k] + dist[k][j],则更新dist[i][j] = dist[i][k] + dist[k][j]
    • 该过程需要遍历所有的顶点组合,因此算法复杂度为O(V^3)。
  3. 检测负权回路

    • 在算法结束后,检查数组dist的对角线元素dist[i][i]是否有负值。如果存在负值,说明图中存在负权回路。
时间复杂度
  • 时间复杂度:O(V^3),因为有三重嵌套循环,分别遍历所有的顶点。
  • 空间复杂度:O(V^2),因为需要存储每对顶点之间的最短路径长度。
算法优势
  • 能够找到所有顶点对之间的最短路径,适用于全源最短路径问题。
  • 可以处理负权边,只要图中不含负权回路。
应用场景
  • 网络路由:计算网络中任意两点之间的最短路径。
  • 图的闭包计算:确定任意两个顶点之间是否存在路径。
  • 动态规划问题:许多动态规划问题可以转化为最短路径问题。

Floyd-Warshall算法虽然时间复杂度较高,但对于顶点数相对较少且边数较多(稠密图)的情况,是一种有效的全源最短路径算法。由于其简洁性和普适性,它在图论算法中占有重要地位。

#include <iostream>
#include <vector>

using namespace std;

const int INF = 1e9;

void floydWarshall(vector<vector<int>>& graph) {
    int V = graph.size();
    vector<vector<int>> dist = graph;
    
    for (int k = 0; k < V; ++k) {
        for (int i = 0; i < V; ++i) {
            for (int j = 0; j < V; ++j) {
                if (dist[i][k] != INF && dist[k][j] != INF) {
                    dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
                }
            }
        }
    }
    
    // 输出结果
    for (int i = 0; i < V; ++i) {
        for (int j = 0; j < V; ++j) {
            if (dist[i][j] == INF)
                cout << "INF ";
            else
                cout << dist[i][j] << " ";
        }
        cout << endl;
    }
}

2. 最小生成树问题(Minimum Spanning Tree, MST)

  • Kruskal算法
    时间复杂度为O(ElogE),适用于稀疏图。
    Kruskal算法是一种用于寻找加权无向图的最小生成树(MST)的贪心算法。该算法通过逐步选择权重最小的边,确保每次选择的边不会形成环,从而构建出最小生成树。它特别适用于稀疏图,即边数相对较少的图。
适用条件
  • 适用于加权无向图。
  • 图可以是连通的也可以是非连通的。对于非连通图,Kruskal算法会找到最小生成森林。
算法步骤
  1. 初始化

    • 创建一个边列表edges,包含图中所有的边(包括起点、终点和权重)。
    • 对边列表按权重进行升序排序。
  2. 并查集初始化

    • 使用并查集(Union-Find)数据结构来检测和避免环的产生。初始化每个顶点为一个独立的集合。
  3. 选择边构建MST

    • 遍历排序后的边列表,依次选择权重最小的边。如果该边连接的两个顶点属于不同的集合,则将这两个集合合并,并将这条边加入到最小生成树中。
    • 如果边连接的两个顶点属于同一个集合,则忽略这条边,避免形成环。
  4. 终止条件

    • 重复上述步骤直到最小生成树包含的边数等于V-1,其中V是图中的顶点数。
时间复杂度
  • 排序边列表:时间复杂度为O(ElogE),其中E是边数。由于对于连通图,E = O(V^2),所以也可以认为是O(ElogV)。
  • 并查集操作:在路径压缩和按秩合并的优化下,单次操作的时间复杂度接近于O(1),总体复杂度为O(ElogV)。
  • 综合时间复杂度:O(ElogE)。
算法优势
  • 适用于稀疏图:在边数远小于顶点数平方的稀疏图中,Kruskal算法性能较优。
  • 简单易实现:通过排序和并查集操作,算法逻辑清晰明了。
  • 灵活性:适用于连通图和非连通图,能够找到最小生成森林。
应用场景
  • 网络设计:构建最经济的网络连接,如通信网络、交通网络和电力网络。
  • 聚类分析:在数据分析中,将数据点聚类为多个类。
  • 图像处理:用于图像分割等操作。

Kruskal算法在计算最小生成树时,通过选择权重最小的边并使用并查集避免环的产生,以贪心策略确保最终生成的树的权重最小。其高效的性能和广泛的适用性,使其成为解决最小生成树问题的经典算法之一。

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

struct Edge {
    int u, v, weight;
    bool operator<(const Edge& other) const {
        return weight < other.weight;
    }
};

struct UnionFind {
    vector<int> parent, rank;
    UnionFind(int n) : parent(n), rank(n, 0) {
        for (int i = 0; i < n; ++i) parent[i] = i;
    }
    int find(int u) {
        if (u != parent[u]) parent[u] = find(parent[u]);
        return parent[u];
    }
    void unite(int u, int v) {
        int rootU = find(u), rootV = find(v);
        if (rootU != rootV) {
            if (rank[rootU] > rank[rootV]) parent[rootV] = rootU;
            else if (rank[rootU] < rank[rootV]) parent[rootU] = rootV;
            else {
                parent[rootV] = rootU;
                rank[rootU]++;
            }
        }
    }
};

int kruskal(int V, vector<Edge>& edges) {
    sort(edges.begin(), edges.end());
    UnionFind uf(V);
    int mstWeight = 0;
    
    for (auto edge : edges) {
        if (uf.find(edge.u) != uf.find(edge.v)) {
            uf.unite(edge.u, edge.v);
            mstWeight += edge.weight;
            cout << "Edge (" << edge.u << ", " << edge.v << ") with weight " << edge.weight << " included in MST." << endl;
        }
    }
    
    return mstWeight;
}
  • Prim算法
    时间复杂度为O(V^2),使用优先队列优化后为O(ElogV)。Prim算法是一种用于计算加权无向图的最小生成树(MST)的贪心算法。它通过逐步扩展已包含的顶点集合,选择权重最小的边,将新的顶点加入生成树,直到包含所有顶点。相比Kruskal算法,Prim算法在稠密图中表现较优。
适用条件
  • 适用于加权无向图。
  • 图必须是连通的。如果图不连通,Prim算法只能找到连通部分的生成树。
算法步骤
  1. 初始化

    • 选择一个起始顶点,初始化一个顶点集合MST,包含起始顶点。
    • 初始化一个权重数组key,用于记录每个顶点到当前生成树的最小权重边。所有元素初始值为无穷大,起始顶点的权重设为0。
    • 初始化一个父节点数组parent,用于记录每个顶点的父节点。起始顶点的父节点设为-1。
  2. 构建MST

    • key数组中选择权重最小的顶点u,将其加入MST集合,并标记为已访问。
    • 更新u的邻接顶点的key值和parent值。如果某个邻接顶点v尚未包含在MST集合中,且通过uv的边权重小于当前记录的key[v],则更新key[v]为该边权重,并将parent[v]设为u
  3. 重复上述步骤

    • 重复步骤2,直到所有顶点都包含在MST集合中。
时间复杂度
  • 基本实现:如果使用邻接矩阵和简单的线性扫描方式,时间复杂度为O(V^2)。每次选择权重最小的顶点需要O(V)时间,总共进行V次操作。
  • 优先队列优化:如果使用邻接表和二叉堆(优先队列)来优化选择最小权重顶点的过程,时间复杂度可以降低到O(ElogV)。每次选择最小权重顶点需要O(logV)时间,总共进行V次操作。每次更新邻接顶点权重需要O(logV)时间,总共进行E次操作。
算法优势
  • 适用于稠密图:在边数接近于顶点数平方的稠密图中,Prim算法性能较优,特别是在使用优先队列优化的情况下。
  • 逐步构建:算法逐步构建最小生成树,每一步选择当前权重最小的边,易于理解和实现。
  • 空间复杂度低:如果使用邻接表表示图,空间复杂度较低。
应用场景
  • 网络设计:用于设计最经济的网络连接,如通信网络、交通网络和电力网络。
  • 图像处理:用于图像分割和物体识别。
  • 地理信息系统(GIS):用于生成最优的道路网和管道网。

Prim算法在构建最小生成树时,通过逐步扩展顶点集合,并选择权重最小的边,将新的顶点加入生成树。其逐步构建的特点和优先队列优化的高效性,使其在稠密图中表现优异,是解决最小生成树问题的经典算法之一。

#include <iostream>
#include <vector>
#include <queue>
#include <utility>

using namespace std;

typedef pair<int, int> P; // first是权重,second是顶点编号

int prim(int V, vector<vector<P>>& graph) {
    priority_queue<P, vector<P>, greater<P>> pq;
    vector<bool> inMST(V, false);
    vector<int> key(V, INT_MAX);
    int mstWeight = 0;
    
    key[0] = 0;
    pq.push({0, 0});
    
    while (!pq.empty()) {
        int u = pq.top().second;
        pq.pop();
        
        if (inMST[u]) continue;
        inMST[u] = true;
        mstWeight += key[u];
        
        for (auto edge : graph[u]) {
            int v = edge.first;
            int weight = edge.second;
            
            if (!inMST[v] && weight < key[v]) {
                key[v] = weight;
                pq.push({key[v], v});
            }
        }
    }
    
    return mstWeight;
}

3. 图的遍历

图的遍历是图论中的一个基本操作,主要包括深度优先搜索(DFS)和广度优先搜索(BFS)。这两种算法各有优劣,适用于不同的应用场景。

深度优先搜索(DFS)

深度优先搜索(Depth-First Search, DFS)是一种图遍历算法,通过尽可能深地探索图的分支节点,直到无法继续,然后回溯并探索下一个分支。DFS在许多图相关的应用中起着关键作用,如路径查找、连通分量检测、拓扑排序等。

适用条件
  • 适用于有向图和无向图。
  • 可以处理连通图和非连通图。
算法步骤
  1. 初始化

    • 创建一个布尔数组visited,初始化为false,表示所有顶点都未被访问。
    • 对于每个顶点u,如果它未被访问,则从u开始调用DFS递归函数。
  2. DFS递归函数

    • 将当前顶点标记为已访问,并进行相关处理(如输出顶点、记录路径等)。
    • 遍历当前顶点的所有邻接顶点v,如果v未被访问,则递归调用DFS函数,继续从v进行深度优先搜索。
  3. 回溯

    • 当所有邻接顶点都被访问或没有未访问的邻接顶点时,递归返回到上一个顶点,继续探索其他未访问的邻接顶点。
算法复杂度
  • 时间复杂度:O(V + E),其中V是顶点数,E是边数。每个顶点和边都会被访问一次。
  • 空间复杂度:O(V),主要用于递归栈和存储访问状态。
应用场景
  • 路径查找:在迷宫问题或路径查找问题中,通过DFS可以找到从起点到终点的路径。
  • 连通分量检测:在无向图中,通过DFS可以找到所有的连通分量。
  • 拓扑排序:在有向无环图(DAG)中,通过DFS可以进行拓扑排序。
  • 检测环:通过DFS可以检测图中是否存在环。
  • 图的强连通分量:在有向图中,通过DFS可以找到图的强连通分量(如Kosaraju算法)。
广度优先搜索(BFS)

广度优先搜索(Breadth-First Search, BFS)是一种图遍历算法,通过逐层扩展图的顶点,先访问距离源点较近的顶点,再逐步访问距离较远的顶点。BFS常用于计算无权图的最短路径和层次遍历。

适用条件
  • 适用于有向图和无向图。
  • 可以处理连通图和非连通图。
算法步骤
  1. 初始化

    • 创建一个布尔数组visited,初始化为false,表示所有顶点都未被访问。
    • 创建一个队列queue,将源点加入队列,并标记为已访问。
  2. BFS循环

    • 从队列中取出一个顶点u,进行相关处理(如输出顶点、记录路径等)。
    • 遍历顶点u的所有邻接顶点v,如果v未被访问,则将其加入队列,并标记为已访问。
  3. 重复上述步骤

    • 重复步骤2,直到队列为空。
算法复杂度
  • 时间复杂度:O(V + E),其中V是顶点数,E是边数。每个顶点和边都会被访问一次。
  • 空间复杂度:O(V),主要用于队列和存储访问状态。
应用场景
  • 最短路径:在无权图中,通过BFS可以找到从源点到其他顶点的最短路径。
  • 层次遍历:在图的层次遍历中,通过BFS可以按层次顺序访问所有顶点。
  • 连通性检测:在无向图中,通过BFS可以检测所有连通分量。
  • 双连通分量:在有向图中,通过BFS可以找到图的双连通分量。
DFS与BFS的比较
  • DFS(深度优先搜索)

    • 遍历顺序:尽可能深地访问顶点,直到无路可走再回溯。
    • 实现方式:可以使用递归或显式栈。
    • 应用场景:路径查找、连通分量检测、拓扑排序、环检测、强连通分量。
    • 时间复杂度:O(V + E)。
    • 空间复杂度:O(V)(主要用于递归栈)。
  • BFS(广度优先搜索)

    • 遍历顺序:逐层访问顶点,从距离源点最近的顶点开始,逐步扩展。
    • 实现方式:使用队列。
    • 应用场景:无权图的最短路径、层次遍历、连通性检测、双连通分量。
    • 时间复杂度:O(V + E)。
    • 空间复杂度:O(V)(主要用于队列)。
注意事项
  • DFS递归深度限制:在递归实现DFS时,需要注意递归深度限制。在图的深度较深时,可能会导致栈溢出。这种情况下,可以选择迭代实现。
  • 访问标记:确保每个顶点只被访问一次,避免无限循环。在递归或迭代过程中,及时更新访问状态。

DFS和BFS是图遍历的两种基本算法,各自适用于不同的应用场景。理解和掌握这两种算法的原理和实现,对于解决复杂的图论问题至关重要。

深度优先搜索(DFS)

DFS用于遍历或搜索图中的所有顶点。时间复杂度为O(V + E)。

#include <iostream>
#include <vector>

using namespace std;

void DFS(int u, vector<vector<int>>& adj, vector<bool>& visited) {
    visited[u] = true;
    cout << u << " ";
    
    for (int v : adj[u]) {
        if (!visited[v]) {
            DFS(v, adj, visited);
        }
    }
}

int main() {
    int V = 5;
    vector<vector<int>> adj(V);
    adj[0] = {1, 2};
    adj[1] = {0, 3, 4};
    adj[2] = {0};
    adj[3] = {1};
    adj[4] = {1};
    
    vector<bool> visited(V, false);
    cout << "DFS traversal: ";
    DFS(0, adj, visited);
    return 0;
}
广度优先搜索(BFS)

BFS用于遍历或搜索图中的所有顶点。时间复杂度为O(V + E)。

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

void BFS(int u, vector<vector<int>>& adj) {
    vector<bool> visited(adj.size(), false);
    queue<int>

 q;
    visited[u] = true;
    q.push(u);
    
    while (!q.empty()) {
        int v = q.front();
        q.pop();
        cout << v << " ";
        
        for (int w : adj[v]) {
            if (!visited[w]) {
                visited[w] = true;
                q.push(w);
            }
        }
    }
}

int main() {
    int V = 5;
    vector<vector<int>> adj(V);
    adj[0] = {1, 2};
    adj[1] = {0, 3, 4};
    adj[2] = {0};
    adj[3] = {1};
    adj[4] = {1};
    
    cout << "BFS traversal: ";
    BFS(0, adj);
    return 0;
}

4. 拓扑排序(Topological Sorting)

拓扑排序适用于有向无环图(DAG),是一种线性排序,使得对于每一条有向边(u, v),顶点u在顶点v之前。

  • Kahn算法:基于入度的拓扑排序。时间复杂度为O(V + E)。

    #include <iostream>
    #include <vector>
    #include <queue>
    
    using namespace std;
    
    void topologicalSort(int V, vector<vector<int>>& adj) {
        vector<int> inDegree(V, 0);
        for (int u = 0; u < V; ++u) {
            for (int v : adj[u]) {
                inDegree[v]++;
            }
        }
        
        queue<int> q;
        for (int i = 0; i < V; ++i) {
            if (inDegree[i] == 0) {
                q.push(i);
            }
        }
        
        int count = 0;
        vector<int> topoOrder;
        
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            topoOrder.push_back(u);
            
            for (int v : adj[u]) {
                if (--inDegree[v] == 0) {
                    q.push(v);
                }
            }
            count++;
        }
        
        if (count != V) {
            cout << "There exists a cycle in the graph" << endl;
            return;
        }
        
        for (int i : topoOrder) {
            cout << i << " ";
        }
        cout << endl;
    }
    
  • DFS算法:基于DFS的拓扑排序。时间复杂度为O(V + E)。

    #include <iostream>
    #include <vector>
    #include <stack>
    
    using namespace std;
    
    void topologicalSortDFS(int u, vector<vector<int>>& adj, vector<bool>& visited, stack<int>& Stack) {
        visited[u] = true;
        
        for (int v : adj[u]) {
            if (!visited[v]) {
                topologicalSortDFS(v, adj, visited, Stack);
            }
        }
        
        Stack.push(u);
    }
    
    void topologicalSort(int V, vector<vector<int>>& adj) {
        stack<int> Stack;
        vector<bool> visited(V, false);
        
        for (int i = 0; i < V; ++i) {
            if (!visited[i]) {
                topologicalSortDFS(i, adj, visited, Stack);
            }
        }
        
        while (!Stack.empty()) {
            cout << Stack.top() << " ";
            Stack.pop();
        }
        cout << endl;
    }
    

5. 强连通分量(Strongly Connected Components, SCC)

SCC适用于有向图,用于找到图中的强连通分量,每个强连通分量是一个子图,其中任意两个顶点是互相可达的。

  • Kosaraju算法
    时间复杂度为O(V + E)。Kosaraju算法是一种用于找到有向图中所有强连通分量(SCC)的高效算法。其时间复杂度为O(V + E),其中V是顶点数,E是边数。该算法基于两次深度优先搜索(DFS)。
算法步骤
  1. 第一次DFS,记录顶点的完成时间

    • 对图进行第一次DFS遍历,并在每个顶点完成时将其记录在栈中。这一步的目的是获取顶点的完成时间顺序,用于后续步骤。
  2. 反转图(Transpose Graph)

    • 构建原图的转置图,将所有边的方向进行反转。转置图的边(u, v)变为(v, u)。
  3. 第二次DFS,确定SCC

    • 对转置图进行DFS遍历,但这次是按照栈中的顶点顺序(即根据第一次DFS的完成时间,从最后完成的顶点开始)。每次遍历得到的顶点集即为一个强连通分量。
详细步骤
  1. 第一次DFS

    • 初始化一个布尔数组visited,表示顶点是否已被访问。
    • 遍历所有顶点,对未访问的顶点执行DFS。在DFS过程中,记录每个顶点的完成时间,并将其压入栈中。
  2. 构建转置图

    • 创建转置图的邻接表,将原图中的每条边(u, v)反转为(v, u)。
  3. 第二次DFS

    • 再次初始化一个布尔数组visited,表示顶点是否已被访问。
    • 按照第一次DFS完成时间的逆序,从栈顶开始对转置图进行DFS。每次遍历得到的顶点集是一个强连通分量,将其输出或存储。
算法复杂度
  • 时间复杂度:O(V + E)。第一次DFS、构建转置图和第二次DFS都需要O(V + E)的时间。
  • 空间复杂度:O(V + E)。需要额外的空间来存储图的转置、访问状态以及栈。
应用场景
  • 程序优化:在编译器中,通过识别程序中的强连通分量,可以进行代码优化。
  • 网络分析:在社交网络中,识别用户群体(强连通分量),分析群体内部的相互影响和传播路径。
  • 电路设计:在电路设计中,通过识别强连通分量,可以优化电路布局和设计。

Kosaraju算法通过两次深度优先搜索(DFS)有效地找到了有向图中的所有强连通分量。第一次DFS记录顶点的完成时间,转置图反转所有边的方向,第二次DFS按照顶点的完成时间逆序来确定强连通分量。其时间复杂度为O(V + E),适用于各种实际应用中需要识别强连通分量的问题。

#include <iostream>
#include <vector>
#include <stack>

using namespace std;

void DFS(int u, vector<vector<int>>& adj, vector<bool>& visited, stack<int>& Stack) {
    visited[u] = true;
    for (int v : adj[u]) {
        if (!visited[v]) {
            DFS(v, adj, visited, Stack);
        }
    }
    Stack.push(u);
}

void reverseDFS(int u, vector<vector<int>>& adj, vector<bool>& visited) {
    visited[u] = true;
    cout << u << " ";
    for (int v : adj[u]) {
        if (!visited[v]) {
            reverseDFS(v, adj, visited);
        }
    }
}

void findSCCs(int V, vector<vector<int>>& adj) {
    stack<int> Stack;
    vector<bool> visited(V, false);
    
    // 1. 按照DFS完成时间顺序,将顶点压入栈中
    for (int i = 0; i < V; ++i) {
        if (!visited[i]) {
            DFS(i, adj, visited, Stack);
        }
    }
    
    // 2. 反转图
    vector<vector<int>> adjRev(V);
    for (int u = 0; u < V; ++u) {
        for (int v : adj[u]) {
            adjRev[v].push_back(u);
        }
    }
    
    fill(visited.begin(), visited.end(), false);
    
    // 3. 按照栈中元素的顺序,对反转图进行DFS
    while (!Stack.empty()) {
        int u = Stack.top();
        Stack.pop();
        
        if (!visited[u]) {
            reverseDFS(u, adjRev, visited);
            cout << endl;
        }
    }
}

总结

图论中的常见问题和算法主要包括最短路径问题、最小生成树问题、图的遍历、拓扑排序和强连通分量。每种问题都有适合的算法,理解这些算法的原理和应用场景是解决图论问题的关键。

  • 28
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值