最短路径

        很久没有练习最短路径的题目了,本以为掌握的不错,却忽略了每种算法都有它的拓展,它的变式,并非所有的题目都是让你求个最短路径那么简单(即使在一般的比赛中spfa也仅仅是用来求最短路径)。刷了几天的水题,也是时候做些总结了。

        众所周知,最短路径的算法一共有四种,分别是(ASAP)Floyed,(SSSP)Dijkstra,(SSSP)BellmanFord,(SSSP)SPFA。个人认为SPFA仅仅是BellmanFord的强化版,应该归属于同一种算法。因此,以下只总结前三种算法。


Floyed算法:

      Floyed算法较为简单,本质就是一个DP来求多源最短路,由于代码简单,常被用于距离的预处理中。不多说,先上标准代码

#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int MAXN = 101;

int n,m,s,t;
int map[MAXN][MAXN];

int main()
{
    memset(map,127,sizeof(map));//距离赋为无穷大 
    scanf("%d%d",&n,&m);
    for (int i=1;i<=m;i++)
    {
        int from,to,dist;
        scanf("%d%d%d",&from,&to,&dist);
        map[from][to] = dist;
    }
    scanf("%d%d",&s,&t);
    //Floyed
    for (int k=1;k<=n;k++)      //先枚举中间节点 
        for (int i=1;i<=n;i++) if (i != k)
            for (int j=1;j<=n;j++) if (j != k && j != i)
                map[i][j] = min(map[i][j],map[i][k]+map[k][j]);
    printf("%d\n",map[s][t]);
    return 0;
}
 

        那么,Floyed算法还有没有其它的用途呢?当然有,首先的一个作用便是判断负权回路,实现很简单,只需要在最后判断map[i][i](0<i<=n)是否小于0,若是,则存在负权回路。但这种判回路的方法时间复杂度为O(n^3),效率远远比不上BellmanFord,因此不经常用。

        除此之外,Floyed最小环算法被广泛地使用,如果你要解决这样一个问题:在一个有向图中,可从任意一顶点出发,求最小回路是多少?

        虽然本题可以使用Dijkstra来完成,但没有Floyed方便快捷。Floyed最小环算法在网上有介绍,这里不作详解,仅贴上核心代码

    //Floyed最小环 
    //map[i][j]表示原距离 
    for (int k=1;k<=n;k++)      //先枚举中间节点 
        for (int i=1;i<=n;i++) if (i != k)
            for (int j=1;j<=n;j++) if (j != k && j != i)
                ans = min(ans,f[i][j]+map[i][k]+map[k][j]);

Dijkstra算法:

        Dijkstra是以贪心为基础的算法,它的原理很好理解,每次都找与起点距离最小的点,再不断更新,直到所有点都被更新过为止。强烈建议使用邻接表来进行存储,因为这样既省空间又省时间,在vector的辅助下,存边已经不需要链表的辅助。

#include <cstdio>
#include <cmath>
#include <cstring>
#include <vector>

using namespace std;

const int MAXN = 10001;
const int INF = 1000001;

struct Node
{
	int to,dist;
};

int n,m,s,t;
int dist[MAXN];
bool vis[MAXN];
vector <Node> G[MAXN];

int main()
{
	scanf("%d%d",&n,&m);
	for (int i=1;i<=m;i++)
	{
		int from,to,dist;
		scanf("%d%d%d",&from,&to,&dist);
		G[from].push_back((Node){to,dist});
	}
	scanf("%d%d",&s,&t);
	memset(dist,127,sizeof(dist));
	for (int i=0;i<G[s].size();i++) dist[G[s][i].to] = min(dist[G[s][i].to],G[s][i].dist);
	dist[s] = 0; vis[s] = true;
	for (int i=1;i<=n;i++)
	{
        int k,_min = INF;;
        for (int j=1;j<=n;j++) if (!vis[j] && dist[j] < _min)
        {
            k = j;
            _min = dist[j];
        }
        if (_min == INF) break;
        vis[k] = true;
        for (int j=0;j<G[k].size();j++)
            if (!vis[G[k][i].to] && dist[G[k][i].to] > dist[k]+G[k][i].dist) dist[G[k][i].to] = dist[k]+G[k][i].dist;
	}
	printf("%d\n",dist[t]);
	return 0;
}

        从以上代码我们发现,Dijkstra算法的时间复杂度为O(n^2),而大部分时间都是用来查找最小边权。有什么数据结构可以来快速寻找最值呢?优先队列!实现方法较为简单,直接上代码

Heap Dijkstra:

#include <cstdio>
#include <cmath>
#include <cstring>
#include <vector>
#include <queue>

using namespace std;

const int MAXN = 10001;

struct Node
{
	int to,dist;
	bool operator < (const Node &x) const
	{
        return dist > x.dist;
    }
};

int n,m,s,t;
int dist[MAXN];
bool done[MAXN];
vector <Node> G[MAXN];

int main()
{
	scanf("%d%d",&n,&m);
	for (int i=1;i<=m;i++)
	{
		int from,to,dist;
		scanf("%d%d%d",&from,&to,&dist);
		G[from].push_back((Node){to,dist});
	}
	scanf("%d%d",&s,&t);
	memset(dist,127,sizeof(dist));
    priority_queue <Node> Q;Q.push((Node){s,0});
    dist[s] = 0;
    while (!Q.empty())
    {
        Node x = Q.top();Q.pop();
        if (!done[x.to])
        {
            done[x.to] = true;
            for (int i=0;i<G[x.to].size();i++)
                if (dist[G[x.to][i].to] > dist[x.to] + G[x.to][i].dist)
                {
                    dist[G[x.to][i].to] = dist[x.to] + G[x.to][i].dist;
                    Q.push((Node){G[x.to][i].to,dist[G[x.to][i].to]});
                }
         }
        
    }
	printf("%d\n",dist[t]);
	return 0;
}
        从上面这段代码我们可以发现,优先队列维护的Dijkstra的时间复杂度为O(n*logn),是一个稳定的优秀算法。正因为它的稳定性,它的作用甚至比SPFA还大——除了不能处理负权。在许多比赛中,正因为SPFA有着不稳定性,常常会被出题人卡数据,导致SPFA降为O(VE)的算法,因此 强烈建议在竞赛中使用Heap Dijkstra。

BellmanFord算法:

        在许多比赛中,边权常常有负值——甚至会出现负权回路。因此,最稳定的Heap Dijkstra便无法发挥出它的作用。于是,BellmanFord算法便展现出了它的威力。O(VE)的算法,可以很好的处理负权并有效地判断负权回路。

#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;

const int MAXN=1001;

struct Node
{
    int from,to,dist;
};

int n,m,s,t;
int dist[MAXN];
int cnt[MAXN];
vector <Node> G;


int main()
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=m;i++)
    {
        int from,to,dist;
        scanf("%d%d%d",&from,&to,&dist);
        G.push_back((Node){from,to,dist});        
    }
    scanf("%d%d",&s,&t);
    memset(dist,127,sizeof(dist));
    for (int i=0;i<m;i++) if (G[i].from == s) dist[G[i].to] = min(dist[G[i].to],G[i].dist);
    dist[s] = 0;
    for (int i=1;i<=n;i++)
    {
        bool flag = false;
        for (int j=0;j<m;j++)
        {
            if (dist[G[j].to] > dist[G[j].from]+G[j].dist)
            {
                flag = true;
                dist[G[j].to] = dist[G[j].from]+G[j].dist;
            }
        }
        if (!flag) break;//避免冗余操作 
    }
    bool Negative_Ctyle = false;
    for (int i=0;i<m;i++) if (dist[G[i].to] > dist[G[i].from]+G[i].dist)
    {
        Negative_Ctyle = true;
        break;
    }
    if (!Negative_Ctyle) printf("%d\n",dist[t]);else printf("Negetive Ctyle Exists!\n");
    return 0;
} 

 
        由代码我们可以发现,O(VE)的操作中存在许多冗余操作,即使是加了flag来进行判断,也仅仅是去除了少部分冗余,在效率上并没有太大的提升。因此,为了有效地提升效率,SPFA算法便产生了。它使用队列来维护松弛操作,将时间复杂度降为O(kE)(可以证明k的平均大小为2),代码如下

SPFA:

#include <cstdio>
#include <cstring>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;

const int MAXN=1001;
const int INF=100000;

struct Node
{
    int to,dist;
};

int n,m,s,t;
int dist[MAXN];//离源点的最短距离 
int cnt[MAXN]; //判断是否存在负权回路 
bool inq[MAXN];//是否在队列中 
vector <Node> G[MAXN];

bool SPFA(int s)
{
    for (int i=1;i<=n;i++) dist[i]=INF;
    memset(inq,false,sizeof(inq));
    dist[s]=0;inq[s]=true;
    queue <int> Q;
    Q.push(s);
    while (!Q.empty())
    {
        int x=Q.front();Q.pop();
        inq[x]=false;
        for (int i=0;i<G[x].size();i++)
            if (dist[x]+G[x][i].dist<dist[G[x][i].to])
            {
                dist[G[x][i].to]=dist[x]+G[x][i].dist;
                if (!inq[G[x][i].to])
                {
                    Q.push(G[x][i].to);
                    inq[G[x][i].to]=true;
                    if (++cnt[G[x][i].to] > n) return true;
                }
            }
    } 
    return false;
}

int main()
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=m;i++)
    {
        int from,to,dist;
        scanf("%d%d%d",&from,&to,&dist);
        G[from].push_back((Node){to,dist});        
    }
    scanf("%d%d",&s,&t);
    bool Negative_Ctyle = SPFA(s);
    if (!Negative_Ctyle) printf("%d\n",dist[t]);else printf("Negative Ctyle Exist!\n");
    return 0;
} 


        以上代码还可以很好地实现差分约束系统,这里不作详解。

        关于SPFA与BFS,这也是最近看书的时候看到的,其实很多BFS的题目都可以由SPFA解决,比如经典的迷宫问题,用SPFA效率会快很多。


后记:

        为了更好地帮助新手实现模板,这里给出最短路径的模板题目

        http://www.rqnoj.cn/problem/341        

        同时,推荐几道题,供大家学习。

        https://www.vijos.org/p/1119(标准最短路,没什么难度但很累)

        https://www.vijos.org/p/1046(Floyed最小环)

        http://poj.org/problem?id=1860(BellmanFord判断负权回路)

        https://www.vijos.org/p/1155(次小路)





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值