C++面试:熟悉图论算法(dijkstra算法、最小生成树、深度优先搜索等)

        熟悉图论算法是对于准备C++后台开发岗位面试非常重要的一部分。我将为你概述Dijkstra算法、最小生成树算法以及深度优先搜索(DFS),这些都是图论中常用的算法。

目录

1. Dijkstra算法

代码解释

运行示例

2. 最小生成树算法

    1. Kruskal算法

2. Prim算法

代码解释

3. 深度优先搜索(DFS)

代码解释

4. 广度优先搜索(BFS)

代码解释

运行示例

5. A* 搜索算法

代码解释

运行示例

6. Floyd-Warshall 算法

代码解释

运行示例

7. Tarjan算法

代码解释

运行示例

8. Bellman-Ford 算法

代码解释

运行示例

9. 拓扑排序

10. 网络流算法

代码解释

运行示例

代码解释

运行示例

总结


1. Dijkstra算法

  • 用途: 用于在加权图中找到一个顶点到图中所有其他顶点的最短路径。
  • 原理: 算法采用了贪心的策略,每次找到距离起始点最近的一个顶点,然后以该顶点为中介,更新其余所有顶点的最短路径。
  • 特点: 仅适用于边权重非负的图。

        以下是Dijkstra算法的一个C++实现示例,我将包含详细的注释来解释每个关键部分。这个实现使用了优先队列来优化效率,它适用于边权重非负的图。

#include 
#include 
#include 
#include 

using namespace std;

// 定义边的结构
struct Edge {
    int to;     // 边指向的顶点
    int weight; // 边的权重
    Edge(int t, int w) : to(t), weight(w) {}
};

// 定义用于优先队列的比较函数
class Compare {
public:
    bool operator() (pair& p1, pair& p2) {
        return p1.second > p2.second;
    }
};

void dijkstra(const vector>& graph, int start) {
    int n = graph.size();
    vector dist(n, INT_MAX); // 存储起始点到所有点的最短距离
    vector visited(n, false); // 标记顶点是否访问过

    priority_queue, vector>, Compare> pq;

    // 从起始点开始
    pq.push({start, 0});
    dist[start] = 0;

    while (!pq.empty()) {
        int current = pq.top().first;
        pq.pop();

        if (visited[current]) continue;
        visited[current] = true;

        // 遍历当前顶点的所有邻接边
        for (const Edge& edge : graph[current]) {
            int next = edge.to;
            int nextDist = dist[current] + edge.weight;

            // 如果找到更短的路径,则更新
            if (nextDist < dist[next]) {
                dist[next] = nextDist;
                pq.push({next, nextDist});
            }
        }
    }

    // 打印最短路径结果
    for (int i = 0; i < n; ++i) {
        if (dist[i] == INT_MAX)
            cout << "Vertex " << i << ": unreachable" << endl;
        else
            cout << "Vertex " << i << ": " << dist[i] << endl;
    }
}

int main() {
    // 创建一个示例图
    int n = 5; // 图中顶点数
    vector> graph(n);

    // 添加边
    graph[0].emplace_back(1, 2);
    graph[0].emplace_back(3, 6);
    graph[1].emplace_back(2, 3);
    graph[1].emplace_back(3, 8);
    graph[1].emplace_back(4, 5);
    graph[2].emplace_back(4, 7);
    graph[3].emplace_back(4, 9);

    // 从顶点0开始应用Dijkstra算法
    dijkstra(graph, 0);

    return 0;
}
代码解释
  • 数据结构: 使用vector>来表示图,每个顶点都有一个边的列表。
  • 边权重: Edge结构体包含了目标顶点和边的权重。
  • 优先队列: 用于存储待访问的顶点及其从起点到该点的路径长度。
  • Dijkstra算法实现:
    • 使用dist数组来跟踪从起点到每个顶点的最短距离。
    • 使用visited数组来标记每个顶点是否已被处理。
    • 通过优先队列优化选择下一个要处理的顶点的过程。
    • 遍历每个顶点的所有邻接边,更新其最短路径。
运行示例
  • 这段代码定义了一个有5个顶点的图,并为其添加了一些边和权重。
  • 应用Dijkstra算法计算从顶点0到所有其他顶点的最短路径,并打印结果。

2. 最小生成树算法

  • 用途: 在一个加权连通图中选取一部分边,构成一棵包含所有顶点的树,并且使边的总权重尽可能小。
  • 常用算法:
    • Kruskal算法: 按照边的权重从小到大的顺序选择边,确保选择的边不会与已选择的边形成环。
    • Prim算法: 从一个顶点开始,每一步添加一条连接已选顶点集合和未选顶点集合的最小权重边。
    1. Kruskal算法

        Kruskal算法使用并查集(Union-Find)结构来避免环的形成。

#include 
#include 
#include 

using namespace std;

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

struct DisjointSets {
    vector parent, rank;
    int n;

    DisjointSets(int n) {
        this->n = n;
        parent.resize(n);
        rank.resize(n);

        for (int i = 0; i < n; i++)
            parent[i] = i;
    }

    int find(int i) {
        if (parent[i] != i)
            parent[i] = find(parent[i]);
        return parent[i];
    }

    void merge(int x, int y) {
        x = find(x), y = find(y);

        if (rank[x] > rank[y])
            parent[y] = x;
        else
            parent[x] = y;

        if (rank[x] == rank[y])
            rank[y]++;
    }
};

void KruskalMST(vector& edges, int V) {
    sort(edges.begin(), edges.end());
    DisjointSets ds(V);

    vector result;

    for (auto edge : edges) {
        int x = ds.find(edge.src);
        int y = ds.find(edge.dest);

        if (x != y) {
            result.push_back(edge);
            ds.merge(x, y);
        }
    }

    // 打印MST
    for (auto e : result)
        cout << e.src << " - " << e.dest << " : " << e.weight << endl;
}

int main() {
    int V = 4; // 顶点数量
    vector edges = {
        {0, 1, 10},
        {0, 2, 6},
        {0, 3, 5},
        {1, 3, 15},
        {2, 3, 4}
    };

    KruskalMST(edges, V);
    return 0;
}

2. Prim算法

        Prim算法使用优先队列来选择最小权重的边。

#include 
#include 
#include 
#include 

using namespace std;

typedef pair iPair; // 用于表示权重和顶点的对

void PrimMST(vector>& graph, int V) {
    priority_queue, greater> pq;
    int src = 0; // 可以从任何顶点开始

    vector key(V, INT_MAX);
    vector inMST(V, false);
    vector parent(V, -1);

    pq.push(make_pair(0, src));
    key[src] = 0;

    while (!pq.empty()) {
        int u = pq.top().second;
        pq.pop();

        inMST[u] = true;

        for (auto i : graph[u]) {
            int v = i.first;
            int weight = i.second;

            if (!inMST[v] && key[v] > weight) {
                key[v] = weight;
                pq.push(make_pair(key[v], v));
                parent[v] = u;
            }
        }
    }

    // 打印构造的MST
    for (int i = 1; i < V; ++i)
        cout << parent[i] << " - " << i << " : " << key[i] << endl;
}

int main() {
    int V = 4;
    vector> graph(V);

    // 构造图
    graph[0].push_back(make_pair(1, 10));
    graph[0].push_back(make_pair(2, 6));
    graph[0].push_back(make_pair(3, 5));
    graph[1].push_back(make_pair(3, 15));
    graph[2].push_back(make_pair(3, 4));

    PrimMST(graph, V);

    return 0;
}
代码解释
  • Kruskal算法:
    • 使用并查集来维护不同集合,并确保添加的边不会形成环。
    • 将所有的边按照权重进行排序,然后按顺序加入边,如果加入该边不会形成环,则该边是MST的一部分。
  • Prim算法:
    • 使用优先队列来存储和选择当前最小权重的边。
    • 从一个顶点开始,重复选择连接已选择顶点和未选择顶点的最小权重边,直到覆盖所有顶点。

        这两种算法在不同情况下都很有用。Kruskal算法适合边比较稀疏的图,而Prim算法适合边比较密集的图。在准备面试时,理解这两种算法的适用场景和优缺点是很重要的。

3. 深度优先搜索(DFS)

  • 用途: 用于遍历或搜索树或图的结构。深度优先搜索沿着树的深度遍历树的节点,尽可能深地搜索树的分支。
  • 原理: 从一个顶点开始,沿当前顶点的边走到未访问的顶点,当没有未访问的顶点时,回溯到上一个顶点,继续搜索直到所有顶点都被访问。
  • 应用: 解决迷宫问题、路径查找、拓扑排序等。

        深度优先搜索(DFS)是一种用于遍历或搜索树或图的算法。它的基本思想是从一个顶点开始,沿着边到达新的顶点,并沿此路径尽可能深地搜索,直到找不到新的未访问顶点为止,然后回溯并继续搜索。以下是DFS算法的C++实现示例,包含详细注释。

#include 
#include 
#include 

using namespace std;

class Graph {
    int V;    // 顶点的数量
    list *adj; // 邻接表

    // DFS递归辅助函数
    void DFSUtil(int v, vector& visited) {
        // 标记当前节点为已访问
        visited[v] = true;
        cout << v << " ";

        // 递归访问所有未访问的邻接顶点
        for (auto i = adj[v].begin(); i != adj[v].end(); ++i) {
            if (!visited[*i]) {
                DFSUtil(*i, visited);
            }
        }
    }

public:
    Graph(int V) {
        this->V = V;
        adj = new list[V];
    }

    // 添加边到图中
    void addEdge(int v, int w) {
        adj[v].push_back(w); // 添加w到v的列表中
    }

    // DFS遍历函数
    void DFS(int v) {
        // 初始化所有顶点为未访问
        vector visited(V, false);

        // 从顶点v开始DFS遍历
        DFSUtil(v, visited);
    }
};

int main() {
    // 创建一个图
    Graph g(4);

    // 添加边
    g.addEdge(0, 1);
    g.addEdge(0, 2);
    g.addEdge(1, 2);
    g.addEdge(2, 0);
    g.addEdge(2, 3);
    g.addEdge(3, 3);

    // 从顶点2开始DFS遍历
    cout << "Starting DFS from vertex 2" << endl;
    g.DFS(2);

    return 0;
}
代码解释
  • 图的表示:这个示例中,图用邻接表的方式表示。Graph类中的adj是一个指向整数列表的数组,表示图的邻接表。
  • 添加边addEdge函数用于向图中添加边。
  • DFS实现
    • DFS函数初始化一个布尔类型的数组来跟踪访问过的顶点。
    • DFSUtil是一个递归函数,用于实际进行DFS遍历。它访问一个顶点,然后对于每一个邻接的未访问顶点,递归调用自身。

        请注意,DFS的特点是它可能不会访问图中的所有顶点,尤其是在图不是完全连通的情况下。要遍历所有顶点,可能需要从不同的顶点分别启动DFS。此外,DFS在实际应用中有多种变体,例如用于路径查找、拓扑排序或解决迷宫问题。在准备面试时,理解这些变体并知道如何实现它们是很有帮助的。

4. 广度优先搜索(BFS)

  • 用途: 主要用于在图中进行层级搜索,适用于找到最短路径问题。
  • 原理: 从一个顶点开始,先访问所有邻接的顶点,再从这些邻接顶点出发,访问它们的邻接顶点,以此类推。

        广度优先搜索(BFS)是图和树的一种遍历算法,它从一个起始顶点开始,先访问所有相邻的顶点,然后再逐层访问更远的顶点。它特别适用于找到从源点到其他顶点的最短路径。以下是广度优先搜索的C++实现,包含详细注释:

#include 
#include 
#include 
#include 

using namespace std;

class Graph {
    int V;    // 顶点的数量
    list *adj; // 邻接表

public:
    Graph(int V) {
        this->V = V;
        adj = new list[V];
    }

    // 添加边到图中
    void addEdge(int v, int w) {
        adj[v].push_back(w); // 在v的列表中添加w
    }

    // BFS遍历函数
    void BFS(int s) {
        // 初始化所有顶点为未访问
        vector visited(V, false);

        // 创建一个队列用于BFS
        queue queue;

        // 标记当前节点为已访问并入队
        visited[s] = true;
        queue.push(s);

        while (!queue.empty()) {
            // 出队一个顶点并打印
            s = queue.front();
            cout << s << " ";
            queue.pop();

            // 获取所有邻接的顶点
            for (auto i = adj[s].begin(); i != adj[s].end(); ++i) {
                if (!visited[*i]) {
                    visited[*i] = true;
                    queue.push(*i);
                }
            }
        }
    }
};

int main() {
    // 创建一个图
    Graph g(4);

    // 添加边
    g.addEdge(0, 1);
    g.addEdge(0, 2);
    g.addEdge(1, 2);
    g.addEdge(2, 0);
    g.addEdge(2, 3);
    g.addEdge(3, 3);

    // 从顶点2开始BFS遍历
    cout << "Starting BFS from vertex 2" << endl;
    g.BFS(2);

    return 0;
}
代码解释
  • 图的表示:图用邻接表的形式表示。Graph类中的adj是一个指向整数列表的数组,表示图的邻接表。
  • 添加边addEdge函数向图中添加边。
  • BFS实现
    • BFS函数初始化一个布尔类型的数组来跟踪访问过的顶点。
    • 使用队列来支持BFS。先将起始顶点加入队列,然后循环直到队列为空。在循环中,从队列中取出一个顶点,访问它的所有未访问的邻接顶点,并将这些邻接顶点加入队列。
运行示例
  • 这个程序创建了一个包含四个顶点的图,并添加了一些边。
  • 然后,它从顶点2开始进行BFS遍历,并打印访问的顶点顺序。

        BFS通常用于找到从一个顶点到另一个顶点的最短路径,尤其是在未加权图中。在准备面试时,理解BFS的工作原理并能够根据需要修改和应用它是很重要的。

5. A* 搜索算法

  • 用途: 用于图中的路径寻找和图遍历,特别是在有成本最小化要求的情况下。
  • 原理: 结合了Dijkstra算法和启发式方法(如曼哈顿距离、欧几里得距离)来找到从起始点到目标点的最低成本路径。

        A搜索算法是一种有效的路径查找和图遍历算法,它结合了Dijkstra算法的确保最短路径的优点和启发式搜索(如贪心算法)的高效性。A算法使用启发式函数来估算从当前节点到目标节点的最佳路径成本,通常用于求解复杂的路径规划问题。以下是A*算法的C++实现示例,包含详细注释: 

#include 
#include 
#include 
#include 
#include 

using namespace std;

// 定义坐标点
struct Point {
    int x, y;
    Point(int x, int y) : x(x), y(y) {}
};

// 计算两点之间的欧几里得距离,用作启发式函数
double heuristic(Point a, Point b) {
    return sqrt(pow(a.x - b.x, 2) + pow(a.y - b.y, 2));
}

// 定义节点,用于A*搜索
struct Node {
    Point point; // 节点的坐标
    double f, g, h; // f = g + h
    Node *parent; // 指向父节点的指针

    Node(Point pt, double g, double h, Node *parent = nullptr) 
        : point(pt), g(g), h(h), f(g + h), parent(parent) {}

    // 重载<运算符,用于优先队列
    bool operator<(const Node& other) const {
        return f > other.f;
    }
};

// A*搜索算法实现
vector AStarSearch(vector>& grid, Point start, Point end) {
    priority_queue openSet; // 开放列表
    vector> closedSet(grid.size(), vector(grid[0].size(), false)); // 关闭列表

    openSet.push(Node(start, 0, heuristic(start, end)));

    while (!openSet.empty()) {
        Node current = openSet.top();
        openSet.pop();

        // 检查是否到达终点
        if (current.point.x == end.x && current.point.y == end.y) {
            vector path;
            while (current.parent != nullptr) {
                path.push_back(current.point);
                current = *current.parent;
            }
            reverse(path.begin(), path.end());
            return path;
        }

        closedSet[current.point.x][current.point.y] = true;

        // 检查邻居
        vector neighbors = {/* 上下左右四个方向的邻居坐标 */};
        for (Point& neighbor : neighbors) {
            if (closedSet[neighbor.x][neighbor.y] || grid[neighbor.x][neighbor.y] == 0) {
                continue; // 跳过障碍物或已经在关闭列表中的节点
            }

            double tentative_g = current.g + heuristic(current.point, neighbor);

            // 添加到开放列表
            if (!closedSet[neighbor.x][neighbor.y]) {
                openSet.push(Node(neighbor, tentative_g, heuristic(neighbor, end), new Node(current)));
            }
        }
    }

    return vector(); // 如果找不到路径,返回空路径
}

int main() {
    // 创建网格地图,1表示可通过,0表示障碍物
    vector> grid = {
        {1, 1, 1, 1},
        {1, 0, 1, 1},
        {1, 1, 1, 1},
        {1, 1, 1, 1}
    };

    // 设置起点和终点
    Point start(0, 0), end(3, 3);

    // 执行A*搜索
    vector path = AStarSearch(grid, start, end);

    // 打印路径
    for (Point p : path) {
        cout << "(" << p.x << ", " << p.y << ") ";
    }

    return 0;
}
代码解释
  • 启发式函数:这里使用的是欧几里得距离,你也可以使用曼哈顿距离或其他合适的启发式函数。
  • Node结构:每个节点包含其在网格中的坐标、从起点到该点的实际成本(g)、从该点到终点的估计成本(h)以及两者之和(f)。
  • 开放列表和关闭列表:开放列表(openSet)存储待访问的节点,关闭列表(closedSet)用于跟踪已访问的节点。
  • 邻居节点:算法检查当前节点的所有邻居,然后计算每个邻居的成本,将其添加到开放列表中。
运行示例
  • 这个程序创建了一个简单的网格地图,并设置了起点和终点。
  • 然后,它使用A*搜索算法找到从起点到终点的路径,并打印路径。

        请注意,这个示例是A算法的一个基本实现。在复杂的实际应用中,比如大型地图或动态变化的环境中,可能需要对算法进行优化和调整。在准备面试时,理解A算法的原理和实现方式非常重要。

6. Floyd-Warshall 算法

  • 用途: 解决所有顶点对的最短路径问题。
  • 原理: 通过考虑所有可能的路径中间点,系统地减少最短路径的估计。

        Floyd-Warshall算法是一种计算图中所有顶点对之间最短路径的算法。它能够处理包含负权边的图(但不允许负权回路)。该算法通过动态规划逐步构建每对顶点间的最短路径。以下是Floyd-Warshall算法的C++实现,包含详细注释:

#include 
#include 
#include 

using namespace std;

#define INF INT_MAX

void FloydWarshall(vector>& graph) {
    int V = graph.size();
    vector> dist = graph;

    // 通过每个顶点k,尝试更新每对顶点i和j之间的最短路径
    for (int k = 0; k < V; k++) {
        for (int i = 0; i < V; i++) {
            for (int j = 0; j < V; j++) {
                // 如果i到k和k到j的路径都存在
                if (dist[i][k] != INF && dist[k][j] != INF) {
                    // 更新i到j的最短路径
                    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;
    }
}

int main() {
    // 创建一个图的邻接矩阵表示
    vector> graph = {
        {0, 5, INF, 10},
        {INF, 0, 3, INF},
        {INF, INF, 0, 1},
        {INF, INF, INF, 0}
    };

    // 执行Floyd-Warshall算法
    FloydWarshall(graph);

    return 0;
}
代码解释
  • 图的表示:图用邻接矩阵表示,其中graph[i][j]表示顶点i到顶点j的边的权重,如果i和j之间没有直接的边,则为INF(无穷大)。
  • 初始化距离矩阵dist数组初始化为图的邻接矩阵。
  • 动态规划:通过三重循环逐渐更新每对顶点之间的最短路径。对于每个顶点k,算法尝试通过顶点k更新所有顶点对i和j之间的最短路径。
  • 输出结果:打印出最终的最短路径矩阵,其中dist[i][j]表示顶点i到顶点j的最短路径长度。
运行示例
  • 这个程序创建了一个含有4个顶点的图,并以邻接矩阵形式表示。
  • 然后,它使用Floyd-Warshall算法计算图中所有顶点对之间的最短路径,并打印结果。

        在准备面试时,了解Floyd-Warshall算法的原理和应用是很重要的,尤其是在处理复杂图论问题时。这个算法虽然简单,但对于理解动态规划和图的最短路径问题非常有帮助。

7. Tarjan算法

  • 用途: 用于寻找图中的强连通分量。
  • 原理: 基于深度优先搜索,通过管理一个索引堆栈,能够有效地识别出强连通分量。

        Tarjan算法是图论中的一个重要算法,它不仅用于识别强连通分量,还可以应用于其他问题,如桥的检测和割点的识别。在准备面试时,理解这个算法的工作原理并能够实现它对于展示你的算法和数据结构知识非常有帮助。 

        Tarjan算法是一种基于深度优先搜索(DFS)的图算法,用于找出有向图中的所有强连通分量。算法维护一个堆栈来追踪已访问的顶点,并使用两个数组来记录每个顶点的访问顺序和可回溯到的最早顶点。以下是Tarjan算法的C++实现,包括详细注释: 

#include 
#include 
#include 
#include 

using namespace std;

class Graph {
    int V;    // 顶点数
    list *adj; // 邻接表

    void tarjanUtil(int u, vector& disc, vector& low, stack& st, vector& stackMember) {
        static int time = 0;

        // 初始化当前节点
        disc[u] = low[u] = ++time;
        st.push(u);
        stackMember[u] = true;

        // 遍历所有邻接顶点
        for (int v : adj[u]) {
            // 如果v未被访问,递归调用
            if (disc[v] == -1) {
                tarjanUtil(v, disc, low, st, stackMember);

                // 检查子树中是否有回路
                low[u] = min(low[u], low[v]);
            }
            // 如果v在栈中,更新u的low值
            else if (stackMember[v] == true) {
                low[u] = min(low[u], disc[v]);
            }
        }

        // 如果u是强连通分量的根,提取整个分量
        int w = 0;  // To store stack extracted vertices
        if (low[u] == disc[u]) {
            while (st.top() != u) {
                w = (int) st.top();
                cout << w << " ";
                stackMember[w] = false;
                st.pop();
            }
            w = (int) st.top();
            cout << w << "\n";
            stackMember[w] = false;
            st.pop();
        }
    }

public:
    Graph(int V) {
        this->V = V;
        adj = new list[V];
    }

    void addEdge(int v, int w) { adj[v].push_back(w); }

    // The function to find SCCs
    void tarjanSCC() {
        vector disc(V, -1), low(V, -1);
        vector stackMember(V, false);
        stack st;

        // Call the recursive helper function to find SCCs
        for (int i = 0; i < V; i++)
代码解释
  • 图的表示:图使用邻接表表示,其中adj是一个指向整数列表的数组。
  • Tarjan算法的核心tarjanUtil函数实现了Tarjan算法的主要逻辑。它使用时间戳来标记每个顶点的访问时间,用low数组来跟踪每个节点可以回溯到的最早访问节点。
  • 强连通分量的提取:当low[u] == disc[u]时,从堆栈中提取出一个完整的强连通分量。
运行示例
  • 这个程序创建了一个含有5个顶点的图,并添加了一些边。
  • 然后,它使用Tarjan算法找到并打印图中的所有强连通分量。

8. Bellman-Ford 算法

  • 用途: 在带权图中计算单源最短路径,特别是处理图中包含负权边的情况。
  • 原理: 通过松弛操作重复地减小从源点到所有其他顶点的距离估计。

        Bellman-Ford 算法是一种在带权图中计算单源最短路径的算法,尤其适用于含有负权边的图。该算法通过重复地松弛所有边,来逐步减小从源点到所有其他顶点的距离估计,直到这些估计不再改变。以下是Bellman-Ford 算法的 C++ 实现,包括详细注释:

#include 
#include 
#include 

using namespace std;

// 定义边的结构
struct Edge {
    int src, dest, weight;
};

class Graph {
    int V, E;    // 图中的顶点数和边数
    vector edges; // 边的列表

public:
    Graph(int V, int E) {
        this->V = V;
        this->E = E;
    }

    // 向图中添加边
    void addEdge(int u, int v, int w) {
        edges.push_back({u, v, w});
    }

    // Bellman-Ford算法实现
    void BellmanFord(int src) {
        vector dist(V, INT_MAX);
        dist[src] = 0;

        // 对每条边进行V-1次松弛操作
        for (int i = 1; i <= V - 1; i++) {
            for (int j = 0; j < E; j++) {
                int u = edges[j].src;
                int v = edges[j].dest;
                int weight = edges[j].weight;
                if (dist[u] != INT_MAX && dist[u] + weight < dist[v])
                    dist[v] = dist[u] + weight;
            }
        }

        // 检查负权回路
        for (int i = 0; i < E; i++) {
            int u = edges[i].src;
            int v = edges[i].dest;
            int weight = edges[i].weight;
            if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {
                cout << "Graph contains negative weight cycle" << endl;
                return;
            }
        }

        // 打印最短路径
        for (int i = 0; i < V; i++)
            cout << "Distance from " << src << " to " << i << " is " << dist[i] << endl;
    }
};

int main() {
    int V = 5; // 顶点数
    int E = 8; // 边数
    Graph g(V, E);

    // 向图中添加边
    g.addEdge(0, 1, -1);
    g.addEdge(0, 2, 4);
    g.addEdge(1, 2, 3);
    g.addEdge(1, 3, 2);
    g.addEdge(1, 4, 2);
    g.addEdge(3, 2, 5);
    g.addEdge(3, 1, 1);
    g.addEdge(4, 3, -3);

    // 从顶点 0 执行Bellman-Ford算法
    g.BellmanFord(0);

    return 0;
}
代码解释
  • 图的表示:图使用边的列表表示,每个边包含源顶点、目标顶点和权重。
  • Bellman-Ford 算法实现
    • 初始化所有顶点的距离为无穷大,除了源顶点的距离为0。
    • 对所有边执行 V-1 次松弛操作,更新每个顶点的距离估计。
    • 再次检查所有边,确认是否存在负权回路。
  • 打印结果:打印从源顶点到所有其他顶点的最短路径。
运行示例
  • 这个程序创建了一个含有5个顶点和8条边的图。
  • 然后,它使用 Bellman-Ford 算法从顶点0开始计算最短路径,并打印结果。

      Bellman-Ford 算法是理解动态规划在图论中应用的一个很好的例子。尽管它的时间复杂度高于 Dijkstra 算法,但它能处理包含负权边的图。在准备面试时,理解和能够实现这个算法是很重要的。

9. 拓扑排序

  • 用途: 对有向无环图(DAG)进行排序,以线性顺序表示图中所有顶点的前后关系。
  • 原理: 基于深度优先搜索或入度表(每个节点入度的计数)。

10. 网络流算法

  • 用途: 用于求解网络流问题,如最大流问题。
  • 常用算法: Ford-Fulkerson 算法、Edmonds-Karp 算法等。

        网络流问题,特别是最大流问题,在图论中是一个重要的主题。Ford-Fulkerson 算法是解决这类问题的一种常用方法,它通过不断查找增广路径来增加流量,直到达到最大流。Edmonds-Karp 算法是 Ford-Fulkerson 算法的一个特定实现,它使用广度优先搜索(BFS)来查找增广路径,保证了多项式时间复杂度。以下是 Edmonds-Karp 算法的 C++ 实现,包含详细注释:

#include 
#include 
#include 
#include 
#include 

using namespace std;

// 寻找从 s 到 t 的路径,并更新残留网络
bool bfs(vector>& rGraph, int s, int t, vector& parent) {
    int V = rGraph.size();
    vector visited(V, false);
    queue q;
    q.push(s);
    visited[s] = true;
    parent[s] = -1;

    // 标准的 BFS 循环
    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int v = 0; v < V; v++) {
            if (visited[v] == false && rGraph[u][v] > 0) {
                // 如果我们找到了一个连接到汇点的路径,则终止 BFS
                if (v == t) {
                    parent[v] = u;
                    return true;
                }
                q.push(v);
                parent[v] = u;
                visited[v] = true;
            }
        }
    }

    return false;
}

// 使用 Ford-Fulkerson 算法的 Edmonds-Karp 实现来返回最大流
int edmondsKarp(vector>& graph, int s, int t) {
    int u, v;

    // 创建残留网络并用给定的容量填充
    int V = graph.size();
    vector> rGraph(V, vector(V)); // 残留网络
    for (u = 0; u < V; u++)
        for (v = 0; v < V; v++)
             rGraph[u][v] = graph[u][v];

    vector parent(V);  // 用于存储路径
    int max_flow = 0;  // 存储最大流量

    // 增广路径循环
    while (bfs(rGraph, s, t, parent)) {
        // 找到最小的残留容量边
        int path_flow = INT_MAX;
        for (v = t; v != s; v = parent[v]) {
            u = parent[v];
            path_flow = min(path_flow, rGraph[u][v]);
        }

        // 更新残留网络的容量和反向边
        for (v = t; v != s; v = parent[v]) {
            u = parent[v];
            rGraph[u][v] -= path_flow;
            rGraph[v][u] += path_flow;
        }

        // 添加路径流量到总流量
        max_flow += path_flow;
    }

    return max_flow;
}

int main() {
    // 创建图:顶点数为 4
    vector> graph = { {0, 10, 0, 10},
                                  {0, 0, 4, 2},
                                  {0, 0, 0, 8},
                                  {0, 0, 0, 0} };

    // 设定源点为 0,汇点为 3
    cout << "The maximum possible flow is " << edmondsKarp(graph, 0, 3) << endl;

    return 0;
}
代码解释
  • 创建残留网络:残留网络 rGraph 初始时与原始图相同,表示流量的可能性。
  • 广度优先搜索(BFS)bfs 函数用于在残留网络中查找从源点 s 到汇点 t 的路径,同时更新 parent 数组来记录路径。
  • 计算最大流:在找到增广路径之后,计算该路径上的最小残留容量,然后更新残留网络,增加流量。
  • 更新残留网络:减少正向边的容量,增加反向边的容量,表示流量的重新分配。
运行示例
  • 程序创建了一个包含 4 个顶点的图,并设置了每条边的容量。
  • 然后,它使用 Edmonds-Karp 算法计算从顶点 0(源点)到顶点 3(汇点)的最大流,并打印结果。

        Edmonds-Karp 算法是网络流问题的一种有效解法,特别是在求解最大流问题时。在准备面试时,了解此算法的原理和实现对于展示你的图论知识非常有帮助。

        Ford-Fulkerson 算法是解决网络流问题中的最大流问题的一种经典方法。它通过不断寻找从源点到汇点的增广路径,并沿着这些路径增加流量,直到无法再增加为止。以下是 Ford-Fulkerson 算法的 C++ 实现,我将包括详细的注释来解释关键部分:

#include 
#include 
#include 
#include 
#include 

using namespace std;

// 使用邻接矩阵来表示图
class Graph {
    int V;    // 顶点数
    vector> rGraph; // 残留网络

public:
    Graph(int V, vector> graph) : V(V), rGraph(graph) {}

    // 使用 BFS 查找从 s 到 t 的路径
    bool bfs(int s, int t, vector& parent) {
        vector visited(V, false);
        queue q;
        q.push(s);
        visited[s] = true;
        parent[s] = -1;

        while (!q.empty()) {
            int u = q.front();
            q.pop();

            for (int v = 0; v < V; v++) {
                if (visited[v] == false && rGraph[u][v] > 0) {
                    if (v == t) {
                        parent[v] = u;
                        return true;
                    }
                    q.push(v);
                    parent[v] = u;
                    visited[v] = true;
                }
            }
        }
        return false;
    }

    // 主函数实现 Ford-Fulkerson 算法
    int FordFulkerson(int s, int t) {
        int max_flow = 0;  // 最大流初始化为 0
        vector parent(V);  // 用于存储路径

        // 增广路径循环
        while (bfs(s, t, parent)) {
            int path_flow = INT_MAX;

            // 计算增广路径的最小残留容量
            for (int v = t; v != s; v = parent[v]) {
                int u = parent[v];
                path_flow = min(path_flow, rGraph[u][v]);
            }

            // 更新残留网络的边和反向边
            for (int v = t; v != s; v = parent[v]) {
                int u = parent[v];
                rGraph[u][v] -= path_flow;
                rGraph[v][u] += path_flow;
            }

            // 将路径流添加到总流量
            max_flow += path_flow;
        }

        return max_flow;
    }
};

int main() {
    // 创建图:顶点数为 4
    vector> graph = { {0, 16, 13, 0},
                                  {0, 0, 10, 12},
                                  {0, 4, 0, 0},
                                  {0, 0, 9, 0} };

    Graph g(4, graph);

    // 计算从顶点 0(源点)到顶点 3(汇点)的最大流
    cout << "The maximum possible flow is " << g.FordFulkerson(0, 3) << endl;

    return 0;
}

代码解释

  • 构造图:图使用邻接矩阵表示,其中 rGraph[u][v] 表示从顶点 u 到顶点 v 的流量。
  • BFS:使用广度优先搜索来查找从源点 s 到汇点 t 的路径,并更新 parent 数组以记录路径。
  • 计算最大流:通过不断查找增广路径并更新残留网络,直到无法找到新的增广路径为止。
  • 更新残留网络:沿着找到的路径减少正向边的流量,并增加反向边的流量。

运行示例

  • 程序创建了一个包含 4 个顶点的图,并设置了每条边的流量。
  • 然后,它使用 Ford-Fulkerson 算法计算从顶点 0(源点)到顶点 3(汇点)的最大流,并打印结果。

        Ford-Fulkerson 算法是理解网络流问题的基础,并且在求解实际问题时非常有用。在准备面试时,了解并能够实现此算法对于展示你的图论和算法能力非常重要。

总结

        以上算法涵盖了图论中的一些关键概念和问题,包括寻找最短路径、探索图的结构,以及解决网络流问题。Dijkstra 和 Bellman-Ford 算法关注于单源最短路径问题,适用于不同类型的图(无负权边和有负权边)。而 Floyd-Warshall 和 A* 算法则扩展到所有顶点对的最短路径和特定场景下的最短路径搜索。Tarjan 算法用于探索图的深层结构,如强连通分量,而 DFS 和 BFS 提供了基础的图遍历方法。最后,Ford-Fulkerson 和 Edmonds-Karp 算法解决网络流和最大流问题。这些算法是计算机科学中图论应用的基石,对于处理复杂的数据结构和网络问题至关重要。

你可能感兴趣的:(c++,算法,c++,图论)