class="hide-preCode-box">

测试结果

在这里插入图片描述

3.3.2、邻接表测试

#include "Greaph.h"

int main()
{
    std::string strs[] = {"张三", "李四", "王五", "赵六"};
    Lenyiin::Table_Graph g1(strs, sizeof(strs) / sizeof(std::string)); // 无向图
    g1.AddEdge("张三", "李四", 10);
    g1.AddEdge("张三", "王五", 20);
    g1.AddEdge("王五", "赵六", 30);
    std::cout << "无向图" << std::endl;
    g1.Display();

    Lenyiin::Table_Graph g2(strs, sizeof(strs) / sizeof(std::string)); // 有向图
    g2.AddEdge("张三", "李四", 10);
    g2.AddEdge("张三", "王五", 20);
    g2.AddEdge("王五", "赵六", 30);
    std::cout << "\n有向图" << std::endl;
    g2.Display();
    
    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

测试结果

在这里插入图片描述

3.4、小结

在本章节中,我们深入探讨了图的两种主要存储结构——邻接矩阵邻接表,分析了它们各自的优缺点和适用场景。总结如下:

  1. 邻接矩阵适合稠密图的表示,尤其是当图中的顶点数量较多且连接较为紧密时。由于邻接矩阵是一种二维数组,它能够在常数时间 O(1) 内判断任意两点之间是否存在边,从而使其在快速查询边的存在性方面表现优越。然而,邻接矩阵的空间复杂度为 O(V^2),当图是稀疏图时,会占用大量冗余空间,导致内存效率低下。对于静态图,即结构固定且不需要频繁增删顶点或边的图结构,邻接矩阵的实现是一个理想的选择。
  2. 邻接表更适合表示稀疏图,即边相对顶点较少的图结构。在邻接表中,每个顶点只存储实际连接的边,这使得邻接表的空间复杂度为 O(V + E),较为高效。邻接表特别适用于需要频繁遍历顶点相邻边的图算法,例如广度优先搜索(BFS)和深度优先搜索(DFS),以及那些侧重于查找特定节点的连接关系的算法。此外,邻接表在需要动态增删边时更加灵活和高效。
  3. 性能和适用场景的对比
  4. 实现与代码复用

选择合适的存储结构不仅能提高图算法的效率,还能让代码更简洁、易于维护。开发者在选择邻接矩阵或邻接表时,应根据图的特性、算法需求和内存限制等多方面权衡,以达到最优效果。通过本章节的学习,相信读者对图的存储结构已有了更深入的理解,并能在实践中灵活应用。


4、图的遍历算法

在图的处理和分析中,遍历是核心操作之一。无论是检测图的连通性、查找路径,还是应用在图的其他问题上,遍历算法都扮演着关键角色。图的遍历方法主要分为两种:深度优先遍历(Depth-First Search, DFS)广度优先遍历(Breadth-First Search, BFS)。在本小节中,我们将深入分析这两种遍历算法的原理、实现方式及应用场景,并展示如何在邻接矩阵和邻接表的图结构上实现这些算法。

4.1、深度优先遍历(DFS)

深度优先遍历是一种递归或栈式的遍历方法,类似于树的前序遍历。在 DFS 中,遍历从一个起始顶点出发,不断向前深入访问相邻的未访问节点,直到所有可能路径都走完。DFS 的特点是 “优先深入”,因此适用于查找图中所有可能的路径以及连通性检测。

4.1.1、DFS 原理

  1. 从起始节点开始,将其标记为已访问。
  2. 访问起始节点的所有邻接节点,对于每个未访问的邻接节点递归地执行 DFS。
  3. 当所有邻接节点都访问完毕后,返回上一个节点,继续尝试访问其他未访问的邻接节点。
  4. 当所有节点都被访问过,则遍历结束。

4.1.2、基于邻接矩阵的 DFS 实现

以下是 DFS 算法在邻接矩阵和邻接表结构上的实现。

在邻接矩阵中,DFS 通过矩阵的行标识节点,并在遍历时利用矩阵中的边值来决定相邻节点。

#include 
#include 

namespace Lenyiin {
    template 
    class Matrix_Graph {
    public:
        // 其他省略 ...

        // 深度优先搜索
        void DFS(const V &src)
        {
            std::vector visited(_vertexs.size(), false);
            size_t srcIndex = GetVertexIndex(src);
            _DFS(srcIndex, visited);
            std::cout << std::endl;
        }

        void _DFS(size_t srcIndex, std::vector &visited)
        {
            std::cout << srcIndex << ":" << _vertexs[srcIndex] << std::endl;
            visited[srcIndex] = true;

            // 找到一个 src 相邻的且没有访问过的点, 去往深度遍历
            for (int i = 0; i < _matrix.size(); i++)
            {
                if (_matrix[srcIndex][i] != MAX_W && !visited[i])
                {
                    _DFS(i, visited);
                }
            }
        }

    private:
        // 其他省略 ...
    };
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

4.1.3、基于邻接表的 DFS 实现

在邻接表中,DFS 使用链表遍历每个节点的相邻节点,避免了不必要的检查,空间复杂度相对更低。

#include 
#include 
#include 

namespace Lenyiin {
    template 
    class Table_Graph {
    public:
        // 其他省略 ...

        // 深度优先搜索
        void DFS(const V &src)
        {
            std::vector visited(_vertexs.size(), false);
            int srcIndex = GetVertexIndex(src);
            _DFS(srcIndex, visited);
            std::cout << std::endl;
        }

        void _DFS(int srcIndex, std::vector &visited)
        {
            std::cout << srcIndex << ":" << _vertexs[srcIndex] << std::endl;
            visited[srcIndex] = true;

            EdgeNode *cur = _linkTable[srcIndex];
            while (cur)
            {
                if (!visited[cur->_dstIndex])
                {
                    _DFS(cur->_dstIndex, visited);
                }
                cur = cur->_next;
            }
        }

    private:
        // 其他省略 ...
    };
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

4.1.4、DFS 的应用


4.2、广度优先遍历(BFS)

广度优先遍历是一种层次遍历方法,类似于树的层次遍历。BFS 从起始节点出发,依次访问所有相邻节点,然后再从这些相邻节点开始,依次访问它们的相邻节点,以此类推。

4.2.1、BFS 原理

  1. 将起始节点加入队列并标记为已访问。
  2. 当队列不为空时,从队首取出一个节点,访问该节点的所有未访问邻接节点,并将这些节点加入队列。
  3. 继续以上过程,直到队列为空,则遍历结束。

4.2.2、基于邻接矩阵的 BFS 实现

BFS 的实现同样可以基于邻接矩阵和邻接表,主要区别在于使用队列来管理节点的访问顺序。

#include 
#include 
#include 

namespace Lenyiin {
    template 
    class Matrix_Graph {
    public:
        // 其他省略 ...

        // 广度优先
        void BFS(const V &src)
        {
            std::vector visited(_vertexs.size(), false);
            size_t srcIndex = GetVertexIndex(src);
            std::queue q;
            q.push(srcIndex);
            visited[srcIndex] = true;

            size_t n = _matrix.size();
            while (!q.empty())
            {
                int levelSize = q.size();
                while (levelSize--)
                {
                    int curIndex = q.front();
                    q.pop();
                    std::cout << curIndex << ":" << _vertexs[curIndex] << " ";

                    for (size_t i = 0; i < n; i++)
                    {
                        if (_matrix[curIndex][i] != MAX_W && !visited[i])
                        {
                            q.push(i);
                            visited[i] = true;
                        }
                    }
                }
                std::cout << std::endl;
            }
            std::cout << std::endl;
        }

    private:
        // 其他省略 ...
    };
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

4.2.3、基于邻接表的 BFS 实现

#include 
#include 
#include 
#include 

namespace Lenyiin {
    template 
    class Table_Graph {
    public:
        // 其他省略 ...

        // 广度优先搜索
        void BFS(const V &src)
        {
            std::vector visited(_vertexs.size(), false);
            int srcIndex = GetVertexIndex(src);
            std::queue q;
            q.push(srcIndex);
            visited[srcIndex] = true;

            while (!q.empty())
            {
                int curIndex = q.front();
                q.pop();
                std::cout << curIndex << ":" << _vertexs[curIndex] << " ";

                EdgeNode *cur = _linkTable[curIndex];
                while (cur)
                {
                    int neighbor = cur->_dstIndex;
                    if (!visited[neighbor])
                    {
                        q.push(neighbor);
                        visited[neighbor] = true;
                    }
                    cur = cur->_next;
                }
            }
            std::cout << std::endl;
        }

    private:
        // 其他省略 ...
    };
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

4.2.4、BFS 的应用


4.3、测试

4.3.1、邻接矩阵的图的遍历测试

int main()
{
    std::string strs[] = {"A", "B", "C", "D", "E", "F", "G", "H", "I"};
    Lenyiin::Table_Graph g(strs, sizeof(strs) / sizeof(std::string));

    g.AddEdge("A", "B", 1);
    g.AddEdge("A", "C", 1);
    g.AddEdge("A", "D", 1);
    g.AddEdge("B", "E", 1);
    g.AddEdge("B", "C", 1);
    g.AddEdge("C", "F", 1);
    g.AddEdge("D", "F", 1);
    g.AddEdge("E", "G", 1);
    g.AddEdge("F", "H", 1);
    g.AddEdge("H", "I", 1);

    g.Display();

    g.DFS("A");
    g.BFS("A");
    
    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

图连通示意图

在这里插入图片描述

测试结果:

在这里插入图片描述


4.3.2、邻接表的图的遍历测试

int main()
{
    std::string strs[] = {"A", "B", "C", "D", "E", "F", "G", "H", "I"};
    Lenyiin::Table_Graph g(strs, sizeof(strs) / sizeof(std::string));

    g.AddEdge("A", "B", 1);
    g.AddEdge("A", "C", 1);
    g.AddEdge("A", "D", 1);
    g.AddEdge("B", "E", 1);
    g.AddEdge("B", "C", 1);
    g.AddEdge("C", "F", 1);
    g.AddEdge("D", "F", 1);
    g.AddEdge("E", "G", 1);
    g.AddEdge("F", "H", 1);
    g.AddEdge("H", "I", 1);

    g.Display();

    std::cout << "邻接矩阵的深度优先搜索顺序:\n";
    g.DFS("A");
    std::cout << "邻接矩阵的广度优先搜索顺序:\n";
    g.BFS("A");
    
    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

测试结果:

4.4、小结

在本章节中,我们探讨了图的两种遍历算法——深度优先搜索(DFS)广度优先搜索(BFS),并在邻接矩阵和邻接表的两种存储结构上进行了实现。这两种遍历方法各有优劣,DFS 在查找路径和连通性方面表现优异,而 BFS 更适合最短路径和层次遍历。理解这两种算法的实现和应用,可以为后续深入研究复杂图算法奠定坚实基础。


5、最小生成树算法

5.1、图的最小生成树算法

在图论中,最小生成树 (Minimum Spanning Tree, MST) 是一个无向加权图的一个子集,它包含图中所有节点的树结构,并且通过最小的边权值形成一个无环的连通子图。对于无向连通图,其最小生成树的边数为 V−1,其中 V 表示节点数。最小生成树在网络设计(如电网布局、交通路线等)中具有广泛应用。常见的最小生成树算法包括 Kruskal 算法Prim 算法,本文将基于邻接矩阵和邻接表分别实现并解析这两种算法。

最小生成树的定义与性质:

在无向加权图中,最小生成树具有以下性质:

5.2、Kruskal 算法

Kruskal 算法是一种贪心算法,用于在无向加权图中构建最小生成树(MST)。算法的核心思想是按权重从小到大选择边,确保在没有形成环的前提下将其添加到生成树中。以下是算法的执行步骤:

  1. 对边按权重排序:将图中所有边按权重从小到大排序。
  2. 并查集:使用并查集(Union-Find)结构来检测环。
  3. 逐步加入边:从最小权重的边开始,依次加入生成树(若不会形成环),直到加入 V-1 条边。

Kruskal 算法通常适用于边稀疏的图,因为它只需要维护边集的排序,而不要求对顶点做邻接操作。这使得 Kruskal 在 边数远小于顶点数的图中效率较高。特别是对于网络设计(如公路、通信网络),该算法可以帮助找到低成本的连接方案。


对并查集不熟悉的可以阅读我的这篇技术博客:《 C++ 修炼全景指南:十五 》突破算法极限:并查集如何轻松搞定最棘手的连通性问题?


5.2.1、基于邻接矩阵的 Kruskal 算法的实现

// 最小生成树
W Kruskal(Self &minTree)
{
    // 初始化
    size_t n = _vertexs.size();
    minTree._vertexs = _vertexs;
    minTree._vertexsIndexMap = _vertexsIndexMap;
    minTree._matrix.resize(n, std::vector(n, MAX_W));

    std::priority_queue, std::greater> minque;

    for (size_t i = 0; i < n; i++)
    {
        for (size_t j = 0; j < i; j++)
        {
            if (_matrix[i][j] != MAX_W)
            {
            	minque.push(EdgeNode(i, j, _matrix[i][j]));
            }
        }
    }

    // 选出 n-1 条边
    std::vector ufs(n, -1);
    int size = n - 1;
    W sumW = W();
    while (!minque.empty())
    {
        EdgeNode min = minque.top();
        minque.pop();

        int srcIndex = min._srcIndex;
        int dstIndex = min._dstIndex;
        while (ufs[srcIndex] != -1)
        {
        	srcIndex = ufs[srcIndex];
        }
        while (ufs[dstIndex] != -1)
        {
        	dstIndex = ufs[dstIndex];
        }

        if (srcIndex != dstIndex)
        {
            std::cout << _vertexs[min._srcIndex] << "->" << _vertexs[min._dstIndex] << ":" << min._weight << std::endl;
            minTree._AddEdge(min._srcIndex, min._dstIndex, min._weight);
            ufs[srcIndex] = dstIndex;
            sumW += min._weight;
            --size;
        }
        else
        {
            std::cout << "构成环: " << _vertexs[min._srcIndex] << "->" << _vertexs[min._dstIndex] << ":" << min._weight << std::endl;
        }
    }

    if (size != 0)
    {
        std::cout << "图不连通";
        return W();
    }

    return sumW;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

5.2.2、基于邻接矩阵的 Kruskal 算法的测试

int main()
{
    const char *str = "abcdefghi";
    Lenyiin::Matrix_Graph g(str, strlen(str));
    g.AddEdge('a', 'b', 4);
    g.AddEdge('a', 'h', 8);
    // g.AddEdge('a', 'h', 9);
    g.AddEdge('b', 'c', 8);
    g.AddEdge('b', 'h', 11);
    g.AddEdge('c', 'i', 2);
    g.AddEdge('c', 'f', 4);
    g.AddEdge('c', 'd', 7);
    g.AddEdge('d', 'f', 14);
    g.AddEdge('d', 'e', 9);
    g.AddEdge('e', 'f', 10);
    g.AddEdge('f', 'g', 2);
    g.AddEdge('g', 'h', 1);
    g.AddEdge('g', 'i', 6);
    g.AddEdge('h', 'i', 7);

    g.Display();
    Lenyiin::Matrix_Graph kminTree;
    std::cout << "Kruskal:" << std::endl;
    std::cout << g.Kruskal(kminTree) << "\n\n";
    kminTree.Display();
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

测试结果:

在这里插入图片描述


5.2.3、基于邻接表的 Kruskal 算法的实现:

// 最小生成树
W Kruskal(Self &minTree)
{
    // 初始化
    size_t n = _vertexs.size();
    minTree._vertexs = _vertexs;
    minTree._vertexsIndexMap = _vertexsIndexMap;
    minTree._linkTable.resize(n, nullptr);

    std::priority_queue, std::greater> minque;

    for (int i = 0; i < n; i++)
    {
        EdgeNode *cur = _linkTable[i];
        while (cur)
        {
            minque.push(EdgeNode(cur->_srcIndex, cur->_dstIndex, cur->_weight));
            cur = cur->_next;
        }
    }

    // 选出 n - 1 条边
    std::vector ufs(n, -1);
    int size = n - 1;
    W sumW = W();
    while (!minque.empty())
    {
        EdgeNode min = minque.top();
        minque.pop();

        int srcIndex = min._srcIndex;
        int dstIndex = min._dstIndex;
        while (ufs[srcIndex] != -1)
        {
        	srcIndex = ufs[srcIndex];
        }
        while (ufs[dstIndex] != -1)
        {
        	dstIndex = ufs[dstIndex];
        }

        if (srcIndex != dstIndex)
        {
            std::cout << _vertexs[min._srcIndex] << "->" << _vertexs[min._dstIndex] << ":" << min._weight << std::endl;
            minTree._AddEdge(min._srcIndex, min._dstIndex, min._weight);
            ufs[srcIndex] = dstIndex;
            sumW += min._weight;
            --size;
        }
        else
        {
            std::cout << "构成环: " << _vertexs[min._srcIndex] << "->" << _vertexs[min._dstIndex] << ":" << min._weight << std::endl;
        }
    }

    if (size != 0)
    {
        std::cout << "图不连通";
        return W();
    }

    return sumW;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

在以上代码中,KruskalMST 方法使用了并查集结构来检测环,并确保生成的树连通。AddEdge 方法用于构造图的边集,生成图的最小生成树。


5.2.4、基于邻接表的 Kruskal 算法的测试

int main()
{
    const char *str = "abcdefghi";
    Lenyiin::Table_Graph g(str, strlen(str));
    g.AddEdge('a', 'b', 4);
    g.AddEdge('a', 'h', 8);
    g.AddEdge('b', 'c', 8);
    g.AddEdge('b', 'h', 11);
    g.AddEdge('c', 'i', 2);
    g.AddEdge('c', 'f', 4);
    g.AddEdge('c', 'd', 7);
    g.AddEdge('d', 'f', 14);
    g.AddEdge('d', 'e', 9);
    g.AddEdge('e', 'f', 10);
    g.AddEdge('f', 'g', 2);
    g.AddEdge('g', 'h', 1);
    g.AddEdge('g', 'i', 6);
    g.AddEdge('h', 'i', 7);

    g.Display();
    Lenyiin::Table_Graph kminTree;
    std::cout << "Kruskal:" << std::endl;
    std::cout << g.Kruskal(kminTree) << "\n\n";
    kminTree.Display();
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

测试结果:

在这里插入图片描述


5.2.5、Kruskal 算法的复杂度分析

5.2.6、复杂案例分析


5.3、Prim 算法

Prim 算法是另一种用于构建无向加权图最小生成树(MST)的贪心算法。它的核心思想是从一个起始顶点开始,不断地扩展到权重最小且未加入生成树的邻接边,直到生成树包含了所有顶点。它与 Kruskal 算法不同,Prim 是基于顶点而不是边的选择,适用于稠密图,即边较多的图。

  1. 初始化:选取任意顶点作为生成树的起点。
  2. 选择最小边:从当前生成树的边中选择权重最小且不构成环的边,加入生成树。
  3. 重复步骤2:直到生成树包含所有顶点。

5.3.1、Prim 算法的应用场景

Prim 算法广泛用于网络设计(如电网、通信网络等),尤其适用于稠密图,因为它在每步只选择与当前生成树直接相连的最小边,避免了遍历整个边集。相比于 Kruskal,Prim 更适合于具有较多边的图,因为它在寻找最小权重边的过程中可以利用优先队列进行优化。

5.3.2、数据结构选择

邻接表或邻接矩阵:邻接矩阵适用于稠密图(如电路设计),而邻接表适用于稀疏图。Prim 算法可以用邻接矩阵实现,但在稀疏图中使用邻接表搭配最小优先队列效果更佳。

最小优先队列(Min-Heap):使用优先队列可以快速找到与生成树相连的最小权重边。一般选择最小堆实现,更新相邻顶点的最短距离时可以高效地调整堆结构。

5.3.3、算法的复杂度分析

5.3.4、邻接表与邻接矩阵实现的选择

邻接矩阵实现的 Prim 适用于边较多的图,而在边稀疏的图中,使用邻接表搭配最小堆能显著提高效率。邻接表的结构可以让我们遍历每个节点的直接邻居,这比遍历整个边集更加高效。


5.3.5、基于邻接矩阵的 Prim 算法的实现

// 最小生成树
W Prim(Self &minTree, const W &src)
{
    size_t srcIndex = GetVertexIndex(src);
    size_t n = _vertexs.size();

    minTree._vertexs = _vertexs;
    minTree._vertexsIndexMap = _vertexsIndexMap;
    minTree._matrix.resize(n, std::vector(n, MAX_W));

    std::set X;
    std::set Y;
    X.insert(srcIndex);
    for (size_t i = 0; i < n; i++)
    {
        if (i != srcIndex)
        {
        	Y.insert(i);
        }
    }

    // 从 X -> Y 集合中连接的边里面选出最小的边
    std::priority_queue, std::greater> minque;
    // 先把 src 连接的边放到 minque 中
    for (size_t i = 0; i < n; i++)
    {
        if (_matrix[srcIndex][i] != MAX_W)
        {
        	minque.push(EdgeNode(srcIndex, i, _matrix[srcIndex][i]));
        }
    }

    size_t size = n - 1;
    W sumW = W();
    while (!minque.empty())
    {
        EdgeNode min = minque.top();
        minque.pop();

        if (X.find(min._dstIndex) != X.end())
        {
            // 构成环
            std::cout << "构成环: " << _vertexs[min._srcIndex] << "->" << _vertexs[min._dstIndex] << ":" << min._weight << std::endl;
            continue;
        }

        minTree._AddEdge(min._srcIndex, min._dstIndex, min._weight);
        std::cout << _vertexs[min._srcIndex] << "->" << _vertexs[min._dstIndex] << ":" << min._weight << std::endl;
        X.insert(min._dstIndex);
        Y.erase(min._dstIndex);
        sumW += min._weight;
        --size;

        if (size == 0)
        {
        	break;
        }

        // 更新 minque
        for (auto it = Y.begin(); it != Y.end(); it++)
        {
            int y = *it;
            if (_matrix[min._dstIndex][y] != MAX_W)
            {
            	minque.push(EdgeNode(min._dstIndex, y, _matrix[min._dstIndex][y]));
            }
        }
    }

    if (size != 0)
    {
        std::cout << "图不连通";
        return W();
    }

    return sumW;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

5.3.6、基于邻接矩阵的 Prim 算法的测试

int main()
{
    const char *str = "abcdefghi";
    Lenyiin::Matrix_Graph g(str, strlen(str));
    g.AddEdge('a', 'b', 4);
    g.AddEdge('a', 'h', 8);
    // g.AddEdge('a', 'h', 9);
    g.AddEdge('b', 'c', 8);
    g.AddEdge('b', 'h', 11);
    g.AddEdge('c', 'i', 2);
    g.AddEdge('c', 'f', 4);
    g.AddEdge('c', 'd', 7);
    g.AddEdge('d', 'f', 14);
    g.AddEdge('d', 'e', 9);
    g.AddEdge('e', 'f', 10);
    g.AddEdge('f', 'g', 2);
    g.AddEdge('g', 'h', 1);
    g.AddEdge('g', 'i', 6);
    g.AddEdge('h', 'i', 7);

    g.Display();
    Lenyiin::Matrix_Graph pminTree;
    std::cout << "Prim:" << std::endl;
    std::cout << g.Prim(pminTree, 'a') << "\n\n";
    pminTree.Display();

    // for (size_t i = 0; i < strlen(str); i++)
    // {
    //     std::cout << "Prim 从 " << str[i] << " 开始:" << std::endl;
    //     std::cout << g.Prim(pminTree, str[i]) << "\n\n";
    // }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

在这里插入图片描述

测试结果:

在这里插入图片描述


5.3.7、基于邻接表的 Prim 算法的实现

// 最小生成树
W Prim(Self &minTree, const W &src)
{
    size_t srcIndex = GetVertexIndex(src);
    size_t n = _vertexs.size();

    minTree._vertexs = _vertexs;
    minTree._vertexsIndexMap = _vertexsIndexMap;
    minTree._linkTable.resize(n, nullptr);

    std::set X;
    std::set Y;
    X.insert(srcIndex);
    for (size_t i = 0; i < n; i++)
    {
        if (i != srcIndex)
        {
        	Y.insert(i);
        }
    }

    // 从 X -> Y 集合中连接的边里面选出最小的边
    std::priority_queue, std::greater> minque;
    // 先把 src 连接的边放到 minque 中
    EdgeNode *cur = _linkTable[srcIndex];
    while (cur)
    {
        minque.push(EdgeNode(cur->_srcIndex, cur->_dstIndex, cur->_weight));
        cur = cur->_next;
    }

    size_t size = n - 1;
    W sumW = W();
    while (!minque.empty())
    {
        EdgeNode min = minque.top();
        minque.pop();

        if (X.find(min._dstIndex) != X.end())
        {
            // 构成环
            std::cout << "构成环: " << _vertexs[min._srcIndex] << "->" << _vertexs[min._dstIndex] << ":" << min._weight << std::endl;
            continue;
        }

        minTree._AddEdge(min._srcIndex, min._dstIndex, min._weight);
        std::cout << _vertexs[min._srcIndex] << "->" << _vertexs[min._dstIndex] << ":" << min._weight << std::endl;
        X.insert(min._dstIndex);
        Y.erase(min._dstIndex);
        sumW += min._weight;
        --size;

        if (size == 0)
        {
        	break;
        }

        // 更新 minque
        EdgeNode *cur = _linkTable[min._dstIndex];
        while (cur)
        {
            if (Y.find(cur->_dstIndex) != Y.end())
            {
            	minque.push(EdgeNode(cur->_srcIndex, cur->_dstIndex, cur->_weight));
            }
            cur = cur->_next;
        }
    }

    if (size != 0)
    {
        std::cout << "图不连通";
        return W();
    }

    return sumW;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

以上代码展示了 Prim 算法的完整实现。PrimMST 方法从指定顶点 start 开始,使用优先队列来选择权重最小的边,扩展生成树。


5.3.8、基于邻接表的 Prim 算法的测试

int main()
{
    const char *str = "abcdefghi";
    Lenyiin::Table_Graph g(str, strlen(str));
    g.AddEdge('a', 'b', 4);
    g.AddEdge('a', 'h', 8);
    g.AddEdge('b', 'c', 8);
    g.AddEdge('b', 'h', 11);
    g.AddEdge('c', 'i', 2);
    g.AddEdge('c', 'f', 4);
    g.AddEdge('c', 'd', 7);
    g.AddEdge('d', 'f', 14);
    g.AddEdge('d', 'e', 9);
    g.AddEdge('e', 'f', 10);
    g.AddEdge('f', 'g', 2);
    g.AddEdge('g', 'h', 1);
    g.AddEdge('g', 'i', 6);
    g.AddEdge('h', 'i', 7);

    g.Display();
    Lenyiin::Table_Graph pminTree;
    std::cout << "Prim:" << std::endl;
    std::cout << g.Prim(pminTree, 'a') << "\n\n";
    pminTree.Display();

    // for (size_t i = 0; i < strlen(str); i++)
    // {
    //     std::cout << "Prim 从 " << str[i] << " 开始:" << std::endl;
    //     std::cout << g.Prim(pminTree, str[i]) << "\n\n";
    // }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

在这里插入图片描述

测试结果:

在这里插入图片描述


5.3.9、复杂案例分析

5.3.10、Prim 算法的优化

综上,Prim 算法提供了一种通过贪心方法构建最小生成树的有效手段。利用优先队列和邻接表的结构,Prim 算法在稠密图中的效率显著优于 Kruskal。同时,结合上述优化和细节分析,可以进一步提高算法的应用广度与实用性。

5.4、Prim 算法与 Kruskal 算法的比较

5.5、小结

Kruskal 和 Prim 是构建无向加权图最小生成树(MST)的两大经典贪心算法。虽然两者的目标相同,但在实现方式、数据结构选择、适用场景、复杂度和细节处理上各具特点。以下将从多个角度详细比较并总结两者的特性和适用性。

5.5.1、算法原理与流程对比

5.5.2、数据结构选择

5.5.3、复杂度分析

5.5.4、适用场景对比

5.5.5、算法的拓展与优化

5.5.6、实现的技术要点与难点

5.5.7、Kruskal 和 Prim 的互补性

尽管 Kruskal 和 Prim 的算法思想和操作细节不同,它们在应用中却可以互为补充。在图设计过程中,可根据边密度灵活选择合适的算法;在分布式计算和网络优化中,Kruskal 能高效构建稀疏连通图,而 Prim 则擅长快速处理密集连通图。在多连通分量的图中,结合两者的特性可以优化最小生成森林的构造。

5.5.8、小结

Kruskal 和 Prim 算法各自针对不同的图结构和应用场景提供了高效的最小生成树构建方式。Kruskal 适合稀疏图,通过并查集保证了快速的连通性检查,而 Prim 适用于稠密图,结合最小堆能减少不必要的边访问。在实际应用中,两者的组合使用可以实现更灵活高效的图结构优化。这两种算法不仅在图论中具有重要地位,同时还在网络、通信、图像处理等领域中发挥了重要作用,深入理解并应用两者有助于在复杂问题中找到更优解。


6、最短路径算法

6.1、图的最短路径概述

图的最短路径问题是指在图结构中,寻找从一个节点到另一个节点的路径,使得路径上的权重之和最小。这是图论中一个非常基础且重要的问题,广泛应用于交通规划、网络路由优化、社交网络分析等领域。

6.1.1、最短路径的基本概念

  1. 路径:在图中,一条路径是从一个节点出发,通过一系列连接的边,最终到达目标节点的一个路线。
  2. 路径长度:路径长度是指路径中所有边权重的总和。对于无权图,路径长度就是路径上的边数;对于有权图,路径长度是所有边权重之和。
  3. 源节点:寻找最短路径时的起始节点,通常称为源节点。
  4. 目标节点:希望到达的最终节点。
  5. 最短路径:从源节点到目标节点的路径中,具有最小路径长度的那条路径称为最短路径。

6.1.2、最短路径问题的分类

根据不同的需求和图的特性,最短路径问题可以分为几种主要类型:

  1. 单源最短路径:在给定图中,从指定的源节点出发,计算到其他所有节点的最短路径。例如,在路网中从一个城市出发,找到到达其他城市的最短路径。常用算法有 Dijkstra 算法和 Bellman-Ford 算法。
  2. 单源单目的最短路径:在单源最短路径的基础上,进一步限制目标节点为唯一一个。这种情况适合在大型图中只需要找到特定两个节点之间的最短路径,减少不必要的计算。
  3. 多源最短路径:在某些应用中,不止一个节点被视为源节点,需要找到所有源节点到其他节点的最短路径。这种问题相对较少。
  4. 多源多目的最短路径(全局最短路径):计算图中任意两个节点之间的最短路径。这类问题通常用于全局性分析,如社交网络中计算两个用户之间的距离。典型算法是Floyd-Warshall算法。

6.1.3、最短路径问题的应用场景

6.1.4、最短路径问题的算法

为了解决不同类型的最短路径问题,图论中提出了多种经典算法:

  1. Dijkstra算法:适用于无负权边的单源最短路径问题,基于贪心策略逐步找到从源节点到其他节点的最短路径。
  2. Bellman-Ford算法:适用于含负权边的单源最短路径问题,并能检测负权环。
  3. Floyd-Warshall算法:适用于多源多目的的最短路径问题,即找到图中任意两个节点之间的最短路径。

6.1.5、算法的选择与适用条件

不同的最短路径算法各有优缺点,具体使用哪个算法取决于图的特性和实际需求:

图的最短路径问题提供了一种在图中衡量距离和路径优化的工具。通过选择合适的最短路径算法,可以有效解决实际中的许多问题,如路径优化、距离测量、路由规划等。在实际应用中,掌握最短路径问题的不同算法及其适用场景,是理解和应用图论的关键。

6.1.6、图的存储结构

在实现最短路径算法之前,选择合适的图存储结构非常重要。通常,图的存储结构有以下两种常见形式:

  1. 邻接矩阵:使用一个二维数组存储图的边信息,适合稠密图(边的数量接近于节点数的平方)。
  2. 邻接表:使用链表数组记录节点的相邻边信息,适合稀疏图(边的数量远小于节点数的平方)。

6.2、Dijkstra 算法

Dijkstra 算法是一种经典的单源最短路径算法,用于解决从一个起始节点(源节点)到图中其他所有节点的最短路径问题。它在图论和计算机科学领域应用广泛,如 GPS 导航、网络路由、地图服务等。Dijkstra 算法只能在没有负权边的图中使用,其时间复杂度相对较低,适合于无负环的图结构。

6.2.1、算法的核心思想

Dijkstra 算法的核心思想是采用 “贪心策略”,逐步扩展已确定的最短路径集合。通过维护一个优先队列,Dijkstra 算法每次都选择路径长度最短的节点,将其加入已知最短路径集合中,更新其邻接节点的最短路径估计值,直到所有节点的最短路径都确定或优先队列为空。

1、Dijkstra 算法的基本步骤
  1. 初始化
  2. 贪心扩展
  3. 重复迭代
  4. 返回结果
2、邻接表与邻接矩阵的实现

Dijkstra算法可以基于两种数据结构实现:

  1. 邻接表:适合稀疏图(即边的数量远小于节点数的平方),通过链表或数组存储每个节点的相邻节点和边权重。
  2. 邻接矩阵:适合密集图(即边的数量接近节点数的平方),使用二维数组表示节点之间的边权重。

邻接表的实现具有较好的空间效率,且在稀疏图中效率更高。邻接矩阵虽然在密集图中效率高,但其空间消耗较大。


6.2.2、基于邻接矩阵的 Dijkstra 算法的实现

// 单源最短路径 负权值不适用
// 顶点个数 N -> 时间复杂度: O(N^2)		空间复杂度: O(N)
void Dijkstra(const V &src, std::vector &dist, std::vector &pPath)
{
    size_t srcIndex = GetVertexIndex(src);
    size_t n = _vertexs.size();

    dist.resize(n, MAX_W);
    pPath.resize(n, -1);
    dist[srcIndex] = 0;
    pPath[srcIndex] = srcIndex;

    std::vector visited(n, false);

    for (size_t i = 0; i < n; i++)
    {
        // 1. 找到未曾访问过的最小的 dist   这里可以使用优先级队列
        W minDist = MAX_W;
        size_t minIndex = 0;
        for (size_t j = 0; j < n; j++)
        {
            if (!visited[j] && dist[j] < minDist)
            {
                minDist = dist[j];
                minIndex = j;
            }
        }

        // 2. 标记为已访问
        visited[minIndex] = true;

        // 3. 更新 dist
        for (size_t j = 0; j < n; j++)
        {
            if (!visited[j] && _matrix[minIndex][j] != MAX_W)
            {
                if (dist[j] > dist[minIndex] + _matrix[minIndex][j])
                {
                    dist[j] = dist[minIndex] + _matrix[minIndex][j];
                    pPath[j] = minIndex;
                }
            }
        }
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

6.2.3、基于邻接矩阵的 Dijkstra 算法的测试

在测试之前先写一个打印函数,这样显示的更直观,下面 BellmanFord 算法和 FloyWarshall 将直接使用这个函数,并且不在重复缀叙。

void PrintShortPath(const V &src, const std::vector &dist, const std::vector &pPath)
{
    size_t srcIndex = GetVertexIndex(src);
    size_t n = _vertexs.size();

    for (size_t i = 0; i < n; i++)
    {
        if (i == srcIndex)
        {
            continue;
        }

        // 找出 i 顶点的路径
        std::vector path;
        size_t parentIndex = i;
        while (parentIndex != srcIndex)
        {
            path.push_back(parentIndex);
            parentIndex = pPath[parentIndex];
        }
        path.push_back(srcIndex);
        reverse(path.begin(), path.end());

        for (const auto &index : path)
        {
        	std::cout << _vertexs[index] << "->";
        }
        std::cout << "权值和: " << dist[i] << std::endl;
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

测试代码:

// 当图中有负权边时, 贪心算法失效, Dijkstra算法不适用
int main()
{
    const char *str = "syztx";
    Lenyiin::Matrix_Graph g(str, strlen(str));
    g.AddEdge('s', 't', 10);
    g.AddEdge('s', 'y', 5);
    g.AddEdge('y', 't', 3);
    g.AddEdge('y', 'x', 9);
    g.AddEdge('y', 'z', 2);
    g.AddEdge('z', 's', 7);
    g.AddEdge('z', 'x', 6);
    g.AddEdge('t', 'y', 2);
    g.AddEdge('t', 'x', 1);
    g.AddEdge('x', 'z', 4);

    std::vector dist;
    std::vector parentPath;
    g.Display();
    g.Dijkstra('s', dist, parentPath);
    g.PrintShortPath('s', dist, parentPath);

    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

在这里插入图片描述

测试结果:

在这里插入图片描述


6.2.4、基于邻接表的 Dijkstra 算法的实现

// 单源最短路径 负权值不适用
// 顶点个数 N -> 时间复杂度: O(N^2)		空间复杂度: O(N)
void Dijkstra(const V &src, std::vector &dist, std::vector &pPath)
{
    size_t srcIndex = GetVertexIndex(src);
    size_t n = _vertexs.size();

    dist.resize(n, MAX_W);
    pPath.resize(n, -1);
    dist[srcIndex] = 0;
    pPath[srcIndex] = srcIndex;

    std::vector visited(n, false);

    for (size_t i = 0; i < n; i++)
    {
        // 1. 找到未曾访问过的最小的 dist
        W minDist = MAX_W;
        size_t minIndex = 0;
        for (size_t j = 0; j < n; j++)
        {
            if (!visited[j] && dist[j] < minDist)
            {
                minDist = dist[j];
                minIndex = j;
            }
        }

        // 2. 标记为已访问
        visited[minIndex] = true;

        // 3. 更新 dist
        EdgeNode *cur = _linkTable[minIndex];
        while (cur)
        {
            if (!visited[cur->_dstIndex] && dist[cur->_dstIndex] > dist[minIndex] + cur->_weight)
            {
                dist[cur->_dstIndex] = dist[minIndex] + cur->_weight;
                pPath[cur->_dstIndex] = minIndex;
            }
            cur = cur->_next;
        }
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

6.2.5、基于邻接表的 Dijkstra 算法的测试

// 当图中有负权边时, 贪心算法失效, Dijkstra算法不适用
int main()
{
    const char *str = "syztx";
    Lenyiin::Table_Graph g(str, strlen(str));
    g.AddEdge('s', 't', 10);
    g.AddEdge('s', 'y', 5);
    g.AddEdge('y', 't', 3);
    g.AddEdge('y', 'x', 9);
    g.AddEdge('y', 'z', 2);
    g.AddEdge('z', 's', 7);
    g.AddEdge('z', 'x', 6);
    g.AddEdge('t', 'y', 2);
    g.AddEdge('t', 'x', 1);
    g.AddEdge('x', 'z', 4);

    std::vector dist;
    std::vector parentPath;
    g.Display();
    g.Dijkstra('s', dist, parentPath);
    g.PrintShortPath('s', dist, parentPath);

    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

在这里插入图片描述

测试结果:

在这里插入图片描述


6.2.6、算法的复杂度

6.2.7、Dijkstra算法的限制与改进

  1. 负权边问题:Dijkstra算法不能处理负权边,因为负权边可能导致最短路径估计不断减少,陷入无限循环。若图中含有负权边,可以选择Bellman-Ford算法。
  2. 适用场景:Dijkstra算法适用于稀疏图(边的数量远小于节点数的平方)和无负权边的情况,且常见于图密度较低的路网或通信网络中。
  3. 堆优化:Dijkstra算法中使用堆优化的优先队列,可以显著降低复杂度,从而提高算法的效率。

6.2.8、应用场景

Dijkstra算法因其高效性和适用性广泛应用于实际中的路径规划和导航,如:

6.2.9、小结

Dijkstra算法是解决无负权边图中单源最短路径的经典算法,利用贪心策略实现高效路径查找。通过优先队列的引入,算法能够在稀疏图上实现快速搜索。尽管Dijkstra算法在含负权边的图中不适用,但在许多实际场景中,如交通、网络路由和物流管理中仍然表现出色。


6.3、Bellman-Ford 算法

Bellman-Ford 算法是图论中的一种经典单源最短路径算法,用于计算从一个源节点到图中其他所有节点的最短路径。与 Dijkstra 算法不同,Bellman-Ford 算法允许图中存在负权边,甚至可以检测到负权环(即路径和为负的环路)。由于其兼容负权边的特性,Bellman-Ford 算法在金融、网络路由以及路径优化等领域具有广泛的应用。

6.3.1、Bellman-Ford 算法的核心思想

Bellman-Ford算法基于动态规划的思想,通过逐步更新最短路径的估计值,逐轮迭代,保证最短路径的计算。它的核心是对所有边进行 “松弛操作” ——如果通过一条边能够获得更短的路径,则更新该边连接的目标节点的路径估计值。重复这一操作,直到不能再优化路径估计值为止。

Bellman-Ford算法的具体执行步骤如下:

  1. 初始化
  2. 边的松弛操作
  3. 负权环检测
  4. 返回结果

6.3.2、算法的复杂度


6.3.3、基于邻接矩阵的 Bellman-Ford 算法的实现

以下提供 Bellman-Ford 算法基于邻接表和邻接矩阵的两种实现方式。邻接表适合稀疏图,而邻接矩阵适合密集图。

邻接矩阵存储图中每对节点间的边权信息,更适合边数较多的密集图。

// 单源最短路径 负权值也适用
// 顶点个数是 N -> 时间复杂度: O(N^3)		空间复杂度: O(N)
bool BellmanFord(const V &src, std::vector &dist, std::vector &pPath)
{
    size_t srcIndex = GetVertexIndex(src);
    size_t n = _vertexs.size();

    // vector dict 记录 srcIndex 到各个顶点的最短路径权值数组
    dist.resize(n, MAX_W);

    // vector pPath 记录 srcIndex 到各个顶点的最短路径的前一个顶点
    pPath.resize(n, -1);

    // 初始化
    dist[srcIndex] = W();
    pPath[srcIndex] = srcIndex;

    // 1. 初始化 dist
    for (size_t i = 0; i < n - 1; i++)
    {
        // 更新松弛
        bool update = false;

        for (size_t j = 0; j < n; j++)
        {
            for (size_t k = 0; k < n; k++)
            {
                if (_matrix[j][k] != MAX_W && dist[j] != MAX_W && dist[k] > dist[j] + _matrix[j][k])
                {
                    dist[k] = dist[j] + _matrix[j][k];
                    pPath[k] = j;
                    update = true;
                }
            }
        }

        // 如果这个轮次中没有更新出最短路径, 那么后面的轮次也不会更新
        if (!update)
        {
        	break;
        }
    }

    // 2. 检测是否有负权回路
    for (size_t j = 0; j < n; j++)
    {
        for (size_t k = 0; k < n; k++)
        {
            if (_matrix[j][k] != MAX_W && dist[j] != MAX_W && dist[k] > dist[j] + _matrix[j][k])
            {
            	return false;
            }
        }
    }

    return true;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

6.3.4、基于邻接矩阵的 Bellman-Ford 算法的测试

int main()
{
    const char *str = "syztx";
    Lenyiin::Matrix_Graph g(str, strlen(str));
    g.AddEdge('s', 't', 6);
    g.AddEdge('s', 'y', 7);
    g.AddEdge('y', 'z', 9);
    g.AddEdge('y', 'x', -3);
    g.AddEdge('z', 's', 2);
    g.AddEdge('z', 'x', 7);
    g.AddEdge('t', 'x', 5);
    g.AddEdge('t', 'y', 8);
    g.AddEdge('t', 'z', -4);
    g.AddEdge('x', 't', -2);

    std::vector dist;
    std::vector parentPath;
    g.Display();
    if (g.BellmanFord('s', dist, parentPath))
    {
        g.PrintShortPath('s', dist, parentPath);
    }
    else
    {
        std::cout << "存在负权回路" << std::endl;
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

在这里插入图片描述

测试结果:

在这里插入图片描述


6.3.5、基于邻接表的 Bellman-Ford 算法的实现

邻接表结构通过链表或数组存储每个节点的相邻节点及其权重,更适合边较少的稀疏图。

// 单源最短路径 负权值也适用
// 顶点个数是 N -> 时间复杂度: O(N^3)		空间复杂度: O(N)
bool BellmanFord(const V &src, std::vector &dist, std::vector &pPath)
{
    size_t srcIndex = GetVertexIndex(src);
    size_t n = _vertexs.size();

    // vector dict 记录 srcIndex 到各个顶点的最短路径权值数组
    dist.resize(n, MAX_W);

    // vector pPath 记录 srcIndex 到各个顶点的最短路径的前一个顶点
    pPath.resize(n, -1);

    // 初始化
    dist[srcIndex] = W();
    pPath[srcIndex] = srcIndex;

    // 1. 初始化 dist
    for (size_t i = 0; i < n - 1; i++)
    {
        // 更新松弛
        bool update = false;

        for (size_t j = 0; j < n; j++)
        {
            EdgeNode *cur = _linkTable[j];
            while (cur)
            {
                if (dist[j] != MAX_W && dist[cur->_dstIndex] > dist[j] + cur->_weight)
                {
                    dist[cur->_dstIndex] = dist[j] + cur->_weight;
                    pPath[cur->_dstIndex] = j;
                    update = true;
                }
                cur = cur->_next;
            }
        }

        // 如果这个轮次中没有更新出最短路径, 那么后面的轮次也不会更新
        if (!update)
        {
        	break;
        }
    }

    // 2. 检测是否有负权回路
    for (size_t j = 0; j < n; j++)
    {
        EdgeNode *cur = _linkTable[j];
        while (cur)
        {
            if (dist[j] != MAX_W && dist[cur->_dstIndex] > dist[j] + cur->_weight)
            {
            	return false;
            }
            cur = cur->_next;
        }
    }

    return true;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

6.3.6、基于邻接表的 Bellman-Ford 算法的测试

int main()
{
    const char *str = "syztx";
    Lenyiin::Table_Graph g(str, strlen(str));
    g.AddEdge('s', 't', 6);
    g.AddEdge('s', 'y', 7);
    g.AddEdge('y', 'z', 9);
    g.AddEdge('y', 'x', -3);
    g.AddEdge('z', 's', 2);
    g.AddEdge('z', 'x', 7);
    g.AddEdge('t', 'x', 5);
    g.AddEdge('t', 'y', 8);
    g.AddEdge('t', 'z', -4);
    g.AddEdge('x', 't', -2);

    std::vector dist;
    std::vector parentPath;
    g.Display();
    if (g.BellmanFord('s', dist, parentPath))
    {
        g.PrintShortPath('s', dist, parentPath);
    }
    else
    {
        std::cout << "存在负权回路" << std::endl;
    }
    
    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

在这里插入图片描述

测试结果:

在这里插入图片描述


6.3.7、Bellman-Ford 算法的应用场景

由于Bellman-Ford算法支持负权边,它在一些特殊领域非常适用,如:

6.3.8、Bellman-Ford 算法的优缺点

优点

  1. 支持负权边:是单源最短路径算法中为数不多支持负权边的算法。
  2. 负权环检测:可以检测图中是否存在负权环,适合负权图的分析和计算。

缺点

  1. 时间复杂度较高:相比Dijkstra算法在密集图中更慢,尤其是边数较多时。
  2. 不适用于大规模密集图:算法在大规模数据中效率低下,适合中小规模或稀疏图的应用。

6.3.9、小结

Bellman-Ford 算法是图最短路径问题中的重要算法,因其支持负权边和负权环检测而广泛应用于复杂网络和金融市场等领域。尽管时间复杂度相对较高,但在稀疏图和负权图中的表现优异。


6.4、Floyd-Warshall 算法

Floyd-Warshall 算法是一种多源最短路径算法,用于计算图中每一对节点之间的最短路径。与单源最短路径算法(如 DijkstraBellman-Ford)不同,Floyd-Warshall 可以在一次运算中找到所有节点对之间的最短路径,因此非常适合解决多源路径问题。该算法适用于带权有向图和无向图,但不适用于包含负权环的图。其时间复杂度为 O(V3) ,其中 V 是图的节点数,因此在稠密图中应用效果较好,但在节点数较大的图中可能性能欠佳。

6.4.1、算法原理

Floyd-Warshall 算法基于动态规划的思想。设定一个 dist[i][j]数组,用于记录从节点 i 到节点 j 的最短路径。算法逐步更新这个数组,通过不断引入新的中间节点来更新节点对之间的最短路径。算法的递推关系为:

d i s t [ i ] [ j ] = m i n ⁡ ( d i s t [ i ] [ j ] , d i s t [ i ] [ k ] + d i s t [ k ] [ j ] ) dist[i][j]=min⁡(dist[i][j],dist[i][k]+dist[k][j]) dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j])

其中,k 表示当前引入的中间节点。如果通过节点 k 作为中间节点可以找到从 i 到 j 的更短路径,就更新 dist[i][j] 的值。

Floyd-Warshall 算法的步骤

  1. 初始化:根据图的邻接矩阵初始化 dist[i][j] 数组。如果 i 到 j 有直接边,设置 dist[i][j] 为该边的权重,否则设置为无穷大。对于每个节点 i,设置 dist[i][i] = 0
  2. 动态规划更新:对于每个可能的中间节点 k,依次更新所有节点对 (i,j) 之间的最短路径。即,检查是否通过中间节点 k 可以让从 i 到 j 的路径变短。
  3. 负权环检测:在更新完成后,检查 dist[i][j] 是否为负值,如果存在负值,则表示图中包含负权环。

6.4.2、基于邻接矩阵的 Floyd-Warshall 算法的实现

以下是基于邻接矩阵的 Floyd-Warshall 算法实现:

// 多源最短路径
void FloyWarshall(std::vector> &vvDist, std::vector> &vvpPath)
{
    size_t n = _vertexs.size();

    // 初始化
    vvDist.resize(n, std::vector(n, MAX_W));
    vvpPath.resize(n, std::vector(n, -1));

    // 初始化
    for (size_t i = 0; i < n; i++)
    {
        for (size_t j = 0; j < n; j++)
        {
            if (_matrix[i][j] != MAX_W)
            {
                vvDist[i][j] = _matrix[i][j];
                vvpPath[i][j] = i;
            }

            if (i == j)
            {
            	vvpPath[i][j] = W();
            }
        }
    }

    // 最短路径更新
    for (size_t k = 0; k < n; k++)
    {
        for (size_t i = 0; i < n; i++)
        {
            for (size_t j = 0; j < n; j++)
            {
                if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W && vvDist[i][j] > vvDist[i][k] + vvDist[k][j])
                {
                    vvDist[i][j] = vvDist[i][k] + vvDist[k][j];
                    vvpPath[i][j] = vvpPath[k][j];
                }
            }
        }
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

代码解析

  1. 邻接矩阵初始化:我们使用一个二维数组 dist 来存储节点之间的距离。MAX_W 表示节点之间不可达。
  2. 核心动态规划步骤:三重循环依次引入所有节点作为中间节点,对每一对节点进行路径更新。
  3. 负权环检测:最后检查 dist[i][i] 是否还能更新,如果还能更新,表示存在负权环。

6.4.3、基于邻接矩阵的 Floyd-Warshall 算法的测试

int main()
{
    const char *str = "12345";
    Lenyiin::Matrix_Graph g(str, strlen(str));
    g.AddEdge('1', '2', 3);
    g.AddEdge('1', '3', 8);
    g.AddEdge('1', '5', -4);
    g.AddEdge('2', '4', 1);
    g.AddEdge('2', '5', 7);
    g.AddEdge('3', '2', 4);
    g.AddEdge('4', '1', 2);
    g.AddEdge('4', '3', -5);
    g.AddEdge('5', '4', 6);

    std::vector> vvDist;
    std::vector> vvParentPath;
    g.Display();
    g.FloyWarshall(vvDist, vvParentPath);

    // 打印任意两点之间的最短路径
    for (size_t i = 0; i < strlen(str); i++)
    {
        g.PrintShortPath(str[i], vvDist[i], vvParentPath[i]);
        std::cout << std::endl;
    }
    
    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

在这里插入图片描述

测试结果:

在这里插入图片描述


6.4.4、基于邻接表的 Floyd-Warshall 算法的实现

// 多源最短路径
void FloyWarshall(std::vector> &vvDist, std::vector> &vvpPath)
{
    size_t n = _vertexs.size();

    // 初始化
    vvDist.resize(n, std::vector(n, MAX_W));
    vvpPath.resize(n, std::vector(n, -1));

    // 初始化
    for (size_t i = 0; i < n; i++)
    {
        EdgeNode *cur = _linkTable[i];
        while (cur)
        {
            vvDist[i][cur->_dstIndex] = cur->_weight;
            vvpPath[i][cur->_dstIndex] = i;
            cur = cur->_next;
        }
        vvDist[i][i] = W();
    }

    // 最短路径更新
    for (size_t k = 0; k < n; k++)
    {
        for (size_t i = 0; i < n; i++)
        {
            for (size_t j = 0; j < n; j++)
            {
                if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W && vvDist[i][j] > vvDist[i][k] + vvDist[k][j])
                {
                    vvDist[i][j] = vvDist[i][k] + vvDist[k][j];
                    vvpPath[i][j] = vvpPath[k][j];
                }
            }
        }
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

6.4.5、基于邻接表的 Floyd-Warshall 算法的测试

int main()
{
    const char *str = "12345";
    Lenyiin::Table_Graph g(str, strlen(str));
    g.AddEdge('1', '2', 3);
    g.AddEdge('1', '3', 8);
    g.AddEdge('1', '5', -4);
    g.AddEdge('2', '4', 1);
    g.AddEdge('2', '5', 7);
    g.AddEdge('3', '2', 4);
    g.AddEdge('4', '1', 2);
    g.AddEdge('4', '3', -5);
    g.AddEdge('5', '4', 6);

    std::vector> vvDist;
    std::vector> vvParentPath;
    g.Display();
    g.FloyWarshall(vvDist, vvParentPath);

    // 打印任意两点之间的最短路径
    for (size_t i = 0; i < strlen(str); i++)
    {
        g.PrintShortPath(str[i], vvDist[i], vvParentPath[i]);
        std::cout << std::endl;
    }
    
    return 0;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

测试结果:

在这里插入图片描述


6.4.6、Floyd-Warshall 算法的特性和应用

特性

应用

Floyd-Warshall 常用于路由规划网络分析交通规划等需要求解多对最短路径的场景,例如在路由器中找出数据包最短转发路径、在城市交通图中规划任意两个地点的最短路径。

时间复杂度分析

Floyd-Warshall 算法的时间复杂度为 O(V3),空间复杂度为 O(V2) 。虽然时间复杂度较高,但在图的节点数相对较少、边数较多(稠密图)的情况下非常适用。对于稀疏图,通常采用 Johnson 算法来计算多源最短路径,因为它在稀疏图上具有更好的性能。

优缺点总结

Floyd-Warshall 算法是多源最短路径问题的一种经典解决方案,通过多轮迭代的路径更新,最终获得了所有节点对间的最短路径。在包含负权边或稠密图的场景中,这一算法提供了高效而直观的解决方案。


6.5、Dijkstra、Bellman-Ford 和 Floyd-Warshall 三种算法的优缺点对比

DijkstraBellman-FordFloyd-Warshall 是三种常用的图算法,分别解决不同的最短路径问题。它们在算法设计、适用场景和复杂度方面各有特点,以下是对三种算法优缺点的详细对比:

6.5.1、Dijkstra 算法

优点

缺点

6.5.2、Bellman-Ford 算法

优点

缺点

6.5.3、Floyd-Warshall 算法

优点

缺点

6.5.4、总结对比表

class="table-box">
算法适用图类型时间复杂度优点缺点
Dijkstra无负权边图O(Elog⁡V)计算效率高,适合单源最短路径问题,适合稀疏图不支持负权边,不适合多源问题
Bellman-Ford允许负权边的图O(V×E)支持负权边,能检测负权环,适用于负权边图速度慢,适合稠密图,无法一次性计算多源最短路径
Floyd-Warshall任意加权图O(V3)解决多源最短路径问题,支持负权边,能方便地生成完整的最短路径矩阵复杂度较高,不适合大规模稀疏图,无法直接检测负权环

6.5.5、适用场景

6.6、小结

图的最短路径问题提供了从节点到节点的距离测量工具。针对不同的图和需求,通过选择合适的最短路径算法,可以实现高效的路径规划与图分析。


7、连通性问题

图的连通性问题是图论中的核心研究内容之一,它用来研究图中节点和边的连通情况。连通性问题广泛应用于网络路由、社交网络分析、电路设计等领域,通过判断图的连通情况,可以识别网络是否连通、存在多少个连通分量、是否存在关键节点或边等。根据图的有向或无向特性,连通性可以分为无向图连通性和有向图连通性两类。

7.1、无向图的连通性

无向图中,若从一个节点出发可以通过若干条边到达另一个节点,则称这两个节点是连通的。无向图的连通性通常通过 “连通分量” 来描述。

求解无向图连通分量的算法

无向图的连通分量可以使用深度优先搜索(DFS)或广度优先搜索(BFS)求解。每次遇到未访问的节点时,通过搜索可以找到该节点所在的整个连通分量。

算法步骤

  1. 初始化一个访问数组 visited,标记节点的访问状态。
  2. 遍历图中每个节点,如果该节点未被访问,则启动 DFS 或 BFS,从该节点开始标记所有连通节点。
  3. 每次遍历完一个未访问节点的连通子图时,计数器 components 增加 1。

代码示例(使用 DFS):

void dfs(int node, vector>& graph, vector& visited) {
    visited[node] = true;
    for (int neighbor : graph[node]) {
        if (!visited[neighbor]) {
            dfs(neighbor, graph, visited);
        }
    }
}

int countConnectedComponents(vector>& graph, int n) {
    vector visited(n, false);
    int components = 0;
    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            dfs(i, graph, visited);
            components++;
        }
    }
    return components;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

7.2、有向图的连通性

在有向图中,连通性的定义更为复杂。根据图中节点间的连通情况,有向图的连通性可以分为强连通性和弱连通性:

求解强连通分量的算法

有向图的强连通分量一般可以通过 Kosaraju 算法、Tarjan 算法或 Gabow 算法来求解。其中,Kosaraju 算法较为常用,主要步骤如下:

  1. 第一次 DFS:对图进行 DFS 遍历,记录每个节点的结束时间,并将节点按结束时间降序存储。
  2. 图的转置:将图中所有边的方向反转,形成转置图。
  3. 第二次 DFS:按照第一步的结束时间降序对转置图进行 DFS,每次遍历得到的节点集合即为一个强连通分量。

代码示例(Kosaraju 算法):

void dfs1(int node, vector>& graph, vector& visited, stack& finishStack) {
    visited[node] = true;
    for (int neighbor : graph[node]) {
        if (!visited[neighbor]) {
            dfs1(neighbor, graph, visited, finishStack);
        }
    }
    finishStack.push(node);
}

void dfs2(int node, vector>& transposeGraph, vector& visited, vector& component) {
    visited[node] = true;
    component.push_back(node);
    for (int neighbor : transposeGraph[node]) {
        if (!visited[neighbor]) {
            dfs2(neighbor, transposeGraph, visited, component);
        }
    }
}

vector> findStronglyConnectedComponents(vector>& graph, int n) {
    vector visited(n, false);
    stack finishStack;
    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            dfs1(i, graph, visited, finishStack);
        }
    }

    vector> transposeGraph(n);
    for (int u = 0; u < n; u++) {
        for (int v : graph[u]) {
            transposeGraph[v].push_back(u);
        }
    }

    fill(visited.begin(), visited.end(), false);
    vector> stronglyConnectedComponents;
    while (!finishStack.empty()) {
        int node = finishStack.top();
        finishStack.pop();
        if (!visited[node]) {
            vector component;
            dfs2(node, transposeGraph, visited, component);
            stronglyConnectedComponents.push_back(component);
        }
    }
    return stronglyConnectedComponents;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

7.3、关键节点与关键边

图的连通性研究中还包含判断哪些节点或边是“关键节点”和“关键边”。如果删除某个节点或边会使图的连通性发生变化,则该节点或边称为关键节点或关键边。这类问题在网络设计、路由规划中具有重要作用。

求解关键节点和关键边的算法

判断关键节点和边常用 Tarjan 算法中的 “割点” 和 “桥” 的概念。割点是指删除该节点会增加图的连通分量的节点,桥是指删除该边会增加图的连通分量的边。Tarjan 算法可以在 O(V+E) 时间内求出所有割点和桥。

7.4、小结

图的连通性问题不仅仅用于判断图是否为连通图,还涉及连通分量、强连通分量以及关键节点和关键边等内容。根据图的有向性或无向性、边的权重等特性,连通性算法的复杂性和适用场景也不同。掌握不同的连通性问题求解方法,可以有效解决实际问题中的连通性判断、网络连通性分析和重要点的检测等需求。


8、二分图与匹配

图论中的二分图和匹配是重要的研究主题,广泛应用于任务分配、资源分配、网络流等实际场景。二分图是一种特殊的图结构,而在二分图中进行最大匹配的研究是应用的核心。理解二分图的定义、性质和求解最大匹配的方法,有助于掌握图论的应用和算法的实现。

8.1、二分图的定义与性质

二分图(Bipartite Graph)是一种特殊的无向图,节点集合可以分成两个不相交的子集 U 和 V,使得图中所有的边仅连接来自不同子集的节点,即任意一条边 (u,v) 中 u∈U 且 v∈V 。直观上,二分图没有奇数长度的环,这一特性可以通过算法来验证。

判断二分图的方法

判断一个图是否为二分图的常用方法是使用 广度优先搜索(BFS)深度优先搜索(DFS)。通过对图进行染色处理,即将每个节点染成两种颜色之一(例如红色和蓝色),如果某一节点的相邻节点能被染成不同的颜色,则该图是二分图;若遇到矛盾(相邻节点同色),则该图不是二分图。

代码示例(BFS染色法判断二分图):

bool isBipartite(vector>& graph, int n) {
    vector color(n, -1);
    for (int i = 0; i < n; i++) {
        if (color[i] == -1) {
            queue q;
            q.push(i);
            color[i] = 0;
            while (!q.empty()) {
                int node = q.front();
                q.pop();
                for (int neighbor : graph[node]) {
                    if (color[neighbor] == -1) {
                        color[neighbor] = 1 - color[node];
                        q.push(neighbor);
                    } else if (color[neighbor] == color[node]) {
                        return false;  // 相邻节点同色,非二分图
                    }
                }
            }
        }
    }
    return true;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

8.2、匹配的定义与类型

在图中,匹配(Matching)是指一个边的子集,其中每个节点最多出现在一条边中。匹配广泛用于解决配对问题,例如任务分配、工人和工作的匹配等。在二分图中,匹配问题的研究尤为常见,常见的匹配类型包括:

8.3、二分图的最大匹配

最大匹配是指在二分图中找到包含最多边的匹配。求解二分图最大匹配的算法包括匈牙利算法和 Hopcroft-Karp 算法,其中匈牙利算法较为经典。以下将介绍基于DFS的匈牙利算法。

匈牙利算法求最大匹配

匈牙利算法的核心思想是构建交替路径,从而找到一个增广路径来增大匹配。每找到一条增广路径,就可以增加一个匹配边,从而达到最大匹配。增广路径是指一条交替使用匹配边和非匹配边的路径,且起点和终点均未匹配。

算法步骤

  1. 初始化一个匹配数组 match,记录每个节点的匹配情况。
  2. 对每个未匹配节点,从该节点出发执行 DFS,寻找增广路径。
  3. 若找到增广路径,则更新匹配。
  4. 重复上述步骤,直到无法找到增广路径为止。

代码示例(邻接表存储的二分图):

bool dfs(int u, vector>& graph, vector& match, vector& visited) {
    for (int v : graph[u]) {
        if (!visited[v]) {
            visited[v] = true;
            if (match[v] == -1 || dfs(match[v], graph, match, visited)) {
                match[v] = u;
                return true;
            }
        }
    }
    return false;
}

int maxMatching(vector>& graph, int U) {
    int result = 0;
    vector match(graph.size(), -1);  // 用于记录匹配
    for (int u = 0; u < U; u++) {
        vector visited(graph.size(), false);
        if (dfs(u, graph, match, visited)) {
            result++;
        }
    }
    return result;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

Hopcroft-Karp 算法

对于大型二分图,匈牙利算法的时间复杂度较高(O(V×E))。Hopcroft-Karp 算法通过分层 BFS 和 DFS 相结合的方式优化了时间复杂度,使之达到 O ( V × E ) O(\sqrt{V} \times E) O(V ×E),适用于大规模图的匹配问题。

8.4、带权二分图的最大匹配

在一些实际问题中,二分图中的边带有权值,此时希望找到权值之和最大的匹配,即 最大权匹配。求解最大权匹配常用 Kuhn-Munkres 算法(又称为 Hungarian 算法),该算法通过对边权进行动态调整,找到具有最大权的匹配。

Kuhn-Munkres 算法的基本思想是从零匹配开始,通过调整节点的顶标(价格)逐步扩大匹配,每次找到增广路径并调整匹配,直到所有节点匹配完毕或无法增大匹配。

Kuhn-Munkres 算法步骤

  1. 初始化两个顶标(价格)数组 lxly,分别记录两个集合中节点的顶标。
  2. 计算每条边的松弛度,若松弛度为零,则可以用这条边扩展匹配。
  3. 通过增广路径不断扩展匹配,直到达到最大匹配。
  4. 若某次搜索没有找到增广路径,则更新顶标,重新搜索。

8.5、小结

二分图和匹配问题在图论中具有广泛应用,二分图的特性使得求解匹配问题相对简单。对于无权二分图,匈牙利算法和 Hopcroft-Karp 算法是常用的解决方案;对于带权二分图,Kuhn-Munkres 算法提供了最大权匹配的有效解法。掌握这些算法不仅有助于理解图的基本结构,还为复杂的配对和分配问题提供了理论支持。


9、图的高级算法

在图论中,图的高级算法涉及解决更复杂的问题,如最大流、最小割、网络流优化、连通性分析、哈密顿与欧拉路径等问题。这些算法不仅在理论上有重要的意义,也在物流、资源调度、数据传输等领域有广泛应用。以下将对一些重要的图的高级算法进行深入讨论,包括其原理、适用场景及实现方法。

9.1、最大流与最小割

在网络流中,最大流问题最小割问题是两个核心问题,它们密切相关并应用于诸如物流、交通、数据传输等场景。最大流问题是指在一个网络中找到从源点到汇点的最大流量,而最小割问题是指找到将源点和汇点分开的最小割集。

9.1.1、最大流问题

最大流问题的目标是求解一个网络中源点到汇点的最大流量。常用的求解最大流的算法包括 Ford-Fulkerson 算法Edmonds-Karp 算法

代码示例(基于邻接表的最大流实现 - Edmonds-Karp)

#include 
#include 
#include 
using namespace std;

int bfs(vector>& residualGraph, int s, int t, vector& parent) {
    fill(parent.begin(), parent.end(), -1);
    queue> q;
    q.push({s, INT_MAX});

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

        for (int v = 0; v < residualGraph.size(); v++) {
            if (parent[v] == -1 && residualGraph[u][v] > 0) {
                parent[v] = u;
                int newFlow = min(flow, residualGraph[u][v]);
                if (v == t) return newFlow;
                q.push({v, newFlow});
            }
        }
    }
    return 0;
}

int maxFlow(vector>& graph, int s, int t) {
    vector> residualGraph = graph;
    vector parent(graph.size());
    int flow = 0, newFlow;

    while (newFlow = bfs(residualGraph, s, t, parent)) {
        flow += newFlow;
        int u = t;
        while (u != s) {
            int v = parent[u];
            residualGraph[v][u] -= newFlow;
            residualGraph[u][v] += newFlow;
            u = v;
        }
    }
    return flow;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

9.1.2、最小割定理

根据最大流最小割定理,一个网络的最大流量等于从源点到汇点的最小割的容量。利用求得的最大流,可以直接得到最小割的割集。这一结果在诸如资源分配、数据传输的场景下,能帮助分析网络中的瓶颈位置。

9.1.3、Dinic 算法

Dinic 算法通过分层网络结构实现流量分配的优化,采用 BFS 和 DFS 结合的方法,时间复杂度降为 O(V2E)。相比 Edmonds-Karp 算法,Dinic 算法在大规模图中性能更优。


9.2、最小费用最大流

在网络流问题中,除了考虑最大流,还可能需要最小化传输费用。这类问题称为最小费用最大流问题(Minimum Cost Maximum Flow),应用于货物运输、资源调度等领域。最小费用最大流问题的求解方法通常是基于 SPFADijkstra 算法优化路径,并结合增广路径法来保证最大流的传输。

算法步骤

  1. 使用 Bellman-Ford 或 Dijkstra 初始化每条边的费用。
  2. 每次找到从源到汇的最短路径,并沿路径增大流量。
  3. 当找不到增广路径时,算法终止,得到最小费用最大流。

9.3、强连通分量算法

在有向图中,强连通分量(SCC, Strongly Connected Components)是指一个子图,其中任意两个节点都是相互可达的。求解强连通分量有助于理解有向图中的循环结构或独立模块。强连通分量的应用包括模块分析、分层网络等。

Kosaraju 算法基于深度优先搜索(DFS),通过两次遍历图得到所有的强连通分量。第一次遍历记录节点的访问顺序,第二次遍历按逆序访问节点并构建强连通分量。

Tarjan 算法 Tarjan 算法利用 DFS 结合低点标号找到所有强连通分量,时间复杂度为 O(V+E) 。

代码示例(Tarjan 算法实现强连通分量)

#include 
#include 
#include 
using namespace std;

void tarjanDFS(int u, int& index, vector>& graph, vector& ids, vector& low, stack& s, vector& onStack, vector>& sccs) {
    ids[u] = low[u] = index++;
    s.push(u);
    onStack[u] = true;

    for (int v : graph[u]) {
        if (ids[v] == -1) {
            tarjanDFS(v, index, graph, ids, low, s, onStack, sccs);
            low[u] = min(low[u], low[v]);
        } else if (onStack[v]) {
            low[u] = min(low[u], ids[v]);
        }
    }

    if (ids[u] == low[u]) {
        vector scc;
        while (true) {
            int v = s.top();
            s.pop();
            onStack[v] = false;
            scc.push_back(v);
            if (u == v) break;
        }
        sccs.push_back(scc);
    }
}

vector> tarjanSCC(vector>& graph) {
    int n = graph.size();
    vector ids(n, -1), low(n, -1);
    vector onStack(n, false);
    stack s;
    vector> sccs;
    int index = 0;

    for (int i = 0; i < n; i++) {
        if (ids[i] == -1) {
            tarjanDFS(i, index, graph, ids, low, s, onStack, sccs);
        }
    }
    return sccs;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

9.4、拓扑排序

拓扑排序是用于 **DAG(有向无环图)**的线性排序,使得每条边从排在前面的节点指向排在后面的节点。拓扑排序应用于依赖关系的分析,如任务调度、编译等。

9.5、最小路径覆盖

最小路径覆盖问题要求在有向无环图中,找到最少路径的集合,使得每个节点都恰好出现在一条路径中。该问题可以转化为二分图最大匹配,通过构建二分图和匈牙利算法求解。

9.6、图的颜色问题

图的颜色问题要求将图的节点着色,使得相邻节点颜色不同。该问题应用在地图着色、频率分配等场景中。常见算法包括贪心算法和回溯法。

1、图的着色算法
2、图着色问题的复杂度分析

图着色问题是典型的 NP 完全问题,复杂度随节点数目呈指数增长。对于大规模图,通常采用启发式或近似算法来获得合理解。

9.7、哈密顿路径与欧拉路径

哈密顿路径欧拉路径是图中两种特殊的路径结构。

1、哈密顿路径

哈密顿路径是经过图中每个节点且仅经过一次的路径。若该路径构成一个环,则称为哈密顿环。求解哈密顿路径属于 NP 完全问题,通常采用回溯法动态规划方法,但在一般情况下复杂度较高。

2、欧拉路径

欧拉路径是经过图中每条边且仅经过一次的路径,若构成一个环则称为欧拉环。Fleury 算法是解决欧拉路径问题的经典算法,要求图满足一定的条件(如所有顶点的度数为偶数)。

9.8、NP完全问题与启发式解法

NP 完全问题在图论中较常见,包括图的颜色问题、哈密顿路径等问题。由于这类问题的求解通常需要指数时间复杂度,实际应用中多采用启发式算法或近似算法。常用的方法包括模拟退火遗传算法等,它们在大规模问题中提供有效解。

9.9、小结

图的高级算法涵盖了网络流、最小费用流、连通分量、拓扑排序、图的颜色问题、哈密顿路径与欧拉路径以及 NP 完全问题。这些算法为复杂图问题的求解提供了理论基础,同时也在诸如调度优化、资源分配等应用场景中广泛使用。掌握这些高级算法,有助于解决更多实际问题并提升算法设计和优化能力。


10、图算法的优化与大规模图处理

随着图数据规模的不断增长,传统图算法在大规模图数据上逐渐面临性能瓶颈,如何对算法进行优化并实现有效的大规模图处理成为重要的研究方向。本文将探讨图算法的性能优化策略及在大规模图上应用的各种技术,包括分布式计算、近似算法及并行化技术。

10.1、图算法的优化策略

图算法优化的目的是在确保正确性的基础上,提高算法的效率和降低计算资源消耗,常用的优化策略包括剪枝、缓存、启发式方法以及使用适合的存储结构。

10.1.1、剪枝策略

剪枝是减少不必要的计算步骤的一种方法,通过筛选出符合条件的子集,从而减小算法的搜索空间。在图遍历和搜索中,剪枝策略常用来优化深度优先搜索(DFS)和广度优先搜索(BFS)。

10.1.2、缓存与备忘录

缓存技术可以在图算法中避免重复计算,提高算法性能。特别是在动态规划算法(如 Floyd-Warshall、Bellman-Ford)中,缓存中间结果以减少重复计算的开销。

10.1.3、启发式算法与近似算法

启发式算法通过引入问题相关的启发信息,指导算法朝最优解的方向搜索,减少不必要的计算。例如在 Dijkstra 算法中,可以引入启发式估计进行优化。

10.1.4、高效存储结构

图的存储结构在一定程度上影响算法的性能。对于不同的图算法和应用场景,选择合适的存储结构可以显著优化性能。

10.2、大规模图处理的技术

随着社交网络、知识图谱等应用场景的发展,图数据规模不断增长。传统的图算法在大规模图数据上无法有效处理,因此需要结合分布式计算、并行化和流处理等技术。

10.2.1、分布式图计算

分布式图计算将图数据划分为多个子图,并将计算任务分配到多个节点上并行处理,常用的分布式图计算框架包括 Pregel、GraphX 和 Giraph 等。

10.2.2、并行化图计算

在多核或 GPU 环境下,通过并行化处理大规模图数据是常用的策略。并行化能够充分利用多核资源,将图算法的不同部分分配到多个处理器核心上运行。

10.2.3、图的流处理技术

对于动态或实时更新的图数据(如社交网络中的好友关系更新),使用流处理技术能快速处理图的变化。流处理技术的核心思想是持续接收数据流并实时更新图的结构或属性。

10.3、典型算法的优化与大规模处理方案

以下介绍几个典型图算法在大规模处理时的优化方案。

10.3.1、Dijkstra 算法的优化

在大规模图中,传统 Dijkstra 算法由于需要维护优先队列和检查所有节点,效率较低。可以通过以下几种方式优化:

10.3.2、PageRank 的分布式实现

PageRank 作为一种迭代算法,适合分布式实现。通过将每次迭代的结果分配给各个节点,在每轮迭代中更新各个页面的权重并汇总结果。例如在 Pregel 或 GraphX 上运行 PageRank 时,每个节点只需要处理与其相关的页面并传递更新后的权重,直至结果收敛。

10.3.3、最大流算法的并行化实现

最大流算法中的 Edmonds-Karp 和 Dinic 算法均可以通过并行化来提高性能。具体可以采用以下策略:

10.4、小结

图算法的优化与大规模图处理技术在数据增长和计算资源需求方面具有重要意义。从传统算法的剪枝、缓存优化,到分布式和并行计算的引入,图算法的处理效率得到了显著提升。对于不同场景的图数据分析,选择合适的优化和处理策略,能够高效地应对复杂的图结构和大规模数据。


11、实际应用与案例分析

图论作为一门重要的数学分支,提供了研究和分析对象之间关系的强大工具。其广泛应用于通信网络、交通运输、社交网络等领域,能够有效解决复杂的关系和网络问题。本文将介绍图论在各个领域的实际应用场景,深入分析其在具体问题中的技术实现和算法选择,并探讨不同场景中图论算法的优化方案。

11.1、通信网络优化

在通信网络中,图论主要用于解决网络流量优化、路由选择以及网络覆盖等问题。节点通常代表通信设备或路由器,边表示设备之间的通信链路。

11.1.1、网络流量与最大流算法

问题:在通信网络中,需要合理配置流量分配,以避免链路拥塞,提高网络资源利用率。最大流算法是解决该问题的常用方法。

11.1.2、最短路径路由与 Dijkstra 算法

问题:在网络中寻找最短路径,以减少延迟和提高传输效率。Dijkstra 算法是解决最短路径问题的经典方法。

11.2、交通运输与物流优化

交通运输网络和物流配送问题通常可以抽象成图的路径问题。图的节点表示地点,边表示地点之间的运输路线或物流路径。

11.2.1、最短路径问题与 A* 算法

问题:在交通导航中,为了确保最佳路径的选择,需要根据实时路况和地理信息计算最短路径。A* 算法利用启发式函数选择优路径,适用于交通路径优化。

11.2.2、物流配送中的旅行商问题

问题:在物流配送中,旅行商问题(TSP)是一个典型的 NP 难题,即寻找一条遍历所有地点且路径最短的路径。

11.3、社交网络分析

社交网络中的节点表示用户,边表示用户之间的关系(如好友、关注等)。图论应用在社交网络中主要解决社交影响力分析、社区检测和信息传播等问题。

11.3.1、社区检测与图的划分

问题:社交网络中通常存在紧密关联的社区或群体,识别这些社区有助于精准营销和舆情分析。

11.3.2、信息传播模型与 SIR 模型

问题:研究信息在网络中的传播路径,尤其是热点事件的扩散,能够帮助预测舆情走势和用户行为。

11.4、图像处理与计算机视觉

图像处理和计算机视觉中,图论被用于图像分割、模式识别和物体检测。图的节点表示图像像素或图像块,边表示像素之间的相似性或距离。

11.4.1、图像分割与最小割算法

问题:图像分割将图像分为不同区域,用于识别图像中的特定对象或模式。最大流最小割算法是解决该问题的常用方法。

11.4.2、特征提取与路径算法

问题:在物体识别中,特征提取是关键步骤,通常需要计算物体轮廓的最优路径以进行特征匹配。

11.5、生物信息学中的网络分析

生物信息学利用图论来分析分子结构、基因网络和蛋白质相互作用。节点可以表示基因或蛋白质,边表示其相互作用。

11.5.1、蛋白质相互作用网络

问题:分析蛋白质相互作用网络,有助于揭示生物体内分子机制。通过图的中心性和路径算法,可以识别关键蛋白质及其作用关系。

11.5.2、基因网络中的最短路径

问题:在基因网络中,识别影响基因表达的最短路径,能够帮助生物学家理解基因调控机制。

11.6、小结

图论在实际应用中的潜力巨大,从通信网络的优化到社交网络的分析,再到图像处理和生物信息学,图论为复杂网络提供了有效的解决方案。在不同应用场景下,通过选择合适的图算法和优化策略,可以实现高效的网络分析和信息挖掘。未来,随着数据规模的进一步增大和计算能力的提升,图论在多领域的应用前景将更加广阔。


12、面试中的常见图问题

图论是计算机科学和算法的核心主题之一,因此在算法和编程面试中经常被考察。图问题通常要求候选人具备对不同图结构和算法的理解,并能够选择合适的算法来高效地解决特定问题。以下将介绍面试中常见的图论问题及其解决方法,并为每个问题提供相应的解题思路和技巧。

12.1、图的遍历

图的遍历是解决许多图问题的基础操作。在面试中,通常会要求候选人实现图的遍历算法,以检查其对图结构的理解。

12.1.1、深度优先搜索(DFS)

问题:实现图的深度优先搜索。

解法:DFS 使用递归或栈实现,依次访问当前节点的所有邻居,并在访问每个邻居之前标记节点为已访问,以防止重复访问。

应用:DFS 常用于连通性检查、拓扑排序、以及图的连通分量的发现等问题。

12.1.2、广度优先搜索(BFS)

问题:实现图的广度优先搜索。

解法:BFS 使用队列实现,从起始节点依次访问所有邻居,随后访问邻居的邻居,直至所有可到达节点均被访问。

应用:BFS 适用于无权图的最短路径查找、网络中的最小跳数计算等问题。

12.2、最短路径问题

最短路径问题是图论中的经典问题之一。在面试中通常会考察以下几种最短路径算法及其适用场景。

12.2.1、单源最短路径(Dijkstra 算法)

问题:求解一个有权无负边图的单源最短路径。

解法:Dijkstra 算法使用优先队列(通常为最小堆)来维护未访问节点的最短路径估计值,不断从起始节点扩展最小权重的路径。

扩展:可以使用邻接表和邻接矩阵两种方式存储图结构并实现 Dijkstra 算法。

12.2.2、含负权图的单源最短路径(Bellman-Ford 算法)

问题:在包含负权边的图中求解单源最短路径。

解法:Bellman-Ford 算法对所有边进行 V−1 次松弛操作,以确保找到从源点到其他节点的最短路径,若在 V 次松弛后仍有更新,则存在负权环。

12.2.3、所有点对最短路径(Floyd-Warshall 算法)

问题:在加权图中求解所有点对的最短路径。

解法:Floyd-Warshall 是基于动态规划的算法,利用三重循环构建一个 V×V 的矩阵,记录从每个节点到其他节点的最短距离。

12.3、拓扑排序

拓扑排序适用于有向无环图(DAG),用于将节点按依赖关系排序。

问题:给定一个有向无环图,对其节点进行拓扑排序。

解法:可以使用 DFS 结合栈的方式实现,也可以使用 BFS 结合入度表实现。DFS 法通过递归调用依次将节点压栈,BFS 法则根据入度表来消除环外节点并依次排序。

12.4、联通分量和环检测

连通性和环检测是图论中的基础问题,通常用在图的结构分析中。

12.4.1、连通分量检测

问题:计算无向图中的连通分量。

解法:可以使用 DFS 或 BFS 从每个未访问的节点开始遍历,记录访问的节点数,直到所有节点都被标记为访问过。

12.4.2、环检测

问题:判断一个图是否包含环。

解法

12.5、二分图与最大匹配

二分图及其最大匹配问题也是图论中的常见面试问题,尤其在网络流和分配问题中具有广泛应用。

12.5.1、 二分图检测

问题:判断一个图是否为二分图。

解法:可以使用 BFS 或 DFS 为图中的每个节点染色。若相邻节点颜色相同,则图不是二分图。

12.5.2、匈牙利算法求最大匹配

问题:在二分图中求最大匹配。

解法:匈牙利算法是一种用于二分图匹配的贪心算法。可以通过 DFS 从每个节点开始寻找增广路径,从而得到最大匹配。

12.6、网络流问题

网络流问题在交通、物流、网络流量优化中应用广泛。

12.6.1、最大流问题(Ford-Fulkerson 与 Edmonds-Karp)

问题:在网络图中求最大流。

解法:Ford-Fulkerson 算法通过找到增广路径来计算最大流。Edmonds-Karp 算法是 Ford-Fulkerson 的改进版本,利用 BFS 计算增广路径。

12.6.2、最小割问题

问题:求图的最小割以优化资源分配。

解法:可以通过最大流最小割定理求解。即通过求解最大流找到图的最小割。

12.7、哈密顿路径与欧拉路径

这类问题常用于路径覆盖和图的遍历。

12.7.1、欧拉路径和欧拉回路

问题:判断图中是否存在欧拉路径或欧拉回路。

解法:通过检查每个节点的度数判断。对于无向图,若有 0 个或 2 个奇数度节点,则存在欧拉路径;若所有节点度数为偶数,则存在欧拉回路。

12.7.2、哈密顿路径与 NP 难问题

问题:判断图中是否存在哈密顿路径。

解法:哈密顿路径问题为 NP 难问题,通常在面试中不会要求求解完整路径,但会询问算法设计思路或近似解法。

12.8、小结

图论面试题涉及广泛的算法和应用场景。在准备面试时,候选人应重点掌握基本图算法(如 DFS、BFS、最短路径算法)、图的结构分析(如环检测、连通分量)、以及特定应用问题(如最大流、最小割)。掌握这些知识有助于在面试中灵活应用图论方法解决多样的问题。


13、总结

本篇博客全面探讨了图论的各个核心领域,涵盖了图的基础概念、常见存储结构、重要算法、以及图在实际应用中的重要性和实用性。通过从理论基础到高级算法的逐步讲解,这篇文章为读者提供了一个系统的学习路径,并帮助他们深入理解图论在计算机科学和工程应用中的广泛用途。

本篇博客通过详细的讲解和技术深度的分析,为读者提供了一个全面的图论知识体系。从基础概念到高级算法,从理论分析到实际应用,每个章节都从不同角度揭示了图论的广泛应用和技术价值。图论在计算机科学、数据分析、网络设计等领域具有广泛应用,而本篇文章不仅帮助读者掌握图论的基础知识,还培养了他们对图论算法的技术敏感性和问题解决能力。希望通过本篇博客,读者能在未来的学习和实践中更加自信地应用图论技术,解决实际问题并应对面试挑战。


希望这篇博客对您有所帮助,也欢迎您在此基础上进行更多的探索和改进。如果您有任何问题或建议,欢迎在评论区留言,我们可以共同探讨和学习。更多知识分享可以访问 **我的个人博客网站 **。



data-report-view="{"mod":"1585297308_001","spm":"1001.2101.3001.6548","dest":"https://blog.csdn.net/mmlhbjk/article/details/143261300","extend1":"pc","ab":"new"}">>
注:本文转载自blog.csdn.net的Lenyiin的文章"https://blog.csdn.net/mmlhbjk/article/details/143261300"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接

评论记录:

未查询到任何数据!