(算法基础)朴素版的Dijkstra算法

Posted Shensk

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了(算法基础)朴素版的Dijkstra算法相关的知识,希望对你有一定的参考价值。

适用情景

  1. 在最短路问题当中的单源最短路(一号点到其他所有点之间的距离)的只有正权边的情况,且朴素版适用于稠密图(n^2 ~ m)


时间复杂度

  1. O(N^2)


算法解释(朴素版的Dijkstra)

  1. 首先是关于这个图的存储,图的话主要是分为稠密图与稀疏图。稠密图就是说n的平方与m是一个量级的,对于稠密图的话,用邻接矩阵来存;稀疏图的话是n与m为一个量级的,对与稀疏图的话,就用邻接表来存。在这边我先举一个用邻接矩阵存图为例子吧。

int g[N][N];
  1. 然后这个邻接矩阵等会儿我是要把具体的边的信息放进去的,所以在一开始有必要进行一些预处理初始化,在最短路问题当中有两类特殊情况:这就是重边,一个就是自环,由于当下背景是最短路,对于重边而言的话,只需要取小的就可以;然后对于自环而言的话,不能是负环(不然最短路就变成负无穷了),然后对于正环而言的话,在最短路里相当于没有。这些得知道。初始化

memset(h,0x3f,sizeof(h));
  1. 然后还需要开两个数组,一个数组就是用来记录一下该点是不是已经确定了最短路距离,还有一个数组用来记录一下每点到一号点之间的最短路距离是多少,一开始的话每点初始化为正无穷(当然,一号点除外,它到他自己本身的距离就是0

int st[N];
int dist[N];
memset(dist,0x3f3f3f3f,sizeof(dist));
dist[1]=0;
  1. 然后往邻接矩阵里面输入每一条边的有关信息.

while(m--)

    int a,b,c;
    scanf("%d %d %d",&a,&b,&c);
    g[a][b]=MIN(g[a][b],c);
  1. 然后就进入了迪杰斯特拉算法的精髓:首先要去找到这么一个点1. 还没有被正式确认下来最短路距离,2. 到1号点的距离比其他与他一样的同胞(还没有被正式确认最短路距离)到一号点的距离都要短(因此逃不开还要遍历一遍所有点)。先把这个点去找到,这个点找到之后,去遍历一下这个点所能连接到的其他点,看一看能不能更新那些点的最短路距离。遍历一圈完成之后,这个点的最短路距离也就正式被确定下来然后依次往复,不断循环。显而易见,每一次循环确定下来一个点的最短路距离,既然有n个点,那么就需要n次循环

for (int i=0;i<n;i++)

    int t=0;
    for (int j=1;j<=n;j++)
    
        if (st[j]==0 && (t==0 || dist[j]<dist[t]))
        
            t=j;
        
    
    st[t]=1;
    for (int j=1;j<=n;j++)
    
        dist[j]=MIN(dist[j],dist[t]+g[t][j]);
    
  1. 然后由于迪杰斯特拉算法的要求,就是说所有的边都必须是正权边,因此如果说从一号点走不到n号点的话,那么n号点的距离dist应该还是保持初始化的那个值0x3f3f3f3f,在当前情形是不存在负权边,这个初始化的正无穷大值不会被略微更新小

if (dist[n]==0x3f3f3f3f)

    printf("%d\\n",-1);

else

    printf("%d\\n",dist[n]);

例题

来源:AcWing

849. Dijkstra求最短路 I - AcWing题库

#include <stdio.h>
#include <string.h>
#define N 510
#define MIN(a,b) ((a)<(b)?(a):(b))
int g[N][N];
int st[N];
int dist[N];
int main()

    memset(dist,0x3f3f3f3f,sizeof(dist));
    dist[1]=0;
    int n,m;
    scanf("%d %d",&n,&m);
    memset(h,0x3f,sizeof(h));
    while(m--)
    
        int a,b,c;
        scanf("%d %d %d",&a,&b,&c);
        g[a][b]=MIN(g[a][b],c);
    
    for (int i=0;i<n;i++)
    
        int t=0;
        for (int j=1;j<=n;j++)
        
            if (st[j]==0 && (t==0 || dist[j]<dist[t]))
            
                t=j;
            
        
        st[t]=1;
        for (int j=1;j<=n;j++)
        
            dist[j]=MIN(dist[j],dist[t]+g[t][j]);
        
    
    if (dist[n]==0x3f3f3f3f)
    
        printf("%d\\n",-1);
    
    else
    
        printf("%d\\n",dist[n]);
    
    return 0;

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

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

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
");
        else{
            printf("%d
",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
",dist[n]);
    else{
        printf("impossible
");
    }
    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
");
        else                printf("NO
");
    }
    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
",dis[n]);
    else        printf("impossible
");
}

以上是关于(算法基础)朴素版的Dijkstra算法的主要内容,如果未能解决你的问题,请参考以下文章

ACwing(基础)--- Dijkstra算法(含堆优化版)

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

Dijkstra算法详解

最短路径-dijkstra算法

Dijkstra:正边权单源最短路算法

基础算法[四]之图的那些事儿