浅谈Dijkstra

一.前言

有一说一,今天心情极度不佳,于是找来了一直似懂非懂的Dijkstra来做......

Dijkstra是一种求最短路径的算法,学名叫单源最短路,这里的源指的是起点,即dijkstra仅能计算唯一起点对应多个终点的问题,和BFS十分相似。

和其他的算法类似,dijkstra有很多种变形,本文主要讲解堆优化版的,其他例如线段树实现什么的请大家自行探索其实我不会

二.存图

众所周知,dijkstra是依赖于图的,所以抛开图讲dijkstra无异于耍流氓。对于刚接触的OIer来说,学习新的(指模拟链表而非邻接矩阵)存图方式是很有必要的。

前面提到,dijkstra和BFS是很像的,简单来说,它的中心思想是通过查看每条边的权值来决定是否松弛,就像做事情总得有一个顺序,我们需要在存图时记录这个路径。

void add(int x, int y, int z){
	e[++cnt].st = x, e[cnt].to = y, e[cnt].w = z;
	e[cnt].next = head[x], head[x] = cnt;
}

值得注意的,这里的st参数并无卵用,但为了方便大家理解(即start),st指起点,to指终点,w是花费,next指上一条共起点的边的终点,head[x]则指上一条边加入的边号。

三.重载运算符及优先队列

堆优化的dijkstra为什么优?就是因为有了重载运算符和优先队列。

重载运算符和优先队列可以保证新加入的边一定按照自己w的值插入合适的位置(升序排列)。

重载运算符可以写在结构体里,也可以单独拎出来写,我个人习惯后一个。

struct po{   //po数组
	int a, b;//a用来记录当前点,b记录起点到a的距离
};

bool operator < (po x, po y){
	return x.b > y.b;
}

优先队列是STL库里的函数,直接使用就行。

重载运算符是因为这个算法需要从当前没有被访问过的点中寻找一个距离起点最近的点,这个时候我们的优先队列要以po这个结构体的b元素为排序依据,也就是凭借b是从起点到a点的最短近距离进行排序。

priority_queue <po> q;

四.dijkstra主函数

当做好前面的准备工作后,就可以写dijkstra了。

void dijkstra(){
	for(int i = 1; i <= n; i++) lc[i] = 0x3f3f3f3f;
	lc[s] = 0;
	priority_queue <po> q;
	memset(vis,0,sizeof(vis));
	po st; st.a = s, st.b = 0;
	q.push(st);
	while(!q.empty()){
		po now = q.top(); q.pop();
		if(vis[now.a]) continue; vis[now.a] = 1;
		for(int i = head[now.a]; i; i = e[i].next){
			lc[e[i].to] = min(lc[e[i].to], lc[now.a] + e[i].w);
			po jia; jia.a = e[i].to; jia.b = lc[e[i].to];
			q.push(jia);
		}
	} 
}

十分通俗易懂,lc[x]代表指x到s的距离,s是起点。至于后面的循环,学过搜索的都懂,没学过搜索的学完搜索也会懂。

最后放上完整版代码:

#include<bits/stdc++.h>
using namespace std;

struct edge{
    int st, to, next, w;
}e[500005];

int n, m, s, u, v, w, cnt, lc[200005], head[200005], vis[200005];

void add(int x, int y, int z){
    e[++cnt].to = y, e[cnt].st = x, e[cnt].w = z;
    e[cnt].next = head[x], head[x] = cnt;
}

struct po{
    int a, b;
};

bool operator < (po x, po y){
    return x.b > y.b;
}

void dijkstra(){
    for(int i = 1; i <= n; i++) lc[i] = 0x3f3f3f3f;
    lc[s] = 0;
    priority_queue <po> q;
    po st; st.a = s, st.b = 0;
    q.push(st);
    while(!q.empty()){
        po now = q.top(); q.pop();
        if(vis[now.a]) continue; vis[now.a] = 1;
        for(int i = head[now.a]; i; i = e[i].next){
            if(lc[e[i].to] > lc[now.a] + e[i].w){
                lc[e[i].to] = lc[now.a] + e[i].w;
                po jia; jia.a = e[i].to, jia.b = lc[e[i].to];
                q.push(jia);
            }
        }
    }
}

int main(){
    scanf("%d%d%d", &n, &m, &s);
    for(int i = 1; i <= m; i++){
        scanf("%d%d%d", &u, &v, &w);
        add(u, v, w);
    }
    dijkstra();
    for(int i = 1; i <= n; i++){
        printf("%d ", lc[i]);
    }
    return 0;
}

以上代码可以 AC luogu 的dijkstra模板题,建议大家自己打打试试,有点想法之后便可以做几道题巩固一下。本文也只是对自己知识的总结,如有纰漏还请各位大佬指出(虽然我知道根本没人看我博客)~~

五.后记

其实dijkstra很早之前就有在学了,只是我菜学不懂,就先学了并查集。感觉并查集简单的,可能是我对BFS有点抵触心理的缘故,时至今日才完全搞懂dijkstra,去做了道题却只有30pts,决定去颓炉石,话说有小伙伴一起下棋吗?

零.后记的后记

想了想,还是把这个线段树优化的Dij放上来吧,说不定看着看着就会了呢?

#include<bits/stdc++.h>
using namespace std;

int n, m;
struct edge {
	int to, w, nxt;
	edge() {}
	edge(int t, int ww, int nn) {to = t, w = ww, nxt = nn;}
}e[maxn << 1];
 
int head[maxn], k = 0;
void add(int u, int v, int w) {e[k] = edge(v, w, head[u]); head[u] = k++;}
 
ll ans[maxn];
struct node {
	ll dis; int x;
	node() {}
	node(ll d, int xx) {dis = d, x = xx;}
}dis[maxn << 2];
 
//建树初始化,主要是编号也要返回所以要先预处理一下 
void build(int p, int l, int r) {
	if(l == r) {dis[p].x = l; return;}
	int mid = l + r >> 1;
	build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
	dis[p].x = dis[p << 1].x;
}
 
void change(int p, int l, int r, int x, int y) {
	if(l == r) {dis[p].dis = y; return;}
	int mid = l + r >> 1;
	if(x <= mid) change(p << 1, l, mid, x, y);
	else change(p << 1 | 1, mid + 1, r, x, y);//单点修改的板子操作 
	if(dis[p << 1].dis < dis[p << 1 | 1].dis) dis[p] = dis[p << 1];
	else dis[p] = dis[p << 1 | 1];
}
 
//因为用距离得到最小,但是需要的是编号,所以返回node 
node ask(int p, int l, int r, int ls, int rs) {
	if(ls <= l && r <= rs) {return dis[p];}
	int mid = l + r >> 1; node ans = node(inf, 0), tmp;
	if(ls <= mid) ans = ask(p << 1, l, mid, ls, rs);
	if(rs > mid) {
		node tmp = ask(p << 1 | 1, mid + 1, r, ls, rs);
		if(ans.dis > tmp.dis) ans = tmp;
	}
	return ans;
}
 
int S;
void dij() {
	for(int k = 1; k < n; k++) {//n-1次够用的。虽然我也不知道为什么最后n次跑的比n-1次还要快…… 
		register int u = ask(1, 1, n, 1, n).x;
		for(int i = head[u]; ~i; i = e[i].nxt) {
			register int v = e[i].to;
			if(ans[u] + e[i].w < ans[v]) {//最短路更新 
				ans[v] = ans[u] + e[i].w, change(1, 1, n, v, ans[v]);//单点修改 
			}
		}
		change(1, 1, n, u, inf);//取出来过后要赋值INF,以免再次取用 
	}
}
 
int main() {
	memset(head, -1, sizeof head);
	n = read(), m = read(), S = read();
	for(int u, v, w, i = 1; i <= m; i++) u = read(), v = read(), w = read(), add(u, v, w);
	
	//初始化 
	for(int i = 1; i <= (n << 2); i++) dis[i].dis = inf;
	for(int i = 1; i <= n; i++) ans[i] = inf;
	
	//线段树初始化,dis是线段树,ans是答案 
	build(1, 1, n);
	change(1, 1, n, S, 0); ans[S] = 0;
	dij();
	
	for(int i = 1; i <= n; i++) printf("%lld ", ans[i]);
	return 0;
}

时间和内存均是优先队列优化版本的一半 。

-------------------------------------------------------------End--------------------------------------------------------------------

posted @ 2021-04-10 03:17  许江一墨  阅读(189)  评论(0编辑  收藏  举报