理论: 图论(6): 单源赋值图最短路径

总括

单源最短路径问题在前几篇已经提到过,在这里不再赘述。

有额外的一点, 终点和起点都固定的问题称之为:两点之间最短路径问题。

但是因为解决单源最短路径问题的时间复杂度也是一样的, 因此通常当作单源最短路径问题处理(在求单源最短路径的同时求出的实际是单点到全图说有点的最短路径)

下面我将讲解最短路径的三种常见算法: Bellman-ford算法 Dijkstra算法 Floyed算法、、

Bellman-ford算法

我们记从顶点S出发到顶点J的最短路径为d[J], 则下面的等式成立:

d[j] = min{d[i] + each verter from i to j};

如果给定的图是一个DAG(无环有向图), 就可以按拓扑排序的方法给顶点编号, 并利用这个条递推关系式计算出d(这种情况的详细优化方法我将在图论7中详细分析)。

但是如果图中有圈, 就无法依赖这种顺序计算。在这种情况下, 记当前的顶点i的最短路径的长度为d[i], 色初始值d[0] = 0; d[i] = INF (INF为足够大 如0xfffffff),再不断使用这种递推关系更新d的值, 就可以计算出新的d。 只要图中不存在负圈, 这样的操作就是有限的。

最终结束时d数组中的值就是点s到相应顶点的最短距离

#include

using namespace std;

const int INF = 0xfffffff;

struct edge{
    int from;
    int cost;
    int to;
};
edge es[1000];
int d[1000];
int V, E;

void shorttest_path(int s)
{
    for (int i = 0; i < V; i++)
    {
        d[i] = INF;
    }
    d[s] = 0;
    while (true)
    {
        bool change = false;
        for (int i = 0; i < E; i++)
        {
            edge e = es[i];
            if (d[e.from] != INF && d[e.to] > d[e.from] + e.cost)
            {
                d[e.to] = d[e.from] + e.cost;
                change = true;
            }
        }
        if (!change)
            break;
    }
}

·
如果在图中不存在从S可达的负圈, 那么最短路径不会经过同一个顶点2次, 也就是说最多通过 V - 1条边,while循环最多执行V - 1次, 因此时间复杂度是0(V * E)。

反之如果存在可以从S到达的负圈, 那么在地V次循环的时候依旧会更新d的值, 因此可以通过这个性质来检查负圈。 简单的说就是检测上述代码中while循环的执行次数 如果在第V次这个循环依旧被执行这个图就是有负环的;

下面贴一个基于Ford算法修改成的判断是否存在负环的代码

bool find_negative_loop(int s)
{
    for (int i = 0; i < V; i++)
    {
        d[i] = INF;
    }
    d[s] = 0;
    for (int j = 0; j < V; j++)
    {
        for (int i = 0; i < E; i++)
        {
            edge e = es[i];
            if (d[e.from] != INF && d[e.to] > d[e.from] + e.cost)
            {
                d[e.to] = d[e.from] + e.cost;
                if (j == V - 1)
                    return true;//在第V - 1次依旧更新了d的数值   说明存在负环
            }
        }
    }
    return false;
}

Djikstra算法

鉴于它的重要地位, 我将在下一篇博客中详细证明。

让我们单独考虑一下没有负边的情况。 在Ford算法中, 如果d[i]还不是最短距离的话那么即使进行:
d[j] = min{d[i] + each verter from i to j};的更新, 那么所得到的d[j]也不是最短距离。

而且, 即使d[i]没有发生变化, 每次循环的时候也要重新检查一遍从i出发的所有边。 这显然拜拜的浪费了大量的时间, 为此我们可以对算法尽享如下的更改:

step1. 找到最短距离已经确定的顶点, 从它出发更新杏林顶点的最短距离。
step2. 此后不再关心step1中的最短距离已经确定的顶点。

现在问题转化成了如何求解step1 中的最短距离已经确定的点。 在开始的时候, 只有起点的最短距离是确定的, 而在尚未使用过的顶点中, 距离d[i]距离最小的顶点就是最短距离已经确定的顶点。 这是因为由于不存在负边, 所以d[i]不会再之后的更新中变得更小。 上述算法叫做Dijkstra算法

int cost[1000][1000];//cost数组表示的是边的权值 INF表示不存在这条边
int d[1000];//finall aim
bool used[1000];
int V;

void dijkstra(int s)
{
    memset(d, INF, sizeof(d));
    memset(used, 0, sizeof(used));
    d[s] = 0;

    while (true)
    {
        int v = -1;
        /*下面是选择最大值*/
        for (int u = 0; u < V; u++)
        {
            if (!used[u] && (v == -1 || d[u] < d[v]))
                v = u;
        }
        /*如果所有的值都被used 跳出循环*/
        if (v == -1)
            break;
        /*在选取到的最小值的基础上更新与之相连的顶点*/
        for (int u = 0; u < V; u++)
        {
            if (d[u] > d[v] + cost[v][u])
                d[u] = d[v] + cost[v][u];
        }
    }
}

分析一下这个算法的时间复杂度:

在使用邻接表的情况下, 每次每条边只需要访问一次即可, 这部分的时间复杂度是0(E)。

但是每次寻找最短距离已经确定的顶点的过程中, 需要遍历全部的顶点, 所以最终的时间复杂度还是O(V2)

这里需要我们需要注意的是, 在稀疏图中, 大部分的时间都浪费在了查找下一个以此顶点为起点的边上, 所以我们可以使用邻接链表来减少去其遍历的过程

下面我们使用邻接链表(不定长数组模拟)和STL中的优先队列来实现对Dijstra的优化。在每次更新的时候,往堆里面插入当前最短距离和顶点的值对。插入的次数时O(E)次, 因此元素也是O(E)个。 当取出的最小值不是最短距离的话就丢弃这个值。 这样整个算法也就可以在同样打复杂度内完成。

int Dijktra(int S, int T)
{
    minDist[S] = 0;
    priority_queueQ;//新建优先队列 用边的长度从小到大排序
    for (int i = 0; i < G[S].size(); i++)
    {//初始化起始点 将全部与起始点之间相连的点更新 压入优先队列
        int vex = G[S][i].v;
        Q.push(G[S][i]);
        minDist[vex] = min(G[S][i].len, minDist[vex]);
        inqueue[vex] = 1;
    }
    while (!Q.empty())
    {//依次取出更新 直至队列为空
        node now = Q.top();
        Q.pop();
        inqueue[now.v] = 0;
        for (int i = 0; i < G[now.v].size(); i++)
        {//更新与队列首元素直接相连的顶点的信息
            int vex = G[now.v][i].v;
            int len = G[now.v][i].len;
            if (len + minDist[now.v] < minDist[vex])
            {
                minDist[vex] = len + minDist[now.v]; //先更新最短路径
                if (!inqueue[vex])
                { //如果没在队列,再加入队列
                    inqueue[vex] = 1;
                    Q.push(G[now.v][i]);
                }
            }
        }
    }
    return minDist[T];
}

因为优先队列的优化, Dijkastra的时间复杂度是O(E log V), 可以更加高效的统计最短路径的长度。 但是在存在负环的情况下, Dijkastra就无法正确求解, 还需要使用Ford算法(这个问题将在下一篇博客中解决)

Floyd算法

这个算法是最简单暴力的:它求解的是任意两点之间的最短路径问题。

让我们现用DP的思路来分析一下任意连点之间的最短路径问题。 只使用顶点0 - k和i,j 的情况下, 我们记i到j 的最短路长度为d[ k + 1 ][i][j]. k == -1时, 认为只使用 i 和 j , 所以d[0][i][j] = cost[i][j]; 接下来, 然我们将问题归结到只是用0 - k - 1的问题上。

在只使用0 - k的时候, 我们分到 i 到 j 的最短路正好经过顶点k和完全不经过顶点k两种情况来讨论。 不经过顶点k的情况下d[k][i][j] = d[k - 1][i][j]; 在通过顶点k的情况下 d[k][i][j] = d[k - 1][i][k] + d[k - 1][k][j];

综合上述两种情况d[k][i][j] = min{ d[k - 1][i][j], d[k - 1][i][k] + d[k - 1][k][j]};

这个算法叫做Floyd算法, 可以在O(V3)的时间里求得任意连通两点之间的最短长度。这个算法和Ford算法一样可以处理有负边的情况, 至于判断是否有负圈至于要检查是否存在d[i][j]是负数的顶点就行了

int d[1000][1000];//用邻接矩阵来保存图
int V;

void Floyd(void)
{
    for (int k = 0; k < V; k++)
    {
        for (int i = 0; i < V; i++)
        {
            for (int j = 0; j < V; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
        }
    }
}

你可能感兴趣的:(理论: 图论(6): 单源赋值图最短路径)