单源最短路算法

知识点

  • dijkstra算法
  • 堆优化的dijkstra算法
  • SPFA算法

求单源最短路的暴力解法

1.枚举

Bellman-Ford算法好像长得比较像枚举

2.DFS应该不可做

3.BFS解法

伪代码如下:

void bfs(int s) {
    新建一个队列;
    visited全都赋值为false;
    dis全都赋值为无穷大;
    dis[s] = 0;
    s入队;
    visited[s] = true;
    while (队列非空) {
        x = 队首元素;
        队首元素出队;
        for (从x出发的所有边e) {
            if (dis[e的终点] > dis[x] + e的长度)
                dis[e的终点] = dis[x] + e的长度;
            if (visited[e的终点] == false) {
                把e的终点入队;
                visited[e的终点] = true;
            }
        }
    }
}

然而这个做法是错误的。

如上图。模拟过程如下:

调用bfs(1)
dis[1]赋值为0,1入队

1出队
dis[2]赋值为1,2入队
dis[6]赋值为100,6入队

2出队
dis[3]赋值为2,3入队

6出队
dis[5]赋值为101,5入队

3出队
dis[4]赋值为3,4入队

5出队
不更新dis[4],4不入队(因为visited[4]true

4出队
dis[5]赋值为4,5不入队(因为visited[5]true

然后你就会惊喜的发现dis[6]应该是5而不是这段代码所算出来的100

那么怎么解决这个问题呢?

A 防止“锁死”

不妨设想一下,如果在最后一步访问4的时候,我们把5入队了:

...(前略)

4出队
dis[5]赋值为4,5入队

5出队
dis[6]赋值为5,6不入队(因为visited[6]true

这样算出来的答案是对的。

所以visited是内鬼

能不能去掉visited数组或者改成别的东西呢?

SPFA算法

先上代码

void bfs(int s) {
    新建一个队列;
    visited全都赋值为false;
    dis全都赋值为无穷大;
    dis[s] = 0;
    s入队;
    visited[s] = true;
    while (队列非空) {
        x = 队首元素;
        队首元素出队;

        visited[x] = false; // 加上这一行

        for (从x出发的所有边e) {
            if (dis[e的终点] > dis[x] + e的长度)
                dis[e的终点] = dis[x] + e的长度;
            if (visited[e的终点] == false) {
                把e的终点入队;
                visited[e的终点] = true;
            }
        }
    }
}

这啥?

可以发现,visited[x]表示x是否在队列里。

然而这段代码显然不对,它会无限循环。

再改一下。

void bfs(int s) {
    新建一个队列;
    visited全都赋值为false;
    dis全都赋值为无穷大;
    dis[s] = 0;
    s入队;
    visited[s] = true;
    while (队列非空) {
        x = 队首元素;
        队首元素出队;
        visited[x] = false;
        for (从x出发的所有边e) {
            if (dis[e的终点] > dis[x] + e的长度) {
                dis[e的终点] = dis[x] + e的长度;

                if (visited[e的终点] == false) { // 把这个if移到上一个if里边来了
                    把e的终点入队;
                    visited[e的终点] = true;
                }

            }
        }
    }
}

为什么这样不会死循环呢?

原因在于,如果每个点的dis都已经正确地求出来了,那么第13行的if根本不会触发。(除非有负权回路)

正确性(选讲)

首先了解一下Bellman-Ford算法。

非常简洁的一个算法,每次循环依次枚举图中每条边,如果发现dis[这条边的终点]>dis[这条边的起点]+这条边的长度就把dis[这条边的终点]赋值为dis[这条边的起点]+这条边的长度。重复循环n-1次。

for (int i = 1; i <= n - 1; ++i)
	for (图中所有边e)
        if (dis[e的终点] > dis[e的起点] + e的长度)
            dis[e的终点] = dis[e的起点] + e的长度;

为什么它是正确的?

因为图中只有n个点,从起点出发到达任何一个点,最多也就走n-1步(或者说,经过n-1条边)(除非有负权回路)

最坏的情况:图是一条链,且每次都是从终点方向往起点方向枚举

只要图中还有未求出最短距离的点,每次循环它都会更新至少一个点的dis。由于dis[s]已知,所以我们最多再循环n-1次即可求得正确答案。

然而这个算法太慢了。特别是第一次循环的时候,能求出dis的其实只有与起点直接相连的那些点,而我们却仍然遍历了所有的边。

为此,我们有“队列优化的Bellman-Ford算法”,也就是SPFA。

SPFA相对于Bellman-Ford算法所做的优化:每次循环不再是枚举所有的边,而是枚举队列的点相连的边。那么什么样的点会放进队列里呢?上一次循环的时候dis值发生了变化的点。

于是我们成功的证明了SPFA算法的正确性(?

B 改变bfs顺序

下面假设图中所有边的长度都是正数。

引理1:一定存在一条边,连接 起点 距离起点最近的点;或者说,距离起点最近的点一定与起点之间直接有边相连。

定理1:起点距离起点最近的点 的最短距离等于连接这两个点的边的长度。

如上图,1是起点,那么1到2的最短距离就一定是1;反之,3不是距离1最近的点,那么1到3的最短距离就不一定是3(本例中是2)

定理1的推广:如果把 起点距离起点最近的点 合并为一个点,那么上述定理仍可使用。

定理1的推广的推广:该推广可多次反复使用。

原图:此时距离1最近的点是2,最短距离是1

第一次合并:此时距离1&2最近的点是3,最短距离是2

第二次合并:此时距离1&2&3最近的点是5,最短距离为3

第三次合并:此时距离1&2&3&5最近的点是4,最短距离是4

最终结果:dis[2] = 1 dis[3] = 2 dis[4] = 4 dis[5] = 3

问题来了:这个性质咋用?喵喵喵?

再看一下刚才那个图:

如果我们按照1 - 2 - 3 - 4 - 5 - 6的顺序进行搜索,好像就能得到正确答案吧?

Dijkstra算法(堆优化版

既然是“队列”这个数据结构导致答案错误,那就干脆不!用!了!

现在假想一种数据结构,叫

水池子

“倒”进去的元素会自动按照“密度”从大到小排列。

你可以从顶上往外“舀”。

人话:自动将内部元素排序,支持取出最小/大值

我们现在考虑用这样的一个数据结构来代替队列。

根据定理1,距离“起点”最近的点算出来的dis值一定是正确的。

也就是说,如果“水池子”里有多个待搜索的节点,我们可以先搜索距离起点近的点。

void bfs(int s) {
    新建一个水池子;
    visited全都赋值为false;
    dis全都赋值为无穷大;
    dis[s] = 0;
    s倒进水池子里;
    visited[s] = true;
    while (水池子非空) {
        x = 水池子里距离s最近的点;
        把x从水池子里舀出来;
        for (从x出发的所有边e) {
            if (dis[e的终点] > dis[x] + e的长度)
                dis[e的终点] = dis[x] + e的长度;
            if (visited[e的终点] == false) {
                把e的终点倒进水池子里;
                visited[e的终点] = true;
            }
        }
    }
}

模拟一下过程:

调用bfs(1)
dis[1]赋值为0,1入水池子

1出水池子
dis[2]赋值为1,2入水池子
dis[6]赋值为100,6入水池子

2出水池子
dis[3]赋值为2,3入水池子

3出水池子
dis[4]赋值为3,4入水池子

4出水池子
dis[5]赋值为4,5入水池子

5出水池子
dis[6]赋值为5,6不入水池子(因为visited[6]true

6出水池子

答案对了!

问题:如何维护这样的一个水池子

没学的话可以先跳过

void bfs(int s) {
    新建一个堆;
    visited全都赋值为false;
    dis全都赋值为无穷大;
    dis[s] = 0;
    s入堆;
    visited[s] = true;
    while (堆非空) {
        x = 堆顶;
        堆顶元素出堆;
        if (visited[x] == true) continue;
        visited[x] = true;
        for (从x出发的所有边e) {
            if (dis[e的终点] > dis[x] + e的长度)
                dis[e的终点] = dis[x] + e的长度;
            e的终点入堆,权值为 dis[e的终点];
        }
    }
}

不带堆优化的dijkstra算法

堆优化的版本都懂了 不带堆优化的不就直接会了啊

堆优化的dijkstra算法使用堆来查找“距离起点最近的点”;不带堆优化的dijkstra算法是暴力找这个“距离起点最近的点”。其他都一样。

for (int i = 1; i <= n; ++i) {
    int x, d = 无穷大;
    for (int p = 1; p <= n; ++p) {
        if (visited[p] == false && dis[p] < d) {
            x = p;
            d = dis[p];
        }
    }
    visited[p] = true;
    for (从x出发的所有边e) {
        if (dis[e的终点] > dis[x] + e的长度)
            dis[e的终点] = dis[x] + e的长度;
    }
}

总结

只要没有负权边,就写堆优化的dijkstra;有负权边就写SPFA。

模板:

P3371 【模板】单源最短路径(弱化版)

P4779 【模板】单源最短路径(标准版)

U69305 【常数PK系列】 #2 单源最短路(备注:我出的;它 非 常 有 意 思

洛谷官方题单:

【图论2-2】最短路

代码

P3371

#include<bits/stdc++.h>
#define inf 2147483647
using namespace std;
int n,m,s;
struct edge{
	int to,dis;
	edge(int a,int b):to(a),dis(b){}
};
vector<edge>edges;
vector<int>G[10003];
void add_edge(int u,int v,int w){
	edges.push_back(edge(v,w));
	G[u].push_back(edges.size()-1);
}
struct node{
	int p,dis;
	node(int a,int b):p(a),dis(b){}
};
bool operator< (node a,node b){
	return a.dis>b.dis;
}
priority_queue<node>q;
int d[10003];
bool u[10003];
int main(){
	cin>>n>>m>>s;
	for(int i=1;i<=m;i++){
		int u,v,w;
		cin>>u>>v>>w;
		add_edge(u,v,w);
	}
	fill(d+1,d+n+1,inf);
	d[s]=0;
	q.push(node(s,0));
	while(!q.empty()){
		node x=q.top();
		q.pop();
		//cout<<x.p<<' '<<x.dis<<endl;
		if(u[x.p])continue;
		u[x.p]=1;
		for(int i=0;i<G[x.p].size();i++){
			edge e=edges[G[x.p][i]];
			//cout<<'\t'<<e.to<<' '<<d[e.to]<<"->";
			d[e.to]=min(d[e.to],d[x.p]+e.dis);
			//cout<<d[e.to]<<endl;
			q.push(node(e.to,d[e.to]));
		}
	}
	for(int i=1;i<=n;i++){
		cout<<d[i]<<' ';
	}
} 

U69305

#include<bits/stdc++.h>
using namespace std;
int n,m,s,t;
int fir[250001],to[1000001],nxt[1000001],dis[1000001],ecnt;
void add(int u,int v,int w){
    to[++ecnt]=v;
    dis[ecnt]=w;
    nxt[ecnt]=fir[u];
    fir[u]=ecnt;
}
struct node
{
    int id;
    unsigned long long dis;
    node(int id,unsigned long long dis):id(id),dis(dis){}
};
bool operator<(node a,node b){
    return a.dis>b.dis;
}
priority_queue<node>q;
bool vis[250001];
unsigned long long d[250001];
int main(){
    cin>>n>>m>>s>>t;
    for(int i=1;i<=m;i++){
        int u,v,w;
        cin>>u>>v>>w;
        add(u,v,w);
        add(v,u,w);
    }
    fill(d+1,d+n+1,ULONG_LONG_MAX-INT_MAX);
    q.push(node(s,0));
    d[s]=0;
    while(!q.empty()){
        node h=q.top();
        q.pop();
        if(vis[h.id])continue;
        vis[h.id]=1;
        for(int e=fir[h.id];e;e=nxt[e]){
            if(!vis[to[e]]&&d[to[e]]>dis[e]+h.dis){
                d[to[e]]=dis[e]+h.dis;
                q.push(node(to[e],d[to[e]]));
            }
        }
    }
    cout<<d[t]<<endl;
} 
posted @ 2020-04-04 15:26  water_lift  阅读(205)  评论(0编辑  收藏  举报