求解最短路的四个算法及其优化

目录

求解最短路的四个算法及其优化

1.Dijkstra算法

Dijkstra很好的运用了贪心算法,其思想是一直找离已加入顶点集合的最短边,更新邻点,下面是实现代码:

<1.朴素Dijkstra算法:

【题意】:给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。

直接Dijkstra即可,注意是有向边,如果是无向边,则需要将两个端点都要存进去而且数组大小要开两倍!!!;这个图的范围为一个稠密图,适合朴树的Dijkstra算法,其时间复杂度为\(O(v^2)\);show code:

#include<bits/stdc++.h>

using namespace std;
const int maxn=1e5+10;
const int inf=0x3f3f3f3f;
struct node
{
    int nex,to,val;
}Edge[maxn];
int head[maxn],n,m,tot=0;
int dis[maxn],v[maxn];
void add(int from,int to,int val)
{
    Edge[++tot].nex=head[from];
    Edge[tot].to=to;
    Edge[tot].val=val;
    head[from]=tot;
}
void Dijkstra(int s)              //s到任一节点的距离
{
    for(int i=0;i<=n;++i)
        dis[i]=(i==s)?0:inf;     //加0方便处理(也可以不加)
    for(int i=1;i<=n;++i)
    {
        int pos=0;
        for(int j=1;j<=n;++j)
            if(!v[j]&&dis[j]<dis[pos])  pos=j;
        v[pos]=1;
        for(int j=head[pos];j!=-1;j=Edge[j].nex)    //找以pos开头的边判断端点是否被访问过
            if(!v[Edge[j].to]&&dis[pos]+Edge[j].val<dis[Edge[j].to]){
                dis[Edge[j].to]=dis[pos]+Edge[j].val;
            }
    }
}

int main()
{
    ios::sync_with_stdio(false);
    while(cin>>n>>m)
    {
        memset(head,-1,sizeof(head));
        for(int i=1;i<=m;++i){
            int a,b,val;
            cin>>a>>b>>val;
            add(a,b,val);
            //add(b,a,val);     //如果是无向图要加上这个
        }
        Dijkstra(1);
        dis[n]==inf?cout<<-1<<endl:cout<<dis[n]<<endl;
    }
}

<2:堆优化的Dijkstra算法

堆优化的Dijkstra算法适合稀疏图,其时间复杂度为\(O(vloge)\)无向图数组开两倍!!!

还是上面的题意,有自环和重边,求到n号节点的最小值,不存在输出-1.

show code:

#include<bits/stdc++.h>

using namespace std;
const int maxn=1e5+10;
const int inf=0x3f3f3f3f;
int n,m,head[maxn],dis[maxn],v[maxn],tot=0;
struct Node{
    int dis, pos;           //dis保存距离 pos保存这个dis的点是pos
    Node(int a = 0, int b = 0): dis(a), pos(b) {}
    friend bool operator < (const Node &a, const Node &b){
        return a.dis > b.dis;
    }       //重载小于号, 令dis越大的Node越小,这样优先队列默认按降序排列,top就是dis最小的
    //相当于大根堆变成小根堆
};
struct node
{
    int nex,to,val;
}edge[maxn];

inline void add(int from, int to, int val)
{
    edge[++tot].nex = head[from];
    edge[tot].to = to;
    edge[tot].val = val;
    head[from] = tot;
}
void Dijkstra(int s)
{
    priority_queue<Node> q;          
    for(int i = 1;i <= n; ++i)
        dis[i] = (i == s) ? 0 : inf;
    q.push(Node(0, s));
    while(!q.empty())
    {
        int pos = q.top().pos;      //找到距离最小的点  
        q.pop();                     //找到距离离源点最大的点将它更新一波
        if(v[pos])  continue;
        v[pos] = 1;
        for(int i = head[pos]; i != -1; i = edge[i].nex){
            if(dis[pos] + edge[i].val < dis[edge[i].to])
            {
                dis[edge[i].to] = dis[pos]+edge[i].val;
                q.push(Node(dis[edge[i].to], edge[i].to));  
            }
        }
    }
}

int main()
{
    ios::sync_with_stdio(false);
    while(cin >> n >> m)
    {
        memset(head, -1, sizeof(head));
        memset(v, 0, sizeof(v));
        tot = 0;
        for(int i = 1; i <= m; ++i){
            int a, b, val;
            cin >> a >> b >> val;
            add(a, b, val);
        }
        Dijkstra(1);
        dis[n] == inf ? cout << -1 << endl : cout << dis[n] << endl;
    }
}

2.Floyd算法

Floyd(弗洛伊德)算法的原理是:一直看能不能走别的点使得两点的距离更短(不会组织语言),其核心思想是区间dp,可以处理带负权的边,其时间复杂度为\(O(v^3)\)看代码:

【题意】:n个点,m条边,q个询问,存在负权和重边,求任意两点之间的最短路径

#include<bits/stdc++.h>

using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=210;
int d[maxn][maxn],n,m,q;
void Floyd()
{
    for(int k=1;k<=n;++k)
        for(int i=1;i<=n;++i)
            for(int j=1;j<=n;++j){
                if(d[i][k]==inf||d[k][j]==inf)  continue;
                d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
            }
}

int main()
{
    scanf("%d %d %d",&n,&m,&q);
    for(int i=1;i<=n;++i)
        for(int j=1;j<=n;++j)
            d[i][j]=(i==j)?0:inf;       //初始化
    for(int i=1;i<=m;++i){
        int a,b,c;
        scanf("%d %d %d",&a,&b,&c);
        d[a][b]=min(d[a][b],c);         //处理重边
    }
    Floyd();
    for(int i=1;i<=q;++i){
        int a,b;
        scanf("%d %d",&a,&b);
        if(d[a][b]==inf)    printf("impossible\n");
        else{
            printf("%d\n",d[a][b]);
        }
    }
}

要想输出路径,其实也很简单,只要在加个Path数组记录一下路径即可:

void Floyd()
{
    for(int i=1;i<=n;++i){
        for(int j=1;j<=n;++j)
            path[i][j]=(d[i][j]==inf)?-1:j;     //初始化路径,输入之后再初始化
    }
    for(int k=1;k<=n;++k)
        for(int i=1;i<=n;++i)
            for(int j=1;j<=n;++j){
                if(d[i][k]==inf||d[k][j]==inf)  continue;
                if(d[i][j]>d[i][k]+d[k][j]){
                    d[i][j]=d[i][k]+d[k][j];
                    path[i][j]=path[i][k];
                }
            }
}
void print(int start,int end)
{
    printf("%d ",start);
    while(start!=end)
    {
        printf("%d ",path[start][end]);
        start=path[start][end];
    }
}

Floyd还可以用来传递闭包(解不等式):

void floyd() 
{
    for(int k=1;k<=n;++k){
        for (int i=1;i<=n;++i){
            for (int j=1;j<=n;++j){
                d[i][j]=d[i][j]|(d[i][k]&&d[k][j]);
            }
        }
    }
}

简单用法:题意为求任意两种货币是否可以获益:

#include<bits/stdc++.h>

using namespace std;
const int maxn=100;
string s,temp;
map<string,int> mp;
int n,m,cas=1;
double d[maxn][maxn];
bool Floyd()
{
    for(int k=1;k<=n;++k)
        for(int i=1;i<=n;++i)
            for(int j=1;j<=n;++j){
                if(d[i][k]==0||d[k][j]==0)  continue;
                if(d[i][j]<d[i][k]*d[k][j])
                    d[i][j]=d[i][k]*d[k][j];
            }
    for(int i=1;i<=n;++i){
        if(d[i][i]>1.0) return true;
    }
    return false;
}

int main()
{
    ios::sync_with_stdio(false);
    while(cin>>n&&n)
    {
        for(int i=1;i<=n;++i)
            for(int j=1;j<=n;++j)
                d[i][j]=(i==j)?1.0:0;
        for(int i=1;i<=n;++i){
            cin>>temp;
            mp[temp]=i;
        }
        cin>>m;
        string a,b;
        double val;
        for(int i=1;i<=m;++i){
            cin>>a>>val>>b;
            d[mp[a]][mp[b]]=val;
        }
        cout<<"Case "<<cas++<<": ";
        if(Floyd())   cout<<"Yes"<<endl;
        else          cout<<"No"<<endl;
    }
}

3.Bellman-Ford算法

Bellman-Ford是基于迭代思想,它扫描所有的边(x,y,z),如果:\(dis[y]>dis[x]+z\),则更新\(dis[y]=dis[x]+z\),一直到算法结束。

其优点是允许图中存在负权边,且时间复杂度为\(O(ve)\)

看模板:

#include <bits/stdc++.h>

using namespace std;
const int maxn=1e5+10;
const int inf=0x3f3f3f3f;
struct node
{
    int from,to,val;
}edge[maxn];
int  dist[maxn],n,m;                //n为点的个数,m为边的个数
bool Bellman_Ford(int s)
{
    for(int i=1;i<=n;++i)
        dist[i]=(i==s)?0:inf;       //初始化
    for(int i=1;i<n;++i)
        for(int j=1;j<=m;++j){
            if(dist[edge[j].from]+edge[j].val<dist[edge[j].to])
                dist[edge[j].to]=dist[edge[j].from]+edge[j].val;
        }
    bool flag=1;
    // 判断是否有负环路
    for(int i=1;i<=m;++i)
        if(dist[edge[i].to]>dist[edge[i].from]+edge[i].val){
            flag=0;
            break;
        }
    return flag;
}
int main()
{
    scanf("%d%d",&n,&m);       //s为源点
    for(int i=1;i<=m;++i)
        scanf("%d%d%d",&edge[i].from,&edge[i].to,&edge[i].val);
    if(Bellman_Ford(1)&&dist[n]!=inf)
        printf("%d\n",dist[n]);
    else{
        printf("impossible\n");
    }
    system("pause");
}

【题意】:poj-1860 每两种货币之间可以兑换,给定兑换比率和手续费,问能不能通过若干次交换回到原始货币且比原有的货币多。

#include<cstdio>
#include<cstring>
#include<cstdlib>

using namespace std;
const int maxn=1e3+10;
int n,m,s,tot=0;
int de[maxn][2];         //0 start 1 end
double ce[maxn][2];      //0 rate 1 commission
double dis[maxn],v;
bool Bellman_ford(int s)
{
    for(int i=1;i<=n;++i)   dis[i]=0;
    dis[s]=v;
    for(int i=1;i<n;++i){
        int flag=false;
        for(int j=1;j<=tot;++j){
            if(dis[de[j][1]]<(dis[de[j][0]]-ce[j][1])*ce[j][0]){
                dis[de[j][1]]=(dis[de[j][0]]-ce[j][1])*ce[j][0];
                flag=true;
            }
        }
        if(!flag)   return false;
    }
    for(int i=1;i<=tot;++i)
        if(dis[de[i][1]]<(dis[de[i][0]]-ce[i][1])*ce[i][0])     //已经是最大的,如果还能更大的话,说明有正环
            return true;
    return false;
}

int main()
{
    while(~scanf("%d%d%d%lf",&n,&m,&s,&v))
    {
        tot=0;
        for(int i=1;i<=m;++i)
        {
            int a,b;
            double c,d,e,f;
            scanf("%d%d%lf%lf%lf%lf",&a,&b,&c,&d,&e,&f);
            tot++;
            de[tot][0]=a;
            de[tot][1]=b;
            ce[tot][0]=c;
            ce[tot][1]=d;
            tot++;
            de[tot][0]=b;
            de[tot][1]=a;
            ce[tot][0]=e;
            ce[tot][1]=f;
        }
        if(Bellman_ford(s)) printf("YES\n");
        else                printf("NO\n");
    }
    system("pause");
}

4.SPFA算法

SPFA算法又叫做队列优化的Bellman-Ford算法,其时间复杂度为\(O(ke)\),其中k是一个很小的常数。不过在特殊情况下其时间复杂度会退化到\(O(ev)\)

它的算法流程和Bellman-Ford类似:先建立一个队列,队列中只包含一个起点s,接着扫描队头所有的出边(x,y,z),如果:\(dis[y]>dis[x]+z\),则更新\(dis[y]=dis[x]+z\),直至算法结束。

裸模板:

#include<bits/stdc++.h>
 
using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=1e5+10;;
int n,m,w;
struct Edge
{
    int nex,to,val;
}edge[maxn];
int head[maxn],dis[maxn],vis[maxn],mark[maxn],tot;
void init()
{
    memset(head,-1,sizeof(head));
    tot=0;
}
void add(int from,int to,int val)
{
    edge[++tot].to=to;
    edge[tot].val=val;
    edge[tot].nex=head[from];
    head[from]=tot;
}
bool SPFA(int s)
{
    for(int i=1;i<=n;i ++){
        mark[i]=0;                  //记录每个点入队列次数
        dis[i]=inf;
        vis[i] = 0;
    }
    queue<int> q;
    q.push(1);                      //我们只需要判断负环,随便找一个起点就好
    dis[s]=0;
    vis[s]=1;//入队列
    mark[s]++;
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        vis[u] = 0;//出队列
        for(int i=head[u];i!=-1;i=edge[i].nex)
        {
            int v=edge[i].to;
            if(dis[v]>dis[u]+edge[i].val)
            {
                dis[v]=dis[u]+edge[i].val;
                if(!vis[v])                 //不在队列中的时候入队
                {
                    q.push(v);
                    mark[v]++;
                    vis[v]=1;
                }
                if(mark[v]>=n)              //说明存在负环
                    return false;
            }
        }
    }
    return true;
}
int main()
{
    init();
    int u,v,z;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&u,&v,&z);
        add(u,v,z);
    }
    if(SPFA(1)&&dis[n]!=inf)  printf("%d\n",dis[n]);
    else        printf("impossible\n");
}

相关推荐