图论最短路及生成树(Prim,Djikstra,Spfa,Bellan-ford,kruskal,topsort)

图论在算法中具有举足轻重的地位,只有学好图才能游刃有余。本文章将介绍图论中一些基础算法,可以说总结的十分全面,文章结尾也会分析各算法的差异,清晰易懂。并附上代码模板.


图论(最短路、生成树)


一、拓扑排序

什么是拓扑序?
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。

算法思想:放在前面的点的入度一定更小(起点为入度为0),每次将入度为0的点放入队列中去更新其相连的点,使其相连的点入度-1。

条件:有向无环图. 注:不能存在自环

bool st[N];
int top[N];
int d[N];

bool topsort(){
    queue<int> q;
    for(int i=1;i<=n;i++)   if(!d[i])   q.push(i);
    
    int k = 0;
    while(!q.empty()){
        int t = q.front();
        q.pop();
        st[t] = 1;
        top[k++] = t;
        for(int i=h[t];i!=-1;i=ne[i]){
            int j = e[i];
            
            if(!st[j]){
                d[j]--;
                
                if(!d[j])   q.push(j);
            }
        }
    }

    return k==n;
}

二、Djikstra算法

算法思想:Dijkastra,每次找出不再集合中的距离源点距离最小的点去更新其他点. dist数组保存到源点的距离。

适用于有向图 可存在重边和自环 但不能存在负权

1. 朴素算法

// 朴素
int Dijkastra1(){
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    
    for(int i=0;i<n;i++){
        int t = -1;
        
        for(int j=1;j<=n;j++){
            if(!st[j] && (t==-1 || dist[t]>dist[j])){
                t = j;
            }
        }
        
        st[t] = 1;
        
        for(int j=h[t];j!=-1;j=ne[j]){
            int k = e[j];
            
            dist[k] = min(dist[k],dist[t]+w[j]);
        }
           
    }
    
    if(dist[n]==0x3f3f3f3f)   return -1;
    else    return dist[n];
}

2. 优先队列优化

typedef pair<int,int> PII;
int Dijkastra2(){
    memset(dist,0x3f,sizeof dist);
    priority_queue<PII,vector<PII>,greater<PII> > p;
    dist[1] = 0;
    p.push({0,1});
    
    while(!p.empty()){
        PII t = p.top();
        p.pop();
        
        if(st[t.second])    continue;
        st[t.second] = 1;
        
        for(int i=h[t.second];i!=-1;i=ne[i]){
            int j = e[i];
            
            if(dist[j]>dist[t.second]+w[i]){
                dist[j] = dist[t.second]+w[i];
                
                p.push({dist[j],j});
            }
        }
    }
    
    if(dist[n]==0x3f3f3f3f)   return -1;
    else    return dist[n];
}

三、Bellan-ford算法

Bellan-ford有边数限制:虽算法效率不高,却有一定的实际意义。

想象这样一个背景:你在一号城市,想去n号城市去旅游。但是飞机没有1号城市从直达n号城市的(只能换乘飞机到达)。你想要花费最少到达目的地;但是吧,每换乘一次,你的心情就会变差一回,也就是说最多换乘k次。问最优解是什么? (来自yxc大佬)

Bellman-ford算法:
可以用于判断是否存在负权回路
步骤:
       for  k次循环(最多经过k条边)
         for  所有边a,b,w(权重)
           更新:dist[b] = min(dist[b],dist[a]+w)
负权环,指的是一个图中存在一个环,里面包含的边的边权总和<0  
int Bellman_ford(){
    memset(dist,0x3f,sizeof(dist));
    dist[1] = 0;
    
    for(int i=0;i<k;i++){
        memcpy(backup,dist,sizeof dist);
        
        for(int i=0;i<m;i++){
            int a = edges[i].a,b = edges[i].b,c = edges[i].w;
            
            dist[b] = min(dist[b],backup[a]+c);
        }
    }
    
    if(dist[n] > 0x3f3f3f3f/2) return -1;
    else return dist[n];
}

注:图中可能存在重边和自环, 边权可能为负数

四、Floyd算法

有向图,图中可能存在重边和自环,边权可能为负数
Floyd算法用于求解两点之间的最短路问题.

int d[N][N];

void floyd(){
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            for(int q=1;q<=n;q++){
                d[j][q] = min(d[j][q],d[j][i]+d[i][q]);
            }
        }
    }
}

五、Spfa算法

spfa算法:可以理解为一圈一圈计算

1.求最短路

int dist[N];
bool st[N];
int spfa(){
    queue<pii> q; 
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    
    q.push({0,1});
    st[1] = 1;
    while(!q.empty()){
        pii t = q.front();
        q.pop();
        st[t.second] = 0;
        
        for(int i=h[t.second];i!=-1;i=ne[i]){
            int j = e[i];
            
            if(dist[j]>dist[t.second]+w[i]){                // 此处与迪杰斯特拉算法不同.         由于dist[] 总是储存的是当前最小值
                dist[j] = dist[t.second] + w[i];
                
                if(!st[j]){
                    st[j] = 1;
                    q.push({dist[j],j});
                }
            }
        }
    }
    
    if(dist[n]==0x3f3f3f3f)  return -1;
    else return dist[n];
}

2.判断负环

bool spfa() {
    memset(dist, 0x3f, sizeof dist);
    for (int i = 1; i <= n; i++) {
        q.push(i);
        st[i] = true;
    }
    st[1] = true;
    while (q.size()) {
        int t = q.front();
        q.pop();
        st[t] = false;
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;
                if (!st[j]) {
                    st[j] = true;
                    q.push(j);
                }
            }
        }
    }
    return false;
}

六、Prim算法求最小生成树

无向图;图中可能存在重边和自环;边权可能为负数。

算法思想:更新到集合的最短距离dist

int state[N],pre[N],st[N],dist[N];
int res;

void prim(){
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    
    for(int i=1;i<=n;i++){
        int t=-1;
        
        for(int j=1;j<=n;j++){
            if(!st[j] && (t==-1||dist[t]>dist[j])){
                t = j;
            }
        }
        
        st[t] = 1;
        res += dist[t];
        
        for(int j=1;j<=n;j++){
            if(!st[j] && dist[j]>g[j][t]){
                dist[j] = g[j][t];
            }
        }
    }
}

七、Kruskal算法求最小生成树

算法思想:借助并查集优化算法

#include <bits/stdc++.h>

using namespace std;

const int N = 100100;
const int M = 2*N;         // 边的数量

struct edge{
    int a;
    int b;
    int w;
    
    bool operator <(const edge &x) const
    {
        return w<x.w;
    }
}egs[M];

int n,m;
int pre[N];

int find(int x){
    if(pre[x]==x)   return x;
    else    return pre[x] = find(pre[x]);
}

int kreskal(){
    int res = 0,cnt = 0;
    
    for(int i=0;i<m;i++){
        int a = egs[i].a, b = egs[i].b, w = egs[i].w;
        
        if(find(a)!=find(b)){
            pre[find(a)] = pre[find(b)];
            cnt ++;
            res += w;
        }
    }
    
    if(cnt==n-1)    return res;
    else    return 0x3f3f3f3f;
}

int main()
{
    scanf("%d%d",&n,&m);
    int a,b,c;
    for(int i=1;i<=n;i++)   pre[i] = i;
    
    for(int i=0;i<m;i++){
        scanf("%d%d%d",&a,&b,&c);
        
        egs[i] = {a,b,c};
    }
    
    sort(egs,egs+m);
    int t = kreskal();
    
    if(t==0x3f3f3f3f)   cout << "impossible" << endl;
    else    cout << t << endl;
    
    return 0;
}

八、一点问题:

1、prim 和 dijkstra 的区别与联系:

  1. prim 为更新到集合的最短距离dist; 而dijkastra 为更新到源点的最短距离dist。即更新方式不同
  2. .两者均可以使用优先队列优化.不过在优化时,prim最好采用kruskal优化.

2、spfa 和 dijkstra 的区别与联系:

  1. st数组的作用不同,可以将一个点反复设为0和1
  2. SPFA算法中使用的是队列,目的只是记录一下当前发生过更新的点;而dijkstra算法使用的是优先队列,目的是取出不在集合中的距离源点最近的点

3、求负环的常用方法:

求负环的常用方法,基于SPFA,一般都用方法 2:
方法 1:统计每个点入队的次数,如果某个点入队n次,则说明存在负环
方法 2:统计当前每个点的最短路中所包含的边数,如果某点的最短路所包含的边数大于等于n,则也说明存在负环


总结:

学会不是目的,重要的是能够熟悉算法思想,理解到代码中的细节,这样才能在做题过程中游刃有余。

上一篇:linux上传python项目,从虚拟环境运行


下一篇:Python3 多线程