zoukankan      html  css  js  c++  java
  • 最短路问题

    最短路总共有四种算法:

    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;
    }
  • 相关阅读:
    mysql日期计算转换
    Mysql的DATE_FORMAT()日期格式转换
    JDBC连接池BoneCP
    JSP之三大指令
    JSP的三大指令 七大动作 九大对象
    JSP的语法
    orcale序列操作
    Orcale约束-------外键约束
    Orcale约束-------檢查約束
    Orcale约束-------主键约束
  • 原文地址:https://www.cnblogs.com/U58223-luogu/p/9559047.html
Copyright © 2011-2022 走看看