最短路径

1 单源最短路径--Dijkstra算法

单源最短路径问题:给定一个图G = ( V , E ) G=(V,E)G=(V,E),求源结点s ∈ V s∈Vs∈V到图中每个结点v ∈ V v∈Vv∈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 V& src, std::vector<W>& dist, std::vector<int>& pPath)
    {
        size_t srci = GetVertexIndex(src);
        size_t n = _vertexs.size();
        dist.resize(n, MAX_W);// src节点 到指定节点的最小距离
        pPath.resize(n, -1); // 记录节点的父亲
        
        // 初始化根节点
        dist[srci] = 0;
        pPath[srci] = srci;
        
        // 记录被选中节点的集合
        std::vector<bool> S(n, false);
        
        for(size_t j = 0; j < n; ++j)
        {
            // 选最短路径节点,且不在S中
            size_t u = 0;// 记录选中节点下标
            W min = MAX_W;
            for(size_t i = 0; i < n; ++i)
            {
                if(S[i] == false && dist[i] < min)
                {
                    u = i;
                    min = dist[i];
                }
            }
            S[u] = true;
            
            // 松弛更新u连接顶点v  srci->u + u->v <  srci->v  更新
            for(size_t v = 0; v < n; ++v)
            {
                if(S[v] == false && _matrix[u][v] != MAX_W
                   && dist[u] + _matrix[u][v] < dist[v])
                {
                    dist[v] = dist[u] + _matrix[u][v];
                    pPath[v] = u;
                }
                    
            }
        }

    }
    
    void PrintShortPath(const V& src, const std::vector<W>& dist, const std::vector<int>& pPath)
    {
        size_t srci = GetVertexIndex(src);
        size_t n = _vertexs.size();
        for(size_t i = 0; i < n; ++i)
        {
            if(i != srci)
            {
                // 找出i顶点的路径  s->i 路径
                std::vector<int> path;
                size_t parenti = i;
                while (parenti != srci)
                {
                    path.push_back(parenti);
                    parenti = pPath[parenti];
                }
                path.push_back(srci);
                std::reverse(path.begin(), path.end());
                
                for(auto index : path)
                    std::cout << _vertexs[index] << "->";
                std::cout << dist[i] << std::endl;
            }
        }
        
    }

    void TestGraphDijkstra()
    {
        const char* str = "syztx";
        Graph<char, int, INT_MAX, true> 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<int> dist;
        std::vector<int> parentPath;
        g.Dijkstra('s', dist, parentPath);
        g.PrintShortPath('s', dist, parentPath);

        // 图中带有负权路径时,贪心策略则失效了。
        // 测试结果可以看到s->t->y之间的最短路径没更新出来
//        const char* str = "sytx";
//        Graph<char, int, INT_MAX, true> g(str, strlen(str));
//        g.AddEdge('s', 't', 10);
//        g.AddEdge('s', 'y', 5);
//        g.AddEdge('t', 'y', -7);
//        g.AddEdge('y', 'x', 3);
//        std::vector<int> dist;
//        std::vector<int> parentPath;
//        g.Dijkstra('s', dist, parentPath);
//        g.PrintShortPath('s', dist, parentPath);
    }

 

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

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

Bellman - Ford 算法是一种用于解决带权有向图中单个源点到其他所有顶点的最短路径问题的算法。它由美国数学家理查德・贝尔曼(Richard Bellman)和莱斯特・福特(Lester Ford)发明。与 Dijkstra 算法不同的是,Bellman - Ford 算法能够处理包含负权边的图,只要图中不存在负权回路(从一个顶点出发,沿着边的方向经过若干条边后又回到这个顶点,且路径上的边权之和为负)。该算法在网络路由、经济系统建模等领域有广泛的应用

 

    bool BellmanFord(const V& src, std::vector<W>& dist, std::vector<int>& pPath)
    {
        size_t n = _vertexs.size();
        size_t srci = GetVertexIndex(src);
        
        // vector<W> dist,记录srci-其他顶点最短路径权值数组
        dist.resize(n, MAX_W);
        
        // vector<int> pPath 记录srci-其他顶点最短路径父顶点数组
        pPath.resize(n, -1);
        
        // 先更新srci->srci为缺省值
        dist[srci] = W();
        
        //std::cout << "更新边:i->j" << std::endl;
        
        for(size_t k = 0; k < n; ++k)
        {
            // i->j 更新松弛
            bool update = false;// 像冒泡排序一样 增加一个标志位
            std::cout << "更新第:" << k << "轮" << std::endl;
            for(size_t i = 0; i < n; ++i)
                for(size_t j = 0; j < n; ++j)
                {
                    // srci -> i + i -> j
                    if(_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
                    {
                        update = true;
                        std::cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << std::endl;
                        dist[j] = dist[i] + _matrix[i][j];
                        pPath[j] = i;
                    }
                }
            
            // 如果这个轮次中没有更新出更短路径,那么后续轮次就不需要再走了
            if(update == false)
                break;
        }
        
        // 还能更新就是带负权回路 这种情况 神仙也救不了
        for(size_t i = 0; i < n; ++i)
            for(size_t j = 0; j < n; ++j)
                if(_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
                    return false;
        
        return true;
    }
    void TestGraphBellmanFord()
    {
        const char* str = "syztx";
        Graph<char, int, INT_MAX, true> 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<int> dist;
        std::vector<int> parentPath;
        g.BellmanFord('s', dist, parentPath);
        std::cout << std::endl;
        g.PrintShortPath('s', dist, parentPath);

//        const char* str = "syztx";
//        Graph<char, int, INT_MAX, true> 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('y', 's', 1); // 新增
//        g.AddEdge('z', 's', 2);
//        g.AddEdge('z', 'x', 7);
//        g.AddEdge('t', 'x', 5);
//        //g.AddEdge('t', 'y', -8); //更改
//        g.AddEdge('t', 'y', 8);
//
//        g.AddEdge('t', 'z', -4);
//        g.AddEdge('x', 't', -2);
//        vector<int> dist;
//        vector<int> parentPath;
//        if (g.BellmanFord('s', dist, parentPath))
//            g.PrintShortPath('s', dist, parentPath);
//        else
//            cout << "带负权回路" << endl;
    }

 输出结果:

代码疑难杂症

 确保最短路径发生变化,而已经计算过的路径用的是旧路径

第一次更新s->t 最短路径为6

第二次更新s->t 最短路径为2

在第一次更新与第二次更新之间,利用了第一次的s->t 的6更新了s -> t -> z 为2

但更新的顺序为 syztx ,有了x->t 才发现s->t 的最短路径为2,因此s->t(6)->z(2),应该及时更改为s->t(2)->z(-2)。由于这种新更新路径可能会影响其他路径,因此需要重新加一层for循环

设置标志位,是一种优化,已经更新到最短路径就不需要继续更新了

还有一种优化,自己找资料吧

 

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

Floyd-Warshall算法是解决任意两点间的最短路径的一种算法。(任意两点之间的最短路径问题,那么dist和pPath就需要二维数组)

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

 

void FloydWarshall(std::vector<std::vector<W>>& vvDist, std::vector<std::vector<int>>& vvpPath)
    {
        size_t n = _vertexs.size();
        vvDist.resize(n);
        vvpPath.resize(n);
        
        // 初始化权值和路径矩阵
        for(size_t i = 0; i < n; ++i)
        {
            vvDist[i].resize(n, MAX_W);
            vvpPath[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_W)
                {
                    vvDist[i][j] = _matrix[i][j];
                    vvpPath[i][j] = i;
                }
                if(i == j)
                    vvDist[i][j] = W();
            }
        
        // 最短路径更新 i-> {其他顶点} ->j
        for(size_t k = 0; k < n; ++k)// 最外层循环遍历中间节点 k,表示将 k 作为中间节点尝试更新最短路径。
            for(size_t i = 0; i < n; ++i)
                for(size_t j = 0; j < n; ++j)
                {
                    // 如果i->k k->j存在 k 作为中间点尝试去更新 i->j的路径
                    if(vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W &&
                       vvDist[i][k] + vvDist[k][j] < vvDist[i][j])
                    {
                        vvDist[i][j] = vvDist[i][k] + vvDist[k][j];
                        
                        // 找跟j相连的上一个邻接顶点
                        // 如果k->j 直接相连,上一个点就k,vvpPath[k][j]存就是k
                        // 如果k->j 没有直接相连,k->...->x->j,vvpPath[k][j]存就是x
                        vvpPath[i][j] = vvpPath[k][j];
                    }
                }
        
        // 打印权值和路径矩阵观察数据
        for (size_t i = 0; i < n; ++i)
        {
            for (size_t j = 0; j < n; ++j)
            {
                if (vvDist[i][j] == MAX_W)
                {
                    //std::cout << "*" << " ";
                    printf("%3c", '*');
                }
                else
                {
                    //std::cout << vvDist[i][j] << " ";
                    printf("%3d", vvDist[i][j]);
                }
            }
            std::cout << std::endl;
        }
        std::cout << std::endl;

        for (size_t i = 0; i < n; ++i)
        {
            for (size_t j = 0; j < n; ++j)
            {
                //std::cout << vvParentPath[i][j] << " ";
                printf("%3d", vvpPath[i][j]);
            }
            std::cout << std::endl;
        }
        std::cout << "=================================" << std::endl;
    }

    void TestFloydWarShall()
    {
        const char* str = "12345";
        Graph<char, int, INT_MAX, true> 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<std::vector<int>> vvDist;
        std::vector<std::vector<int>> vvParentPath;
        g.FloydWarshall(vvDist, vvParentPath);

        // 打印任意两点之间的最短路径
        for (size_t i = 0; i < strlen(str); ++i)
        {
            g.PrintShortPath(str[i], vvDist[i], vvParentPath[i]);
            std::cout << std::endl;
        }
    }

 

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐