Dijkstra的朴素模板、堆优化模板与一些问题

基本思想

  • 问题:有 n 个点,m 条边的正权图有向图中,求源点 s 到终点 end 的最短路径

先给出算法流程

  1. 首先定义distance数组,让 distance[s] = 0 ,其余节点的值赋为正无穷。其意义是源点 s 到其他节点的初始距离(点s 自己到自己的距离 == 0)。
  2. 找出一个未被标记且distance最小的点 t ,然后标记点 t 。
  3. 遍历一遍 t 的所有出边,并distance[t] 来 松弛 其余节点。
  4. 重复 2、3 步,直至所有的点被标记完。

dij算法是基于贪心思想,当图不含负权边时,该图就拥有最优子结构,既“最短路的子路径还是最短路”。可以看出,在不含负权边的图中,被标记后的节点就是 distance[t] 最小值,不会再被其他节点更新。

朴素模板【Code】

朴素Dij模板题: 洛谷P3371

#include<iostream>
#include<cstdio>
#include<cmath>

using namespace std;
const int inf = 0x3f3f3f3f;//正无穷大
const int N = 1e4 + 10;//点的最大值
const int M = 5e5 + 10;//边的最大值

//数组模拟邻接表存图
int h[N],e[M],w[M],ne[M],idx = 1;
int n,m,s;
int dis[N];//distance数组
bool st[N];//标记数组

// a->b 的边权值为 c 存入邻接表
void add(int a,int b,int c){
    e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx++;
}

//核心模板
void dij(int s){
    //第一步,给distance数组赋值
    fill(dis + 1,dis + n + 1,inf);
    dis[s] = 0;

    //第四部,重复 n - 1 次
    for(int i = 0;i < n - 1; ++ i){
        //第二步,找出未标记且distance最小的节点 t ,并标记
        int t = -1;
        for(int j = 1;j <= n; ++ j)
            if(!st[j] && (t == -1 || dis[j] < dis[t]))
                t = j;
        st[t] = 1;//标记操作

        //第三步,遍历节点 t 的所有出边,进行“松弛”
        for(int j = h[t]; j ;j = ne[j]){
            int k = e[j];
            dis[k] = min(dis[k],dis[t] + w[j]);//松弛操作
        }
    }
}

int main(){
    scanf("%d%d%d",&n,&m,&s);
    while(m -- ){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    dij(s);
    for(int i = 1;i <= n; ++ i)
        if(dis[i] == inf) printf("%d ",(int)(pow(2,31) - 1));
        else printf("%d ",dis[i]);
    return 0;
}

这个朴素模板的时间复杂度是O(n ^ 2),主要的瓶颈在于第二步寻找最小值的过程是O(n)的。这个寻找最小值的过程可以用二叉堆来实现,将O(n)的复杂度降低至O(logn)。最终总体实现O(mlogn)。

这里用STL的priority_queue容器来代替手写堆。

堆优化模板【Code】

堆优化Dij模板题: 洛谷P4779

#include<iostream>
#include<cstdio>
#include<cmath>
#include<queue>

using namespace std;
typedef pair<int,int> PII;
const int inf = 0x3f3f3f3f;//正无穷大
const int N = 1e5 + 10;//点的最大值
const int M = 2e5 + 10;//边的最大值

//数组模拟邻接表存图
int h[N],e[M],w[M],ne[M],idx = 1;
int n,m,s;
int dis[N];//distance数组
bool st[N];//标记数组

// a->b 的边权值为 c 存入邻接表
void add(int a,int b,int c){
    e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx++;
}

//核心模板
void dij(int s){
    //第一步,给distance数组赋值
    fill(dis + 1,dis + n + 1,inf);
    dis[s] = 0;

    priority_queue<PII,vector<PII>,greater<PII> > q;
    q.push({0,s});//first代表距离,second代表点

    while(!q.empty()){
        //取出堆顶(最小值),并删除堆顶
        int t = q.top().second;
        q.pop();

        //如果已经被标记了就跳过
        if(st[t]) continue;
        st[t] = 1;//标记

        for(int i = h[t]; i ;i = ne[i]){
            int j = e[i];
            if(dis[j] > dis[t] + w[i]){
                dis[j] = dis[t] + w[i];
                q.push({dis[j],j});
            }
        }
    }
}

int main(){
    scanf("%d%d%d",&n,&m,&s);
    while(m -- ){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    dij(s);
    for(int i = 1;i <= n; ++ i)
        if(dis[i] == inf) printf("%d ",(int)(pow(2,31) - 1));
        else printf("%d ",dis[i]);
    return 0;
}
关于Dij的部分问题
为什么不能用于含负权边的图?

Dij算法中已经被标记过的点就是最短路径点,如果拓展到负权边,则无法保证已经被标记的点是最短路径点,会破坏已经标记的点的路程不会改变的性质。

为什么不能用Dij的思想求解最长路径?

首先,如果图中含有正权环的话,是不存在最长路径的,就像图中含负权环就不存在最短路径一样。

  • 那在仅含正权边的DAG(有向无环图)中呢?

Dij之所以能求解最短路径,是因为最短路径有 最优子结构 的性质,既“最短路的子路径还是最短子路”,而最长路径则不含有这种子结构,既“最长路径的子路径不一定是最长子路”。所以不行。

  • 那要如何求解最长路径呢?
  1. 将所有边乘以 -1 并建图,用Bellman-Ford 算法 或 SPFA算法求其最短路,这个最短路的答案再乘以-1就是最长路。
  2. 拓扑排序 + 关键路径。

最长路模板题: 洛谷P1807

如何求最短路径中的最长边呢?

改变一下distance数组的意义即可,将意义从距离源点 s 的最短距离 改成 距离源点 s 的最短路径的最长边距离,既 if( dis[j] > max(dis[t],w[i]) ) dis[j] = max(dis[t],w[i]);

求最长边: 洛谷P1396

除了这样做还可以用kruskal算法求其最小生成树 or 二分 + dfs。


2020.08.05

©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页