最短路问题2:Dijkstra算法及其堆优化

今天研究Dijkstra算法及其优化

前言:

在竞赛中,单源最短路的话,就算是使用SPFA算法,也可能被神奇的数据卡成复杂度接近O(n * m)的,那么就GG了……所以我们主要学习Dijkstra算法(复杂度在:O(n^2))及其堆优化(优化后复杂度在:O(n * logn))。

分析Dijkstra算法

算法思想建模:

它的算法思想是贪心,咱们可以这样去理解:假设起点是 1号节点,我们要求出从 1号节点出发,到其他节点的最短路径,现在我们在每条邻接边上摆放多米诺骨牌,假设权重越大的边上摆放的骨牌数目越多,并且规定骨牌的倒下速度是一样的,那么也就是说骨牌数目越多倒下的时间越长,我没以起点 1号节点开始推到骨牌,很明显,骨牌少的那条边相邻的节点 u 是最先到达的,最先到达的节点与起点之间的路径必然是最短路径,如果不是最短路径,则可以找到更短的路径更先到达,然后 u 就可以算作已经确定最短路径的节点了。然后,此时倒塌的多米诺骨牌就分成了两路:一路还是从起点 1 开始继续倒塌,另一路就是从新的已经确定的节点 u 开始倒塌,然后更新已经确定最短路的节点集合A与未确定最短路径的节点集合B之间的直连路径,然后不断反复这个过程。

最短路问题2:Dijkstra算法及其堆优化

算法步骤分析:

以上图为例子:先设出两个集合A和B,分别为已经确定最短路径的点的集合,另一个为A集合中的节点的相邻节点,先假设以 1号节点为起点
第零步:A{1} B{2 3 4}

第一步:A{1 4} B{2 3 6}

第二步:A{1 2 4} B{5 3 6}

……
直到所有节点的都在A集合中

Dijkstra算法的核心代码:

int Dijkstra(int s, int t) {
	for (int i = 0; i <= n; ++i)
		dis[i] = inf;
	dis[s] = 0;
	for (int i = 0; i < n; ++i) {
		int u = 0;
		for (int j = 1; j <= n; ++j)
			if (!vis[j] && (!u || dis[u] > dis[j]))
				u = j;
		vis[u] = true;
		for (int k = 0; k < G[u].size(); k++) {
			int v = G[u][k].to, w = G[u][k].cost;
			if (dis[v] > dis[u] + w)
				dis[v] = dis[u] + w;
		}
	}
	return dis[t];
}

算法习题:洛谷 P1576 最小花费,也是中南大学保研上机考试的题

算法分析:

这个题乍一看不太像求最短路的问题,但是看数据规模,用DFS肯定会超时,不过神奇的是,在dengdengoj上提交是可以用DFS解决掉的(肯定是数据水,但是洛谷数据不水)
这个题说每次经过一个点,都要交手续费,那么怎么才能用最小的花费让对方收到100的金额?我们假设经过第 i 条边需要交付 zi%的手续费,也就是说,真正对方收到的是我付出的金额:MyMoney * (100 - zi)%的金额
假设要经过k条边,那么我们这个题要满足的公式就是:
MyMoney * PI((100 - zi)/100) = 100
我们要想MyMoney尽量少,那么这个 PI(100 - zi)/100 就要尽量大!
于是就变成了最大路径问题,这里注意初始化:dis[s] = 1(重要!!)
因为我们自己转给自己是不要手续费的,于是就是100/100=1,如果不这样初始化的话,由于我们是乘法,其他乘起来都是0了!而其他的点,暂时还没有连接,全都初始化为0,因为最开始,无法互通,也就是多少钱都汇不过去!

Dijkstra的AC代码:

#include <cstdio>
#include <vector>
using namespace std;

const int maxp = 2005;
struct Edge{
	int to;
	double cost;
	Edge(int t, double c) : to(t), cost(c) {}
};
int n, m, vis[maxp], a, b;
double dis[maxp], c;
vector<Edge> G[maxp];
void Solve(int s){
	dis[s] = 1.0;
	for(int i = 0;i < n;++i){
		int u = 0;
		for(int j = 1;j <= n;++j)
			if(!vis[j] && (!u || dis[j] > dis[u]))
				u = j;
		vis[u] = 1;
		for(int k = 0;k < (int)G[u].size();++k)
			if(dis[G[u][k].to] < dis[u] * G[u][k].cost)
				dis[G[u][k].to] = dis[u] * G[u][k].cost;
	}
}

int main(){
	scanf("%d %d", &n, &m);
	while(m--){
		scanf("%d %d %lf", &a, &b, &c);
		G[a].push_back(Edge(b, (100 - c) / 100.0));
		G[b].push_back(Edge(a, (100 - c) / 100.0));
	}
	int s, t;
	scanf("%d %d", &s, &t);
	Solve(s);
	printf("%.8lf", 100.0 / dis[t]);
	return 0;
}

在dengdengoj上的提交:2019中南大学保研夏令营

在中南这个oj的DFS算法AC代码:

#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>
#include <vector>
using namespace std;

const int maxp = 2005;
struct Edge {
	int to, cost;
	Edge(int t, int c) : to(t), cost(c) {}
};
int n, m, a, b, s, t, vis[maxp];
vector<Edge> G[maxp];
double ans = 9.9999999999, c;
void dfs(int p, double cnt) {
	if (t == p) {
		if (ans * 10000000000.0 > cnt * 10000000000.0)
			ans = cnt;
		return;
	}
	for (int i = 0; i < G[p].size(); ++i) {
		int v = G[p][i].to;
		if (!vis[v]) {
			vis[v] = 1;
			dfs(v, cnt * 100.0 / (100 - G[p][i].cost));
			vis[v] = 0;
		}
	}
}

int main() {
	scanf("%d %d", &n, &m);
	while (m--) {
		scanf("%d %d %lf", &a, &b, &c);
		G[a].push_back(Edge(b, c));
		G[b].push_back(Edge(a, c));
	}
	scanf("%d %d", &s, &t);
	vis[s] = 1;
	dfs(s, 1.0);
	printf("%.8lf", ans * 100);
	return 0;
}

Dijkstra算法的堆优化:

为什么要堆优化呢???
首先我们来分析一下Dijkstra算法的复杂度:
我们要处理N个点,每次呢要在集合B中找到最优的点作为下一个点去更新集合A,如果集合B是乱序的,那么每次更新集合A就要去遍历集合B,集合B的最坏情况是包含了N个点,所以复杂度是O(NM)
如果设想B集合是有序的,那么每次找到最优的点就只需要找第一个元素就好了,有没有高效的算法或者是数据结构呢?其实有很多,平衡树、线段树都可以,但是代码量最短的肯定是运用STL包含的模板类:优先队列就可以了!
可以达到复杂度:O(M
logN)

算法实现模板题:堆优化Dijkstra算法模板题

AC代码:

#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>
#include <vector>
#include <queue>
using namespace std;

const int maxn = 1e5 + 5;
struct Edge {
	int to, cost;
	Edge(int t, int c) : to(t), cost(c) {}
};
vector<Edge> G[maxn];
struct Node {
	int N_id, N_dis;
	Node(int id, int di) : N_id(id), N_dis(di) {}
	bool operator< (const Node& n) const {
		return N_dis > n.N_dis;
	}
};
int n, m, a, b, c, s, vis[maxn], dis[maxn];
void Dijkstra() {
	for (int i = 0; i <= n; ++i)	dis[i] = 0x3f3f3f3f;
	dis[s] = 0;
	priority_queue<Node> q;
	q.push(Node(s, dis[s]));
	while (!q.empty()) {
		Node Now = q.top(); q.pop();
		if (vis[Now.N_id])	continue;
		vis[Now.N_id] = 1;
		for (int j = 0; j < (int)G[Now.N_id].size(); ++j) {
			Edge e = G[Now.N_id][j];
			if (vis[e.to])	continue;
			if (dis[e.to] > e.cost + dis[Now.N_id]) {
				dis[e.to] = e.cost + dis[Now.N_id];
				q.push(Node(e.to, dis[e.to]));
			}
		}
	}
}


int main() {
	scanf("%d %d %d", &n, &m, &s);
	while (m--) {
		scanf("%d %d %d", &a, &b, &c);
		G[a].push_back(Edge(b, c));
	}
	Dijkstra();
	for (int i = 1; i <= n; ++i)
		if (dis[i] < 0x3f3f3f3f)
			printf("%d ", dis[i]);
		else
			printf("2147483647 ");
	return 0;
}

从算法上进一步细致分析堆优化:

这里我们不再明确的维护一个“确定最短路径的点”的一个集合A了,而是只是去研究集合B,因为图的边有M条,每次就需要向集合B拓展M次,因为要更新B集合中的连接状态,所以我们维护集合B其实也就隐性地维护了集合A!然而我们每次都要从集合B中找到最优的状态作为下一个确定最短路的节点。这个是可以用优先队列O(1)获取,O(logn)维护的。但是这就牵扯到入队和出队的问题,我们分析一下:
最开始,连起点都在集合B中,所以最初优先队列只有一个点:起点。然后逐步扩展,在扩展的时候,就自动维护了,让最短(最优)的节点放在队首,然后依次往后。每次队首出队(意味着被确定为新的最短路径的点),那么这个队首的邻接的点的距离就需要被更新,这个更新不光是距离的更新,还要把他们入队!因为我们可以知道,节点 u 的所有邻接的节点比不邻接的节点近(没有负权边的时候一定是这样的!)所以他们必须入队作为下一波的候选节点!

优化之后,复杂度:O(M*logN),但是用其他更高级的数据结构优化能够有更高的效率,在有些数据中,这种效率优势相当明显,等小良水平到了那个层次再和大家分享!

上一篇:HDU - 6005 Pandaland (无向图最小环,动态加边Dijkstra)


下一篇:路径优化算法研究