最短路问题

Posted on 2018-08-30 11:18  亦辰落  阅读(362)  评论(0编辑  收藏  举报

最短路总共有四种算法:

Dijkstra算法,Floyd算法,Bellman-ford算法,spfa算法

bellman-ford可以用于边权为负的图中,图里有负环也可以,如果有负环,算法会检测出负环。 
时间复杂度O(VE); 
dijkstra只能用于边权都为正的图中。 
时间复杂度O(n2); 
spfa是个bellman-ford的优化算法,本质是bellman-ford,所以适用性和bellman-ford一样。(用队列和邻接表优化)。 
时间复杂度O(KE); 
floyd可以用于有负权的图中,即使有负环,算法也可以检测出来,可以求任意点的最短路径,有向图和无向图的最小环和最大环。 
时间复杂度O(n3); 
任何题目中都要注意的有四点事项:图是有向图还是无向图、是否有负权边,是否有重边,顶点到自身的可达性。 

例题:洛谷P3371

传送门

1.Dijkstra(单源点最短路) :

这个算法只能计算单源最短路,而且不能计算负权值,这个算法是贪心的思想,

dis数组用来储存起始点到其他点的最短路,但开始时却是存的起始点到其他点的初始路程。通过n-1遍的遍历找最短。

每次在剩余节点中找dist数组中的值最小的,加入到s数组中,并且把剩余节点的dist数组更新。

#include<bits/stdc++.h>
using namespace std;
#define maxn 10005
#define maxm 500005
#define INF  2147483647
inline int read(){
    int x=0,k=1; char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')k=-1;c=getchar();}
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x*k;
}
struct Edge
{
    int u,v,w,next;
}e[maxm];
int head[maxm],cnt,n,m,s,vis[maxn],dis[maxn];
#define P pair<long long,int>
priority_queue<P,vector<P>,greater<P> >q;
//把最小的元素放在队首的优先队列
inline void add(int u,int v,int w)
{
    e[++cnt].u=u;
    //这句话对于此题不需要,但在缩点之类的问题还是有用的
    e[cnt].v=v;
    e[cnt].w=w;
    e[cnt].next=head[u];
    //存储该店的下一条边
    head[u]=cnt;
    //更新目前该点的最后一条边(就是这一条边)
}
//链式前向星加边
void dijkstra()
{
    for(int i=1;i<=n;i++)
    {
        dis[i]=INF;
    }
    dis[s]=0;
    //赋初值
    q.push(make_pair(0,s));
    while(!q.empty())
    //堆为空即为所有点都更新
    {
        int x=q.top().second;
        q.pop();
        //记录堆顶并将其弹出
        if(!vis[x])
        //没有遍历过才需要遍历
        {
            vis[x]=1;
            for(int i=head[x];i;i=e[i].next)
            //搜索堆顶所有连边
            {
                int v=e[i].v;
                dis[v]=min(dis[v],dis[x]+e[i].w);
                //松弛操作
                q.push(make_pair(dis[v],v));
            }
        }
    }
}
int main()
{
    n=read(),m=read(),s=read();
    for(int i=1;i<=m;i++)
    {
        int x,y,z;
        x=read(),y=read(),z=read();
        add(x,y,z);
    }
    dijkstra();
    for(int i=1;i<=n;i++)
    {
        printf("%d ",dis[i]);
    }
    return 0;
}

 

2.Floyd:

不少人可能刚接触floyd的时候非常容易把它写错,错误的写法就是三层循环的从外到内的变量分别为i,j,k

正确的写法应该是k,i,j。写错的原因是不理解floyd算法造成的,那么为什么从顺序是k,i,j呢?

其实floyd的算法本质是个动态规划!

dp[k][i][j]代表i到j的中间节点(不包括i和j)都在区间[1,k]时,i到j的最短路。算法的最外层循环是个从小到大枚举k的过程,

当最外层刚刚进入第k次循环的时候,我们已经得到了所有点对的dp[k-1][][]的值,也就是所有点对(i,j)的i到j的中间节点都在[1,k-1]区间的i到j的最短路。

那么对任意的点对(i,j),如果他俩的最短路经过k点,则dp[k][i][j]=dp[k-1][i][k]+dp[k-1][k][j];

如果不经过k点,则dp[k][i][j]=dp[k-1][i][j]。所以当我们求dp[k][][]的时候,要保证所有的dp[i-1][][]都求出来了,

因此,最外层循环是k。 

每一层都是有上一层决定,不会受这一层影响,所以可以利用滚动数组优化内存空间,将k去除掉

任意节点i到j的最短路径不外乎两种可能:1、直接从i到j;2、从i经过若干个节点k到j。

Dis(i,j)表示节点i到j最短路径的距离,对于每一个节点k,检查Dis(i,k)+Dis(k,j)小于Dis(i,j),如果成立,

Dis(i,j) = Dis(i,k)+Dis(k,j);遍历每个k,每次更新的是除第k行和第k列的数。

floyd能做很多事情,下面简单说两个:

求有向图的最小环或者最大环(顶点数>=2),求无向图的最小环(顶点数>=3)。

先说求有向图最小环(最大环略)。有两种方法可以求,一种是设定g[i][i]为无穷大

这样最后找所有的g[i][i]里的最小值就行;另一种是正常做floyd,然后对每个点对(i,j)

求g[i][j]+dp[n][j][i]的最小值,这样的原理是最小环如果存在的话,那么可以枚举一个这个环里的边i->j,那么包含这条边的最小的环一定是i->j和dp[n][j][i]构成的最短路。

无向图的最小环做法和有向图不一样,是因为无向边可能会被用两次导致出错,

举例说就是:枚举了一条边i->j,然后其与dp[n][j][i]的和作为一个结果,但是如果j到i的最短路就是边j->i的话,

那么我们找的环其实只是一条边而已,这样的结果显然是错误的。正确的做法是先判断最小环再更新最短路.

因为会出现重边的现象,所以一个环一定至少有三个点,所以每次先判断最小环,

因为k必须是未用过的,此时的dist[i][j]是遍历了k-1次的最短路,用完判断了再去更新k对应的最短路。

每次比较dist[i][j]+ g[i][k]+g[k][j]的最小值。k—>i—>j—>k;这样一直保证环是三个点。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define inf 2147483647
#define maxn 10005
inline int read(){
//快读
    int x=0,k=1; char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')k=-1;c=getchar();}
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x*k;
}
int a[maxn][maxn],n,m,s;
inline void floyd()
{
    for(int k=1;k<=n;k++)
    //这里要先枚举k(中转点)
    {
        for(int i=1;i<=n;i++)
        {
            if(i==k||a[i][k]==inf)
            {
                continue;
            }
            for(int j=1;j<=n;j++)
            {
                a[i][j]=min(a[i][j],a[i][k]+a[k][j]);
                //松弛操作,即更新每两个点之间的距离
                //松弛操作有三角形的三边关系推出
            }
        }
    }
}
int main(){
    n=read(),m=read(),s=read();
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            a[i][j]=inf;
        }
    }
    //初始化,相当于memset(a,inf,sizeof(a))
    for(int i=1,u,v,w;i<=m;i++)
    {
        u=read(),v=read(),w=read();
        a[u][v]=min(a[u][v],w);
        //这种方法可以对付重边
    }
    floyd();
    a[s][s]=0;
    for(int i=1;i<=n;i++)
    {
        printf("%d ",a[s][i]);
    }
    return 0;
}

 

3.Bellman-Ford

适用范围:

1、单源最短路径(从源点到其他所有点v); 
2、有向图&无向图; 
3、边权可正可负 
4、差分约束系统 

图G(v,e),源点s,数组Distant[i]记录了从源点s到顶点i的路径长度,循环执行至多n-1次,n为顶点数。 

对每一条边e(u,v),如果Distant[u]+w[u,v]小于Distant[v],则Distant[v] = Distant[u]+w(u,v); 

每一次循环都会至少更新一个点,所以才会出现至多循环n-1次,一次更新是指用所有节点进行一次松弛操作

因为:

1、将所有节点分为两类:已知最短距离的节点和剩余节点。 

2、这两类节点满足这样的性质:已知最短距离的节点的最短距离值都比剩余节点的最短路值小。 

3、易知到剩余节点的路径一定会经过已知节点。 

4、而从已知节点连到剩余节点的所以边中的最小的那个边,这条边所更新后的剩余节点就一定是确定的最短距离,从而多找到了一个能确定最短距离的节点(不用知道它是哪个节点)。

实现过程的三步:

1、初始化所有的点,每一个点保存一个值,表示源点到这个点的距离其他点的值设为无穷大。 

2、进行循环,从1到n-1,进行松弛计算。 

3、遍历所有边,如果的d[v]大于d[u]+w(u,v)存在,则有从源点可达的权为负的回路。

#include<iostream>
using namespace std;
const int maxx=10001;
int n,m,s,dis[maxx],w[500001],num[maxx],f[maxx][maxx/10][2],a=0;
int main(){
    ios::sync_with_stdio(false);
    cin>>n>>m>>s;
    for(int i=1;i<=n;i++) dis[i]=400;
    for(int i=1;i<=m;i++)
    for(int j=1;j<=m;j++) w[i]=400;
    for(int i=1;i<=m;i++){
        int x,y,v;
        cin>>x>>y>>v;
        f[x][++num[x]][0]=y;
        f[x][num[x]][1]=i;
        w[i]=v;
    }
    dis[s]=0;
    while(a<=50){ //循环大法好
        for(int i=1;i<=n;i++)
        for(int j=1;j<=num[i];j++)
        dis[f[i][j][0]]=min(dis[f[i][j][0]],dis[i]+w[f[i][j][1]]);
        a++;
    }
    for(int i=1;i<=n;i++) if(dis[i]==400) cout<<2147483647<<' '; else cout<<dis[i]<<' ';
    return 0;
}

 

4.SPFA

spfa其实是Bellman-Ford的优化

它和Bellman-Ford一样可以处理负边权

定理: 只要最短路径存在,上述SPFA算法必定能求出最小值。

运用从不证明思想,所以把它背下来就好

其次SPFA无法处理带负环的图

下面为例题代码:

//SPFA
#include<cstdio>
#include<cstring>
using namespace std;
const int M=5e5+200;
const int N=1e5+100;
#define ll long long 
#define init memset(head,0,sizeof(head))
#define inf 2147483647
int head[N],tot(0),n,m,s;
struct edge
{
    ll to,next,dis;
}e[M];
ll dis[N];
inline void add(int u,int v,int dis)
{
    e[++tot]=(edge){v,head[u],dis};
    head[u]=tot;
}
inline void input()
{
    int f,g,w;
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&f,&g,&w);
        add(f,g,w);
    }
}
inline void spfa()
{
    int queue[M<<1],t(1),h(0);
    for(int i=1;i<=n;i++) dis[i]=inf;
    bool vis[N];
    memset(vis,true,sizeof(vis));
    vis[s]=false;
    dis[s]=0;
    queue[0]=s;
    while(t>h)
    {
        int node=queue[h++];
        for(int i=head[node],x=e[i].to,w=e[i].dis;i;i=e[i].next,x=e[i].to,w=e[i].dis)
        {
            if(dis[node]+w<dis[x])
            {
                dis[x]=dis[node]+w;
                if(vis[x]) queue[t++]=x,vis[x]=false;
            } 
        }
        vis[node]=true;
    }
}
inline void output()
{
    for(int i=1;i<=n;i++)
    {
        printf("%lld ",dis[i]);
    }
}
int main()
{
    init;
    input();
    spfa();
    output();
    return 0;
}