【C】单源查找最短路径(负权边+单向路)

版权声明:Copyright © 2019年 Lcost. All rights reserved. https://blog.csdn.net/m0_43448982/article/details/85260518

本文仅用于给小白解释贝尔曼-福特算法,大部分的优化都会删去,复杂度较高(为O(NM)),最后会给出一些优化建议但不给出具体优化后的代码,各位大佬可以忽略本文,如果有大佬对本文有修改意见,欢迎评论

先看一道OJ题

时间旅行者

题目描述

爱因斯坦的相对论理论使得时间旅行成为可能。
Sake想做一次时间旅行,从城市1出发,希望到达城市n的时刻是在出发之前。
n个城市之间有m条单向路。通过这条路我们可能花费正时间到达未来,也可能花费负时间到达过去。我们保证从城市1出发肯定能到达城市n。

输入

输入有多组测试数据。

第一行包含两个整数n,m,(2<=n<=1000,n-1<=m<=20000)。

接下来m行,每行有3个整数a,b,t,表示一条从城市a到城市b的一条路,需要花费t时间,-1000<=t<=1000。

输出

如果有一种旅行方案能在过去时刻到达城市n,请输出“YES”。

否则输出“NO”。

样例输入

3 2
1 2 4
2 3 -5

样例输出

YES

解题思路

分析

这道题是一道很典型的“负权边”(从一个点到另一个点花费的时间可以是负值)+“单向路”(每条路仅限单向通过)+“单源”(起点唯一且确定)的查找最短路径的问题(此题中的最短路径应为最早到达的时间)我们采用贝尔曼-福特算法(Bellman-Ford)来解决。

贝尔曼-福特算法:用于解决“单源”的查找最短路径的方法,支持“负权边”。

简要概括一下步骤:

  1. 保存所有的路径
  2. 为所有路点创建一个权值,并赋值为∞
  3. 将起点的权值改为0
  4. 遍历所有的路径
  5. 如果当前路的起点的权值+当前路的权值<当前路终点的权值,则将当前路终点的权值赋值为当前路起点的权值+当前路的权值
  6. 重复四、五两步直到没有路的权值发生改变
  7. 终点的权值即为最短路径的长度

好吧我承认写成这样完全不适合刚刚入门的人,那我们附上图片解释一下

图文解释

默认起点为A,终点为G
路径列表为(AB5代表此路为从A到B权值为5):
AB5
AC8
BE12
CD4
CF6
EG9
DG7
FG15

1.保存所有的路径

数值都写在路径上方
 1.保存所有的路径

2.为所有路点创建一个权值,并赋值为∞

图中每个点上面的紫色数字即为它们对应的权值
 2.为所有路点创建一个权值,并赋值为∞

3.将起点的权值改为0

 3.将起点的权值改为0

4.遍历所有的路径

5.如果当前路的起点的权值+当前路的权值<当前路终点的权值,则将当前路终点的权值赋值为当前路起点的权值+当前路的权值

6.重复四、五两步直到没有路的权值发生改变

(此处用组图来表现第一次遍历的过程)
12
下面就不一一展示每一条路径的判断,直接跳至第一次所有的路径都遍历完后的结果
3
在遍历完所有的路径之后,我们需要再一次遍历所有路径,发现没有任何点的权值发生了改变,则这三步结束(如果有点的权值发生改变,则需要再一次遍历所有路径)

7.终点的权值即为最短路径的长度

得出最短路径长度为19

回到这道OJ上来

我们已经掌握了可以解决这道题的理论方法,现在需要的就是把理论的东西转换成计算机可以理解的代码

我们来罗列一下我们需要解决的问题

  1. 如何保存路径
  2. 如何记录每个点的权值
  3. 多少才算是∞,100?1000?还是231-1?
  4. 如何判断已经没有点的权值发生改变了

我们一一给出解决方案

  1. 可以建立一个结构体,含有三个int类型的值,分别为起点,终点,路的权值
struct road
{
    int form;
    int to;
    int power;
};
  1. 可以建立一个int类型的数组f,来保存对应点的权值
int f[1005];
  1. 无穷大才是我们最需要解决的,多少算无穷大?我不知道,但是我们可以这样理解,无论∞为多少,它一定小于两个非∞的值相加。那么我们可以为每一个点定义一个标记,如果为false,则这个点不是∞,那么取他的权值,如果为true,则这个点为∞,那么可以直接跳过部分操作,这里我用结构体的方式来解决这个问题,那么第二步的数组也就没有必要了
struct node
{
    int value;
    int isInfinity;
};
  1. 解决这个问题非常简单,可以在循环外定义一个标记,每次进入循环前判断是否为true,如果为true则进入,并改为false,每当有点的权值发生改变时,改成true。
int flag=1;
while (flag)
{
	flag=0;
	//这里来给点赋权值
}

至此,我们已经基本完成了这个问题,只需要我们去把这些分析过程改成代码即可,这里就直接给出代码了。

#include <stdio.h>

struct road
{
    int form;
    int to;
    int power;
}roadlist[20010];

struct node
{
    int value;
    int isInfinity;
}nodelist[1005];

int main()
{
    int n,m,i;
    while (scanf("%d%d",&n,&m)!=EOF)
    {
    	//输入保存路径
        for (i=0; i<m; i++)
        {
            scanf("%d%d%d",&roadlist[i].form,&roadlist[i].to,&roadlist[i].power);
        }
        for (i=2; i<=n; i++)
        {
            nodelist[i].isInfinity=1;
        }
        //初始化起始点
        nodelist[1].value=0;
        nodelist[1].isInfinity=0;
        //遍历部分
        int flag=1;
        while (flag)
        {
            flag=0;
            for (i=0; i<m; i++)
            {
                if (nodelist[roadlist[i].form].isInfinity)//如果连起点都是∞,那就没有继续下去的意义了
                {
                    continue;
                }
                else if (nodelist[roadlist[i].to].isInfinity)//如果终点是∞
                {
                    nodelist[roadlist[i].to].value=nodelist[roadlist[i].form].value+roadlist[i].power;//由于∞必定大于非∞的数,所以直接赋值
                    nodelist[roadlist[i].to].isInfinity=0;//别忘记标记为非∞
                    flag=1;//有点的权值发生改变了
                }
                else
                {
                    if (nodelist[roadlist[i].to].value>nodelist[roadlist[i].form].value+roadlist[i].power)//如果走这条路到达时间更早,则赋值并标记flag
                    {
                        nodelist[roadlist[i].to].value=nodelist[roadlist[i].form].value+roadlist[i].power;
                        flag=1;
                    }
                }
            }
        }
        if (!nodelist[n].isInfinity && nodelist[n].value<0)
        {
            printf("YES\n");
        }
        else
        {
            printf("NO\n");
        }
    }
    return 0;
}

接下来,把代码提交到OJ上去,编译、运行并评判,然后TLE(时间超限)……
那么问题来了,哪里超时了?
对,这个代码没有任何优化,每一次都要把所有的路都遍历过去,时间复杂度非常高,这里有很大的隐患。
然而并不全是时间复杂度太高
我尝试改良,减少了很多的时间,但OJ仍然给出了TLE。
那么说明了问题并不在遍历次数太多,而在于另外一个我们没有关注到的细节——“负权边”
我们可以假设有这样一组数据

2 2
1 2 3
2 1 -4

这组数据有个很关键的点,这条路形成了一个环(1->2->1)
而且,每一次这样转一圈,会使得1的权值减少1,所以每一次遍历都会有点的权值发生改变,虽然在某个时刻开始,2的权值已经小于0了,但是由于有权值发生改变,所以永远不会退出循环。
一开始我认为解决方法很简单,就是去判断一下每次遍历结束之后是否会出现终点的权值为负值且非∞,然后就AC了。代码如下

#include <stdio.h>

struct road
{
    int form;
    int to;
    int power;
}roadlist[20010];
 
struct node
{
    int value;
    int isInfinity;
}nodelist[1005];
 
int main()
{
    int n,m,i;
    while (scanf("%d%d",&n,&m)!=EOF)
    {
        for (i=0; i<m; i++)
        {
            scanf("%d%d%d",&roadlist[i].form,&roadlist[i].to,&roadlist[i].power);
        }
        for (i=2; i<=n; i++)
        {
            nodelist[i].isInfinity=1;
        }
         
        nodelist[1].value=0;
        nodelist[1].isInfinity=0;
         
        int flag=1;
        while (flag)
        {
            flag=0;
            for (i=0; i<m; i++)
            {
                if (nodelist[roadlist[i].form].isInfinity)
                {
                    continue;
                }
                else if (nodelist[roadlist[i].to].isInfinity)
                {
                    nodelist[roadlist[i].to].value=nodelist[roadlist[i].form].value+roadlist[i].power;
                    nodelist[roadlist[i].to].isInfinity=0;
                    flag=1;
                }
                else
                {
                    if (nodelist[roadlist[i].to].value>nodelist[roadlist[i].form].value+roadlist[i].power)
                    {
                        nodelist[roadlist[i].to].value=nodelist[roadlist[i].form].value+roadlist[i].power;
                        flag=1;
                    }
                }
            }
            if (!nodelist[n].isInfinity && nodelist[n].value<0)//避免有负环的时候发生死循环
            {
                break;
            }
        }
        if (!nodelist[n].isInfinity && nodelist[n].value<0)
        {
            printf("YES\n");
        }
        else
        {
            printf("NO\n");
        }
    }
    return 0;
}

本来想想已经AC了,没有想下去的欲望的,但是在写博客前,想到了一点。庆幸的是后台数据没有注意到这个细节,让我偷偷通过了评判。这里给出一组特例

//无法到达目标
3 2
1 2 3
2 1 -4

这里索性都没有前往3点的路了,但是,这个数据违规了,题目有说一定能到3点(终点),那我再给出一组数据

//负环完全不会影响目标点的权值
4 4
1 2 3
2 3 1
3 2 -2
1 4 5

这样可以到点4了吧,但是也是死循环。
这两种情况OJ都没有涉及到可见OJ后台数据……
但是我们是来追求正确的解答的,所以这里给出补充部分来解决这个问题。
因为有n个点,所以最多只需要遍历n-1次,因为在一个含n个顶点的图中,任意两点之间的最短路径最多含有n-1条边
那么我们可以在这里做文章,如果遍历了超过n-1次,且目标点还是∞,那么肯定不能到达。如果不是∞,那就记录下当前的权值,然后与下一次比较,如果权值减少/改变,则说明了负环的影响范围包含了目标点,反之,就不包含。
这里直接给出最终代码

#include <stdio.h>

struct road
{
    int form;
    int to;
    int power;
}roadlist[20010];
 
struct node
{
    int value;
    int isInfinity;
}nodelist[1005];
 
int main()
{
    int n,m,i;
    while (scanf("%d%d",&n,&m)!=EOF)
    {
        for (i=0; i<m; i++)
        {
            scanf("%d%d%d",&roadlist[i].form,&roadlist[i].to,&roadlist[i].power);
        }
        for (i=2; i<=n; i++)
        {
            nodelist[i].isInfinity=1;
        }
         
        nodelist[1].value=0;
        nodelist[1].isInfinity=0;
         
        int flag = 1,run_time = 0,lasttargetvalue = 0;
        while (flag)
        {
            flag=0;
            run_time++;
            for (i=0; i<m; i++)
            {
                if (nodelist[roadlist[i].form].isInfinity)
                {
                    continue;
                }
                else if (nodelist[roadlist[i].to].isInfinity)
                {
                    nodelist[roadlist[i].to].value=nodelist[roadlist[i].form].value+roadlist[i].power;
                    nodelist[roadlist[i].to].isInfinity=0;
                    flag=1;
                }
                else
                {
                    if (nodelist[roadlist[i].to].value>nodelist[roadlist[i].form].value+roadlist[i].power)
                    {
                        nodelist[roadlist[i].to].value=nodelist[roadlist[i].form].value+roadlist[i].power;
                        flag=1;
                    }
                }
            }
            if (!nodelist[n].isInfinity && nodelist[n].value<0)
            {
                break;
            }
            else if (nodelist[n].isInfinity && run_time>=n)//如果在经历超过n-1次之后,目标点仍为∞
            {
                break;
            }
            else if (!nodelist[n].isInfinity && run_time==n-1)//在经历n-1次后,目标点有权值
            {
                lasttargetvalue=nodelist[n].value;
            }
            else if (!nodelist[n].isInfinity && run_time>=n)//在经历n次后,目标点有权值,则上一个判断必定保存了上一次的权值
            {
                if (lasttargetvalue>nodelist[n].value)//判断
                {
                    nodelist[n].value=-1;//直接赋值为-1,那不就一定能到了吗
                    break;
                }
                else
                {
                    break;
                }
            }
        }
        if (!nodelist[n].isInfinity && nodelist[n].value<0)
        {
            printf("YES\n");
        }
        else
        {
            printf("NO\n");
        }
    }
    return 0;
}

至此,这道题的分析结束。

当然有能力的大佬可以选择尝试用队列的方式来减少复杂度
这里就不再给出优化后的代码了

猜你喜欢

转载自blog.csdn.net/m0_43448982/article/details/85260518