图|dfs bfs|最小生成树|最短路|一篇搞定图的所有知识点

文章目录

    • 前言
    • 项目代码仓库
    • 图的基本概念
    • 图的表示方法
      • 邻接矩阵
      • 邻接表
      • 图的一些相关概念
    • 图的遍历
      • bfs
      • dfs
      • 如果给的图不是连通图?
    • 最小生成树
      • Kruskal算法
      • Prim算法
    • 最短路径
      • 单源最短路径--Dijkstra算法
      • 单源最短路径--Bellman-Ford算法
      • 多源最短路径--Floyd-Warshall算法

前言

这里分享我的一些博客专栏,都是干货满满的。

  • 手撕数据结构专栏
  • 高质量博客专栏

项目代码仓库

  • CPlusPlus-review-main/

图的基本概念

一般表示成 G = (V, E)。

图的表示方法

  1. 邻接矩阵

  2. 邻接表

邻接矩阵

邻接矩阵存储方式非常适合稠密图。邻接矩阵可以O(1)判断两个顶点的连接关系,并拿到权值。

邻接矩阵不适合用来查找,一个顶点连接的所有边,这个是O(n)的。

邻接矩阵的表示:

namespace yufc_graph_matrix
{
    template <class vertex_type, class weight_type, weight_type __max_weight = INT_MAX, bool direction = false>
    class graph
    {
    private:
        std::vector<vertex_type> __vertexs;             // 顶点的集合
        std::map<vertex_type, int> __index_map;         // 顶点映射的下标
        std::vector<std::vector<weight_type>> __matrix; // 邻接矩阵
    public:
        // 图的创建
        /*
            三种创建图的方法:
            1. IO输入 -- 不方便测试,oj中更合适
            2. 图结构关系写到文件,读取文件
            3. 手动添加边
        */
        graph(const vertex_type *a, size_t n)
        {
            __vertexs.reserve(n);
            for (size_t i = 0; i < n; i++)
            {
                __vertexs.push_back(a[i]); // 每个顶点存进去
                __index_map[a[i]] = i;     // 顶点映射的下标,就是这个顶点是__vertexs中哪一个下标上的
            }
            __matrix.resize(n);
            for (size_t i = 0; i < __matrix.size(); ++i)
            {
                __matrix[i].resize(n, __max_weight);
            }
        }
        size_t get_vertex_index(const vertex_type &v)
        {
            /* 确定顶点的下标 */
            auto it = __index_map.find(v);
            if (it != __index_map.end())
                return it->second;
            else
            {
                throw std::invalid_argument("vertex not exists");
                return -1;
            }
        }
        void add_edge(const vertex_type &src, const vertex_type &dest, const weight_type &weight)
        {
            size_t srci = get_vertex_index(src);
            size_t desti = get_vertex_index(dest);
            __matrix[srci][desti] = weight;
            // 如果是无向图
            if (direction == false)
                __matrix[desti][srci] = weight;
        }

    public:
        void print()
        {
            // 把顶点打印出来
            for (size_t i = 0; i < __vertexs.size(); i++)
                std::cout << "[" << i << "]"
                          << "->" << __vertexs[i] << std::endl;
            std::cout << std::endl;
            // 打印一下矩阵
            // 先打印一下下标
            std::cout << "  ";
            for (size_t i = 0; i < __vertexs.size(); ++i)
                std::cout << i << " ";
            std::cout << std::endl;
            for (size_t i = 0; i < __matrix.size(); ++i)
            {
                // 先打印一下下标
                std::cout << i << " ";
                for (size_t j = 0; j < __matrix[i].size(); ++j)
                    if (__matrix[i][j] == __max_weight)
                        std::cout << "* ";
                    else
                        std::cout << __matrix[i][j] << " ";
                std::cout << std::endl;
            }
        }
    };
}

邻接表

邻接表是一个指针数组,把自己连接的顶点都挂在下面。

适合存储稀疏图。适合查找一个顶点连接出去的边。

不适合确定两个顶点是否相连,及其权值

邻接表的表示:

namespace yufc_graph_link_table
{
    template <class weight_type>
    struct edge
    {
        int __dest_idx;            // 目标点的下标
        weight_type __weight;      // 权值
        edge<weight_type> *__next; // 链接指针
        edge(int desti, const weight_type &weight)
            : __dest_idx(desti), __weight(weight), __next(nullptr) {}
    };
    template <class vertex_type, class weight_type, bool direction = false>
    class graph
    {
    private:
        typedef edge<weight_type> Edge;
        std::vector<vertex_type> __vertexs;     // 顶点的集合
        std::map<vertex_type, int> __index_map; // 顶点映射的下标
        std::vector<Edge *> __table;            // 下面挂边
    public:
        graph(const vertex_type *a, size_t n)
        {
            __vertexs.reserve(n);
            for (size_t i = 0; i < n; i++)
            {
                __vertexs.push_back(a[i]); // 每个顶点存进去
                __index_map[a[i]] = i;     // 顶点映射的下标,就是这个顶点是__vertexs中哪一个下标上的
            }
            // 初始化邻接表
            __table.resize(n, nullptr);
        }
        size_t get_vertex_index(const vertex_type &v)
        {
            /* 确定顶点的下标 */
            auto it = __index_map.find(v);
            if (it != __index_map.end())
                return it->second;
            else
            {
                throw std::invalid_argument("vertex not exists");
                return -1;
            }
        }
        void add_edge(const vertex_type &src, const vertex_type &dest, const weight_type &weight)
        {
            size_t srci = get_vertex_index(src);
            size_t desti = get_vertex_index(dest);
            Edge *eg = new Edge(desti, weight);
            // 头插
            eg->__next = __table[srci];
            __table[srci] = eg;
            if (direction == false)
            {
                // 无向图反过来还要弄一下才行
                Edge *eg = new Edge(srci, weight);
                eg->__next = __table[desti];
                __table[desti] = eg;
            }
        }

    public:
        void print()
        {
            // 把顶点打印出来
            for (size_t i = 0; i < __vertexs.size(); i++)
                std::cout << "[" << i << "]"
                          << "->" << __vertexs[i] << std::endl;
            std::cout << std::endl;
            //
            for (size_t i = 0; i < __table.size(); ++i)
            {
                std::cout << __vertexs[i] << "[" << i << "]->";
                Edge *cur = __table[i]; // 遍历一下单链表而已
                while (cur)
                {
                    std::cout << cur->__weight << "->" << __vertexs[cur->__dest_idx] << "[" << cur->__dest_idx << "]->";
                    cur = cur->__next;
                }
                std::cout << "nullptr" << std::endl;
            }
        }
    };
}

图的一些相关概念

完全图:在有n个顶点的无向图中,若有 n ∗ ( n − 1 ) / 2 n*(n-1)/2 n(n1)/2条边,即任意两个顶点之间有且仅有一条边,则称此图为无向完全图。在n个顶点的有向图中,若有 n ∗ ( n − 1 ) n*(n-1) n(n1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图。

邻接顶点:在无向图中G中,若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点,并称边(u,v)依附于顶点u和v;在有向图G中,若<u, v>是E(G)中的一条边,则称顶点u邻接到v,顶点v邻接自顶点u,并称边<u, v>与顶点u和顶点v相关联。

顶点的度:顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中,顶点的度等于该顶点的入度与出度之和,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v);顶点v的出度 是以v为起始点的有向边的条数,记作outdev(v)。因此:dev(v) = indev(v) + outdev(v)。注意:对于无向图,顶点的度等于该顶点的入度和出度,即dev(v) = indev(v) = outdev(v)

路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶 点序列为从顶点vi到顶点vj的路径。

路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和。

简单路径与回路:若路径上各顶点v1,v2,v3,…,vm均不重复,则称这样的路径为简单路径。若路径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环。

子图:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图。

连通图:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。

强连通图:在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj到vi的路径,则称此图是强连通图。

生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边。

#BUG: 0206发现潜在问题:如果一直给无向图add_edge,重复的边权值矛盾了怎么处理,要记得处理一下(还未处理)

图的遍历

两种遍历针对的都是图的顶点。

然后下面的代码先用了邻接矩阵的方式来实现了,不过用邻接表也是一样的,自行改一下就行,主要是思路而已。

bfs

广度优先遍历,然后我们都用这个图。

第一次先把A入队列。

此时队列为:A

然后A出来,把和A相连的带进来(BCD)。

此时队列为:B C D

此时B出来,把和B相连的带进来,和B相连的有E,C,A。不用说,E肯定是要进来的,AC进不进队列呢?

A不要进了,C可以进,因为此时C还没有被访问过。这时候虽然队列里面有两个C,但是也没问题,到时候的访问过的节点,不要再去访问就行了。

然后怎么标记呢,可以用一个set去记录一下下标(元素可能是其他类型,不好搞,所以就记录下标就行了)

当然可以不这样标记也可以,我们可以把进队列的标记一下,这样效率高一点

代码如下所示:

void bfs(const vertex_type &src)
{
    // 需要给一个起点
    size_t srci = get_vertex_index(src); // 找到起点的下标
    std::queue<int> q;
    std::vector<bool> visited(__vertexs.size(), false); // 所有顶点一开始先标记成false
    q.push(srci);                                       // 起点入队列
    visited[srci] = true;                               // 标记起点,因为起点已经入队列了
    while (!q.empty())
    {
        // 队列不为空就继续遍历
        int front = q.front();                                             // 队头的数据
        std::cout << __vertexs[front] << "[" << front << "]" << " "; // 访问这个值
        q.pop();
        // 把和front相连的顶点入队
        for (size_t i = 0; i < __vertexs.size(); ++i)
        {
            if (__matrix[front][i] != __max_weight && visited[i] == false)
            {
                q.push(i);         // 和当前顶点相连的节点
                visited[i] = true; // 入队列的,标记一下
            }
        }
    }
    std::cout << std::endl << "bfs done!" << std::endl;
}

输出:

A[0] B[1] C[2] D[3] E[4] F[5] G[6] H[7] I[8] 
bfs done!

这个也符合我们的预期。

同时,我们也可以给这个bfs进行改进,因为做力扣的时候是有一个二叉树的层序遍历的题目的,如果我想知道每一层分别是什么,我们在代码中也可以控制,下面是改进后的版本。

// 遍历
void bfs(const vertex_type &src)
{
    // 需要给一个起点
    size_t srci = get_vertex_index(src); // 找到起点的下标
    std::queue<int> q;
    std::vector<bool> visited(__vertexs.size(), false); // 所有顶点一开始先标记成false
    q.push(srci);                                       // 起点入队列
    visited[srci] = true;                               // 标记起点,因为起点已经入队列了
    int levelSize = 1;
    while (!q.empty())
    {
        // 控制一次出一层
        for (size_t i = 0; i < levelSize; i++)
        {
            // 队列不为空就继续遍历
            int front = q.front(); // 队头的数据
            std::cout << __vertexs[front] << "[" << front << "]"
                        << " "; // 访问这个值
            q.pop();
            // 把和front相连的顶点入队
            for (size_t i = 0; i < __vertexs.size(); ++i)
            {
                if (__matrix[front][i] != __max_weight && visited[i] == false)
                {
                    q.push(i);         // 和当前顶点相连的节点
                    visited[i] = true; // 入队列的,标记一下
                }
            }
        }
        std::cout << std::endl; // 出完每一层才去换行
        levelSize = q.size();   // 此时levelSize就是下一层个数,就是现在队列的元素个数,因为我们已经把当前层出完了,剩下的都是下一层的
    }
    std::cout << "bfs done!" << std::endl;
}

输出:

A[0] 
B[1] C[2] D[3] 
E[4] F[5] 
G[6] H[7] 
I[8] 
bfs done!

这样的结果也是符合预期的。

dfs

深度优先遍历。

深度优先遍历可以用递归。

void __dfs(size_t srci, std::vector<bool>& visited)
{
    
}
void dfs(const vertex_type &src)
{
    size_t srci = get_vertex_index(src);
}

肯定就要写一个子函数了,因为从A开始走要转化成从B开始走,从C开始走…

然后带上一个visited数组就行,记录哪些点是走过的。

完成后的代码如下所示:

void __dfs(size_t srci, std::vector<bool> &visited)
{
    std::cout << __vertexs[srci] << "[" << srci << "]" << " ";
    visited[srci] = true; // 标记访问过了
    // 找一个srci相邻的,没有访问过的点去往深度遍历
    for (size_t i = 0; i < __vertexs.size(); ++i)
        if (__matrix[srci][i] != __max_weight && visited[i] == false) // 遍历矩阵里面有连接的点就行
            __dfs(i, visited);
}
void dfs(const vertex_type &src)
{
    size_t srci = get_vertex_index(src);
    std::vector<bool> visited(__vertexs.size(), false);
    __dfs(srci, visited);
    std::cout << std::endl << "dfs done" << std::endl;
}

输出结果:

A[0] B[1] C[2] F[5] D[3] H[7] I[8] E[4] G[6] 
dfs done

这个结果,看图,也是符合我们dfs预期的

如果给的图不是连通图?

如果给的图不是连通图?以某个点为起点就不能遍历完成,那么怎么保证遍历完剩下的点?

  • 在visited数组找没有遍历过的点然后再去遍历就行了

最小生成树

一定是在连通图中找生成树。

连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不再连通

反之,在其中引入任何一条新边,都会形成一条回路。

若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。因此构造最小生成树的准则有三条:

  1. 只能使用图中的边来构造最小生成树

  2. 只能使用恰好n-1条边来连接图中的n个顶点

  3. 选用的n-1条边不能构成回路

构造最小生成树的方法: Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略。

  • 贪心算法:是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体最优的的选择,而是某种意义上的局部最优解。

  • 贪心算法不是对所有的问题都能得到整体最优解。

Kruskal算法

任给一个有n个顶点的连通网络 N = { V , E } N=\{V,E\} N={V,E}

首先构造一个由这n个顶点组成、不含任何边的图 G = { V , N U L L } G=\{V,NULL\} G={V,NULL},其中每个顶点自成一个连通分量。

其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。如此重复,直到所有顶点在同一个连通分量上为止。

核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。

那怎么判断新加入的边是否会构成环呢?并查集就行了。

如果新加入的边的两个顶点已经属于同一个并查集了,那么这两个点已经连通,因此这条新的边不能加入到生成树当中。

我们直接看例子,这个例子是我在《算法导论》上截取下来的例子。

下面就是代码实现。

这里的测试用例,使用的是算法导论上面这张图的例子进行测试的。

// 最小生成树
weight_type Kruskal(self &minTree)
{
    // 要先初始化最小生成树
    size_t n = __vertexs.size();
    minTree.__vertexs = this->__vertexs;
    minTree.__index_map = this->__index_map;
    minTree.__matrix.resize(n);
    for (size_t i = 0; i < n; i++)
        minTree.__matrix[i].resize(n, __max_weight);
    assert(direction == false); // 只能无向图
    // 如果有最小生成树,就返回到minTree里面,如果没有,就返回一个默认的weight_type
    // 然后比较边,优先队列要重载一个比较才行
    std::priority_queue<__edge, std::vector<__edge>, std::greater<__edge>> minq;
    for (size_t i = 0; i < n; i++)
        for (size_t j = 0; j < n; j++)
            if (i < j && __matrix[i][j] != __max_weight) // 表示有这条边,那就把这条边加入到优先队列中, i<j 可以保证无向图不会重复添加边
                minq.push(__edge(i, j, __matrix[i][j]));
    // 找到最小的边,依次添加
    int size = 0;                             // 选出n-1条边就行了
    weight_type total_weight = weight_type(); // 总的权值
    union_find_disjoint_set<int> ufs(n); // 这里不能用size_t
    while (!minq.empty())
    {
        __edge min = minq.top(); // 选出一条边
        minq.pop();
        if (ufs.InSet(min.__srci, min.__dsti))
            continue;                                             // 如果这个边的两个顶点在一个集合里面,直接跳过本轮
        minTree.__add_edge(min.__srci, min.__dsti, min.__weight); // 添加边就行了
        ufs.Union(min.__srci, min.__dsti);
        size++;
        total_weight += min.__weight;
    }
    if (size == n - 1)
        return total_weight;
    else
        return weight_type(); // 如果找不到,就返回一个缺省值
}

这个输出也是没有问题的,符合预期。

Kruskal:37
[0]->a
[1]->b
[2]->c
[3]->d
[4]->e
[5]->f
[6]->g
[7]->h
[8]->i

  0 1 2 3 4 5 6 7 8 
0 * 4 * * * * * 8 * 
1 4 * * * * * * * * 
2 * * * 7 * 4 * * 2 
3 * * 7 * 9 * * * * 
4 * * * 9 * * * * * 
5 * * 4 * * * 2 * * 
6 * * * * * 2 * 1 * 
7 8 * * * * * 1 * * 
8 * * 2 * * * * * *

当然,如果想看选边的过程,也是在代码中打印出来,就能看到了。

Prim算法

与Kruskal算法类似。Prim算法的工作原理与Dijkstra的最短路径算法相似。Prim算法所具有的一个性质是集合A中的边总是构成一棵树。如图所示,还是这个图,这棵树从一个任意的根结点r开始,一直长大到覆盖V中的所有结点时为止。算法每一步在连接集合A和A之外的结点的所有边中,选择一条轻量级边加入到A中。这条规则所加入的边都是对A安全的边。因此,当算法终止时,A中的边形成一棵最小生成树。本策略也属于贪心策略,因为每一步所加人的边都必须是使树的总权重增加量最小的边。

Prim算法代码:

weight_type Prim(self &minTree, const weight_type &src)
{
    size_t srci = get_vertex_index(src);
    size_t n = __vertexs.size();
    minTree.__vertexs = __vertexs;
    minTree.__index_map = __index_map;
    minTree.__matrix.resize(n);
    for (size_t i = 0; i < n; ++i)
    {
        minTree.__matrix[i].resize(n, __max_weight);
    }
    std::vector<bool> X(n, false);
    std::vector<bool> Y(n, true);
    X[srci] = true;
    Y[srci] = false;
    // 从X->Y集合中连接的边里面选出最小的边
    std::priority_queue<__edge, std::vector<__edge>, std::greater<__edge>> minq;
    // 先把srci连接的边添加到队列中
    for (size_t i = 0; i < n; ++i)
        if (__matrix[srci][i] != __max_weight)
            minq.push(__edge(srci, i, __matrix[srci][i]));

    // std::cout << "Prim开始选边" << std::endl;
    size_t size = 0;
    weight_type totalW = weight_type();
    while (!minq.empty())
    {
        __edge min = minq.top();
        minq.pop();
        // 最小边的目标点也在X集合,则构成环
        if (X[min.__dsti])
        {
            // 构成环
            // print logs or do nothing
        }
        else
        {
            minTree.__add_edge(min.__srci, min.__dsti, min.__weight);
            X[min.__dsti] = true;
            Y[min.__dsti] = false;
            ++size;
            totalW += min.__weight;
            if (size == n - 1) // 找够了边
                break;
            // 把Y集合连出去的边都加入加入到优先队列里面,准备下一次循环选最小的边
            for (size_t i = 0; i < n; ++i)
                if (__matrix[min.__dsti][i] != __max_weight && Y[i])
                    minq.push(__edge(min.__dsti, i, __matrix[min.__dsti][i]));
        }
    }

    if (size == n - 1)
        return totalW;
    else
        return weight_type();
}

最短路径

最短路径问题:从在带权有向图G中的某一顶点出发,找出一条通往另一顶点的最短路径,最短也就是沿路径各边的权值总和达到最小。

单源最短路径–Dijkstra算法

单源最短路径问题:给定一个图G=(V, E)。求源结点s∈V到图中每个结点v∈V的最短路径。

Dijkstra算法就适用于解决带权重的有向图上的单源最短路径问题,同时算法要求图中所有边的权重非负。一般在求解最短路径的时候都是已知一个起点和一个终点,所以使用Dijkstra算法求解过后也就得到了所需起点到终点的最短路径。

针对一个带权有向图G,将所有结点分为两组S和Q,S是已经确定最短路径的结点集合,在初始时为空(初始时就可以将源节点s放入,毕竟源节点到自己的代价是0),Q为其余未确定最短路径的结点集合,每次从Q中找出一个起点到该结点代价最小的结点u,将u从Q中移出,并放入S中,对u的每一个相邻结点v进行松弛操作。松弛即对每一个相邻结点v,判断源节点s到结点u的代价与u到v 的代价之和是否比原来s到v的代价更小,若代价比原来小则要将s到v的代价更新为s到u与u到v的代价之和,否则维持原样。如此一直循环直至集合Q为空,即所有节点都已经查找过一遍并确定了最短路径,至于一些起点到达不了的结点在算法循环后其代价仍为初始设定的值,不发生变化。Dijkstra算法每次都是选择V-S中最小的路径节点来进行更新,并加入S中,所以该算法使用的是贪心策略。

Dijkstra算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径。

代码如下所示:

void Dijkstra(const vertex_type &src, std::vector<weight_type> &dist, std::vector<int> &parent_path)
{
    // 用一个数组存 s->{yztx} 的距离
    size_t srci = get_vertex_index(src);
    size_t n = __vertexs.size();
    dist.resize(n, __max_weight);
    parent_path.resize(n, -1);
    dist[srci] = 0; // 自己到自己的长度就是0
    parent_path[srci] = srci;
    std::vector<bool> S(n, false); // 已经确定最短路径的顶点集合
    for (size_t cnt = 0; cnt < n; ++cnt)
    {
        // 选最短路径顶点且不在S更新其他路径
        int u = 0;
        weight_type min = __max_weight;
        for (size_t i = 0; i < n; ++i)
        {
            if (S[i] == false && dist[i] < min)
            {
                u = i;
                min = dist[i];
            }
        }
        // 选到最小的点叫u
        S[u] = true;
        // 松弛更新 srci->u 已经确定了 u->... 需要更新
        for (size_t v = 0; v < n; v++)
        {
            // 确认u连接的所有边v
            /*srci->u 就是 dist[u]
            u->v 就是 __matrix[u][v]
            srci->v 就是 dist[v]
            如果 srci->u + u->v < srci->v 更新 srci->v*/
            if (S[v] == false && __matrix[u][v] != __max_weight && dist[u] + __matrix[u][v] < dist[v])
            {
                dist[v] = dist[u] + __matrix[u][v];
                parent_path[v] = u; // v的父亲路径节点就不是srci了,就是u了
            }
        }
    }
}

所以我们也需要一个打印最短路径的接口,后面我们也要用到的。

// 打印最短路径
void print_short_path(const vertex_type &src, std::vector<weight_type> &dist, const std::vector<int> &parent_path)
{
    size_t srci = get_vertex_index(src);
    size_t n = __vertexs.size();
    for (size_t i = 0; i < n; ++i)
    {
        if (i == srci)
            continue;
        // 找出i顶点的路径
        std::vector<int> current_path;
        size_t parent_i = i;
        while (parent_i != srci)
        {
            current_path.push_back(parent_i); // 先把自己存进去,然后找上一层父亲
            parent_i = parent_path[parent_i];
        }
        current_path.push_back(srci); // 最后把原点加进去就行了
        std::reverse(current_path.begin(), current_path.end());
        // 此时已经找到路径了
        for (const auto &e : current_path)
            std::cout << __vertexs[e] << "[" << e << "]"
                        << "->";
        std::cout << "<" << dist[i] << ">" << std::endl;
    }
}

迪杰斯特拉算法中,如果顶点数量为N,时间复杂度: O(N^2),空间复杂度: O(N)

单源最短路径–Bellman-Ford算法

Dijkstra算法只能用来解决正权图的单源最短路径问题,但有些题目会出现负权图。这时这个算法 就不能帮助我们解决问题了,而bellman—ford算法可以解决负权图的单源最短路径问题。它的 优点是可以解决有负权边的单源最短路径问题,而且可以用来判断是否有负权回路。它也有明显 的缺点,它的时间复杂度 O(N*E) (N是点数,E是边数)普遍是要高于Dijkstra算法O(N2)的。像这里 如果我们使用邻接矩阵实现,那么遍历所有边的数量的时间复杂度就是O(N^3),这里也可以看出 来Bellman-Ford就是一种暴力求解更新。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Bellman-Ford算法的SPFA优化:SPFA算法

  1. 第一轮更新:所有边入队列
  2. 后面的轮次:更新最短路径的边入队列

代码:

bool BellManFord(const vertex_type &src, std::vector<weight_type> &dist, std::vector<int> &parent_path)
{
    /*
    return value:
        false: There is a negative weight circuit present
        true: Find the shortest path
    */
    size_t N = __vertexs.size();
    size_t srci = get_vertex_index(src);
    dist.resize(N, __max_weight);
    parent_path.resize(N, -1);
    dist[srci] = weight_type();
    for (size_t k = 0; k < N; k++)
    {
        std::cout << "update round: " << k << std::endl;
        // i->j 更新k次
        bool is_update = false;
        for (size_t i = 0; i < N; i++)
            for (size_t j = 0; j < N; j++)
                // srci->i + i->j < src->j
                if (__matrix[i][j] != __max_weight && dist[i] + __matrix[i][j] < dist[j])
                {
                    dist[j] = dist[i] + __matrix[i][j];
                    parent_path[j] = i;
                    is_update = true;
                }
        // 如果此轮没有更新出最短路径,后面的轮次就不用再走了
        if (is_update == false)
            break;
    }
    // 前面其实已经跟新完成了,如果还能发生更新,说明会出现负环情况
    for (size_t i = 0; i < N; i++)
        for (size_t j = 0; j < N; j++)
            if (__matrix[i][j] != __max_weight && dist[i] + __matrix[i][j] < dist[j])
                return false; // 如果还能发生更新,返回false
    return true;
}

多源最短路径–Floyd-Warshall算法

Floyd-Warshall算法是解决任意两点间的最短路径的一种算法。

Floyd算法考虑的是一条最短路径的中间节点,即简单路径p={v1,v2,...,vn}上除v1和vn的任意节点。 设k是p的一个中间节点,那么从i到j的最短路径p就被分成i到k和k到j的两段最短路径p1,p2。p1 是从i到k且中间节点属于{1,2,...,k-1}取得的一条最短路径。p2是从k到j且中间节点属于{1, 2,...,k-1}取得的一条最短路径。

Floyd-Warshall算法的原理是动态规划

D i , j , k D_{i, j, k} Di,j,k 为从 i i i j j j 的只以 ( 1.. k ) (1 . . k) (1..k) 集合中的节点为中间节点的最短路径的长度。

  1. 若最短路径经过点 k \mathrm{k} k , 则 D i , j , k = D i , k , k − 1 + D k , j , k − 1 D_{i, j, k}=D_{i, k, k-1}+D_{k, j, k-1} Di,j,k=Di,k,k1+Dk,j,k1 ;
  2. 若最短路径不经过点 k \mathrm{k} k , 则 D i , j , k = D i , j , k − 1 D_{i, j, k}=D_{i, j, k-1} Di,j,k=Di,j,k1

因此, D i , j , k = min ⁡ ( D i , j , k − 1 , D i , k , k − 1 + D k , j , k − 1 ) D_{i, j, k}=\min \left(D_{i, j, k-1}, D_{i, k, k-1}+D_{k, j, k-1}\right) Di,j,k=min(Di,j,k1,Di,k,k1+Dk,j,k1)

即Floyd算法本质是三维动态规划,D[i][j][k]表示从点i到点j只经过0到k个点最短路径,然后建立 起转移方程,然后通过空间优化,优化掉最后一维度,变成一个最短路径的迭代算法,最后即得 到所以点的最短路。

代码:

void FloydWarshall(std::vector<std::vector<weight_type>> &vv_dist, std::vector<std::vector<int>> &vv_parent_path)
{
    // 初始化
    size_t n = __vertexs.size();
    vv_dist.resize(n);
    vv_parent_path.resize(n);
    for (size_t i = 0; i < n; ++i)
    {
        vv_dist[i].resize(n, __max_weight);
        vv_parent_path[i].resize(n, -1);
    }
    // 先把直接相连的边更新一下
    for (size_t i = 0; i < n; ++i)
        for (size_t j = 0; j < n; ++j)
        {
            if (__matrix[i][j] != __max_weight)
            {
                vv_dist[i][j] = __matrix[i][j];
                vv_parent_path[i][j] = i;
            }
            if (i == j)
                vv_dist[i][j] = 0;
        }
    // 最短路径的更新 i->{其他顶点}->j
    // k作为i->j的中间点,k可能是任意一个顶点,尝试去更新i->j的路径
    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 (vv_dist[i][k] != __max_weight && vv_dist[k][j] != __max_weight && vv_dist[i][k] + vv_dist[k][j] < vv_dist[i][j])
                {
                    vv_dist[i][j] = vv_dist[i][k] + vv_dist[k][j];
                    vv_parent_path[i][j] = vv_parent_path[k][j]; // 注意,这里不是=k
                    // 应该要找跟j相连的上一个邻接顶点,但是k不一定是邻接顶点哦!
                    // 所以要找 vv_parent_path[k][j]
                    /*
                        如果k->j直接相连,上一个点就是k,vv_parent_path[k][j]就是k
                        如果k->j不直接相连,k->...->x->j,vv_parent_path[k][j]是x
                    */
                }
}

本部分原理讲解及图文大量参考了《算法导论》和《殷人昆 数据结构:用面向对象方法与C++语言描述 (第二版)》的内容。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/444180.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据治理实践——YY 直播业务指标治理实践

目录 一、问题背景 1.1 问题场景 1.2 问题小结 二、治理方案 2.1 治理目标 2.2 团队协同&#xff0c;共建规范 2.3 指标管理的定位 2.4 指标管理的目标及思路 2.5 指标管理&#xff0c;规范内容落地 2.6 数仓设计-关联指标维度 2.7 数据报表开发-配置口径说明 2.8 …

MongoDB在Linux环境下的安装与配置

目录 1. 准备工作 2. 安装MongoDB 2.1 传输MongoDB安装包 2.2 解压安装包 2.3 创建MongoDB安装目录 2.4 创建数据目录和日志目录 3. 启动MongoDB服务 3.1 启动MongoDB 3.2 连接MongoDB 3.3 退出MongoDB 1. 准备工作 在安装MongoDB之前&#xff0c;请确保您已具备以下…

Solidity Uniswap V2 价格预言机

预言机是连接区块链与链下服务的桥梁&#xff0c;这样就可以从智能合约中查询现实世界的数据。Chainlink 是最大的oracle网络之一&#xff0c;创建于 2017 年&#xff0c;如今已成为许多 DeFi 应用的重要组成部分。https://github.com/XuHugo/solidityproject Uniswap 虽然是链…

【leetcode热题】寻找旋转排序数组中的最小值 II

难度&#xff1a; 困难通过率&#xff1a; 38.7%题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 题目描述 假设按照升序排序的数组在预先未知的某个点上进行了旋转。 ( 例如&#xff0c;数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。 请找出其中最小的…

【react框架】跟我一起速读Next.js官方入门教学课程文档

文章目录 前言目录结构样式方案正常引入样式文件Tailwind方案CSS Modules方案clsx方案 文字和图片优化文字图片 Pages和Layout的机制PagesLayout 通过Link组件改变路由并且拆分打包提供Hooks通过Vercel创建数据未完待续... 前言 对于那些对Next.js一无所知的前端伙伴来说&…

鸿蒙开发(二)-项目结构

鸿蒙开发(二)-项目结构 上篇文章我们讲了如何配置鸿蒙开发的基础环境&#xff0c;以及创建了第一个鸿蒙程序。 这篇我们讲述了鸿蒙应用的项目目录结构。 如图所示&#xff1a;我们切换项目project可以看到。 另一种则是Ohos模式: AppScope->app.json5 应用的全局配置 {&q…

华为新发布磁电存储“王炸”,到底是什么?

最近&#xff0c;在巴塞罗那举行的2024年世界移动通信大会&#xff08;MWC24&#xff09;上&#xff0c;华为数据存储产品线总裁周彼得博士介绍了这款即将面世的产品。他向听众表示&#xff0c;与磁带存储相比&#xff0c;该设备可以降低20%的总连接成本&#xff0c;而与硬盘相…

DHCP中继实验(华为)

思科设备参考&#xff1a; 一&#xff0c;技术简介 DHCP中继&#xff0c;可以实现在不同子网和物理网段之间处理和转发DHCP信息的功能。如果DHCP客户机与DHCP服务器在同一个物理网段&#xff0c;则客户机可以正确地获得动态分配的IP地址。如果不在同一个物理网段&#xff0c;…

Web渗透测试流程

什么是渗透测试 渗透测试 (penetration test),是通过模拟恶意黑客的攻击方法&#xff0c;来评估计算机网络系统安全的一种评估方法。这个过程包括对系统的任何弱点、技术缺陷或漏洞的主动分析&#xff0c;这个分析是从一个攻击者可能存在的位置来进行的&#xff0c;并且从这个…

机器学习第29周周报 Beyond Dropout

文章目录 week29 Beyond Dropout摘要Abstract一、泛化理论二、文献阅读1. 题目2. abstract3. 网络架构3.1 特征图失真3.2 失真优化 4. 文献解读4.1 Introduction4.2 创新点4.3 实验过程4.3.1 全连接层实验4.3.2 卷积网络上的实验 4.4 结论 小结参考文献 week29 Beyond Dropout …

国产硅片膜厚检测仪

硅片膜厚检测仪是半导体行业中一种至关重要的设备&#xff0c;用于精确测量硅片上薄膜的厚度。在半导体制造工艺中&#xff0c;薄膜厚度的控制对于保证器件性能和可靠性具有决定性的作用。因此&#xff0c;硅片膜厚检测仪的研发和应用对于推动半导体技术的发展具有重要意义。 一…

SSM框架,MyBatis-Plus的学习(下)

条件构造器 使用MyBatis-Plus的条件构造器&#xff0c;可以构建灵活高效的查询条件&#xff0c;可以通过链式调用来组合多个条件。 条件构造器的继承结构 Wrapper &#xff1a; 条件构造抽象类&#xff0c;最顶端父类 AbstractWrapper &#xff1a; 用于查询条件封装&#xf…

苍穹外卖-day01

苍穹外卖-day01 目录 苍穹外卖-day01课程内容1. 软件开发整体介绍1.1 软件开发流程1.2 角色分工1.3 软件环境 2. 苍穹外卖项目介绍2.1 项目介绍2.2 产品原型2.3 技术选型 3. 开发环境搭建3.1 前端环境搭建3.2 后端环境搭建3.2.1 熟悉项目结构3.2.2 Git版本控制3.2.3 数据库环境…

一篇搞懂什么是LRU缓存|一篇搞懂LRU缓存的实现|LRUCache详解和实现

LRUCache 文章目录 LRUCache前言项目代码仓库什么时候会用到缓存(Cache)缓存满了&#xff0c;怎么办&#xff1f;什么是LRUCacheLRUCache的实现LRUCache对应的OJ题实现LRUCache对应的STL风格实现 前言 这里分享我的一些博客专栏&#xff0c;都是干货满满的。 手撕数据结构专栏…

账号管理支持批量测试资产可连接性,资产管理支持通过标签方式选择资产,JumpServer堡垒机v3.10.4 LTS版本发布

2024年3月4日&#xff0c;JumpServer开源堡垒机正式发布v3.10.4 LTS版本。JumpServer开源项目组将对v3.10 LTS版本提供长期的支持和优化&#xff0c;并定期迭代发布小版本。欢迎广大社区用户升级至v3.10 LTS最新版本&#xff0c;以获得更佳的使用体验。 在v3.10.4 LTS版本中&a…

Chrome中如何导出和导入书签

导出书签 如下图所示&#xff1a; 右上角三点->书签和清单->书签管理器->右上角三点->导出书签 然后你选择保存地址即可。打开后如下&#xff1a; 导入书签 如下图所示&#xff1a; 右上角三点->书签和清单->导入书签和设置->选择以前导出的书签&…

基于Vue移动端电影票务服务APP设计与实现

目 录 摘 要 I Abstract II 引 言 1 1 相关技术 3 1.1 Vue框架 3 1.2 数据库MongoDB 3 1.3 Axios请求 3 1.4 H5、CSS3和JavaScript 4 1.5 本章小结 4 2 系统分析 5 2.1 功能需求 5 2.2 用例分析 5 2.3 用户功能 6 2.4本章小结 6 3 基于Vue电影票务服务APP设计 7 3.1 页面设计 …

DFS和BFS以及练习题目(未完待续)

DFS和BFS 温馨提示&#xff1a;学习dfs之前最好先了解一下递归的思想。 递归思想 斐波那契 题目分析 题目代码 import java.util.Scanner; public class Main{static long dp[]; public static void main(String[] args) {Scanner scanner new Scanner(System.in);int t…

手机号验证码重新发送

前文叙述 很久以前做的一个 demo &#xff0c;纯 HTML 、CSS、js 制作&#xff0c;一定时间段之后才可以重新发送验证码&#xff0c;如 60s 后再次发送验证码&#xff0c;在该时间段内发送验证码按钮为禁用状态&#xff0c;实战开发过程也亦是同理&#xff0c;因此记录一手。 一…

供应商评价与选择改进研究——21年数学建模国赛C题分析

题目描述 问题一分析&#xff08;基于APH、PCA和TOPSIS的供应商评价与选择&#xff09; 问题一需要我们对附件一中的402家供应商的数据进行处理并量化分析&#xff0c;并构建数学模型选择当中最重要的50家供应商。 附件一&#xff1a; 部分订货量 部分供货量 注意&#xff…