【高阶数据结构】图的应用--最短路径算法

发布于:2024-07-05 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、最短路径

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

二、单源最短路径–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算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径。

在这里插入图片描述

代码实现:

// 临接矩阵
namespace Matrix
{
    template <class V, class W, W MAX_W = INT_MAX, bool Direction = false>
    class Graph
    {
        typedef Graph<V, W, MAX_W, Direction> Self;

    private:
        std::vector<V> _vertexs;             // 顶点集合
        std::map<V, size_t> _vIndexMap;      // 顶点的下标映射关系
        std::vector<std::vector<W>> _matrix; // 存储边集合的矩阵
        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顶点的路径
                    std::vector<int> path;
                    size_t parenti = i;
                    while (parenti != srci)
                    {
                        path.push_back(parenti);
                        parenti = pPath[parenti];
                    }
                    path.push_back(srci);
                    reverse(path.begin(), path.end());

                    for (auto index : path)
                    {
                        std::cout << _vertexs[index] << "->";
                    }
                    std::cout << "权值和:" << dist[i] << std::endl;
                }
            }
        }
        // 顶点个数是N  -> 时间复杂度:O(N^2)空间复杂度:O(N)
        void Dijkstra(const V &src, std::vector<W> &dist, std::vector<int> &pPath)
        {
            size_t srci = GetVertexIndex(src);
            int n = _vertexs.size();

            dist.resize(n, MAX_W);
            pPath.resize(n, -1);

            dist[srci] = 0;
            pPath[srci] = srci;

            // 已经确定最短路径的顶点集合
            std::vector<bool> S(n, false);

            // n个节点每个节点都要作为起点
            for (int i = 0; i < n; i++)
            {
                // 选最短路径顶点且不在S更新其他路径
                int u = 0;
                W min = MAX_W;

                for (int j = 0; j < n; j++)
                {
                    if (S[i] == false && dist[i] < min)
                    {
                        u = i;
                        min = dist[i];
                    }
                }

                S[u] = true;
                // 松弛更新u连接顶点v  srci->u + u->v <  srci->v  更新
                for (int 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 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);
}

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

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

在这里插入图片描述

代码实现:

// 临接矩阵
namespace Matrix
{
    template <class V, class W, W MAX_W = INT_MAX, bool Direction = false>
    class Graph
    {
        typedef Graph<V, W, MAX_W, Direction> Self;

    private:
        std::vector<V> _vertexs;             // 顶点集合
        std::map<V, size_t> _vIndexMap;      // 顶点的下标映射关系
        std::vector<std::vector<W>> _matrix; // 存储边集合的矩阵
        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顶点的路径
                    std::vector<int> path;
                    size_t parenti = i;
                    while (parenti != srci)
                    {
                        path.push_back(parenti);
                        parenti = pPath[parenti];
                    }
                    path.push_back(srci);
                    reverse(path.begin(), path.end());

                    for (auto index : path)
                    {
                        std::cout << _vertexs[index] << "->";
                    }
                    std::cout << "权值和:" << dist[i] << std::endl;
                }
            }
        }
        // 时间复杂度:O(N^3) 空间复杂度:O(N)
        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();

            // 总体最多更新n轮
            for (int k = 0; k < n; k++)
            {
                bool update = false;
                std::cout << "更新第:" << k << "轮" << std::endl;
                for (int i = 0; i < n; i++)
                {
                    for (int 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 (int i = 0; i < n; i++)
            {
                for (int 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);
    // 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', 'x', -3);
    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', 'z', -4);
    g.AddEdge('x', 't', -2);
    std::vector<int> dist;
    std::vector<int> parentPath;
    if (g.BellmanFord('s', dist, parentPath))
        g.PrintShortPath('s', dist, parentPath);
    else
        std::cout << "带负权回路" << std::endl;
}

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

在这里插入图片描述

代码实现:

// 临接矩阵
namespace Matrix
{
    template <class V, class W, W MAX_W = INT_MAX, bool Direction = false>
    class Graph
    {
        typedef Graph<V, W, MAX_W, Direction> Self;

    private:
        std::vector<V> _vertexs;             // 顶点集合
        std::map<V, size_t> _vIndexMap;      // 顶点的下标映射关系
        std::vector<std::vector<W>> _matrix; // 存储边集合的矩阵
		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顶点的路径
                    std::vector<int> path;
                    size_t parenti = i;
                    while (parenti != srci)
                    {
                        path.push_back(parenti);
                        parenti = pPath[parenti];
                    }
                    path.push_back(srci);
                    reverse(path.begin(), path.end());

                    for (auto index : path)
                    {
                        std::cout << _vertexs[index] << "->";
                    }
                    std::cout << "权值和:" << dist[i] << std::endl;
                }
            }
        }
        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 (int i = 0; i < n; i++)
            {
                vvDist[i].resize(n, MAX_W);
                vvpPath[i].resize(n, -1);
            }

            for (int i = 0; i < n; i++)
            {
                for (int 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();
                    }
                }
            }

            // abcdef  a {} f ||  b {} c
            // 最短路径的更新i-> {其他顶点} ->j
            for (int k = 0; k < n; k++)
            {
                for (int i = 0; i < n; i++)
                {
                    for (int j = 0; j < n; j++)
                    {
                        // i -> 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 (int i = 0; i < n; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    if (vvDist[i][j] == MAX_W)
                    {
                        printf("%3c", '*');
                    }
                    else
                    {
                        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)
                {
                    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]);
    }
}