dijkstra算法+堆优化(HYSBZ - 3040 手写配对堆) 详解

dijkstra算法是一种最短路径算法
用于计算单源最短路径,即从一个源点出发,到图中其他所有点的最短路径
要求是所有边的权值都为

过程
令源点为s

  1. 建立两个数组dis,vis。dis[i]表示从源点出发到编号为i的点的距离 (初始时dis[s]=0,其他所有点的dis值为无穷大,在计算过程中,如果找到一条到达点 i 的更短的路径,dis[i]将更新为这条更短路径的距离) vis[i]表示 i 点的dis[i] 是否为真正的最短路径(vis[i]==true 表示 i 点的dis值已经确定是最小值,不能更短了)初始时所有点的vis值都为false。

  2. 每一轮中挑选出所有 vis 值为false的点中dis值最小的那一个,令为点 u ,这时可以保证 u 点的dis值为最短的路径,将其vis值更改为true。

  3. 更新点 u 周围的点的dis值,规则是:如果存在一条从 u 到 v 的权值为 w 的边,则我们可以找到一条路径从源点到u点,再经过这条边到达 v 点的路径,且其长度为dis[u]+w,如果比现在的dis[v]小的话,那么就找了一条更短的路径,更新dis[v]的值

  4. 重复过程2、3,直至所有点的vis值都为true。

证明
简单的口述证明一下 (懂的同学也看看,有些名词后面要用)

首先我们可以将图中的点分为三类

  1. vis为true的点,称为“内部点
  2. vis为false,但是dis!=正无穷的点 称为边界点
  3. dis==正无穷的点,称为“外部点
    显然,内部点不可能与外界点相连(相连指从内部点到外部点的边)

实际上,上述过程2中每次挑选的点 u 都是边界点,因为外部点的dis值为无穷大
到达点 u 的真正最短路径首先必经过一些内部点(点 u 为源点除外) ,如果经过了边界点 k,那么这条路的长度必然大于dis[k](因为所有边的权值都大于零,路径只可能越走越长),而dis[u]是所有边界点中最短的,即dis[u]<=dis[k] 所以到达 u 的最短路径不可能经过其他边界点。

其实上述证明不够严谨,因为dis[k]不一定是最短路径,可能之后dis[k] 可能更新为更小的值,但是更新后的值也一定大于dis[u] 因为每次跟更新都是由边界点向外更新(之后才把边界点纳入内部点)而dis[u]已经是边界点中最小的了,之后更新的值也肯定比dis[u]更大。

堆优化

我们已经知道原理,接下来要用算法实现,其中最重要的就是,怎么才能在步骤2中快速挑选出边界点中dis值最小的那个,堆正适合这项工作(不懂的同学先去别的地方看看堆是什么)既能在O(1)时间内找出最小值,又能在O(logn)时间将被更新的点的dis值加入到堆中排序。

代码
首先贴一下我的宏

#define fors(x,n) for(int x = 0,end = n; x < end; x++)

再贴一下我存图的代码

#define MAX_NODE 1000000+20
#define MAX_EDGE 10000000+20
int edge_cnt = 0;
int head[MAX_NODE];
Edge edges[MAX_EDGE];
void init() {
	memset(head, 0, sizeof head);
	edge_cnt = 0;
}
void add_edge(int from, int to, int weight) {
	edges[++edge_cnt] = Edge(weight, to, head[from]);
	head[from] = edge_cnt;
}

向前星,不懂的可以搜搜,一种类链式存储结构

接下来是dijkstra的代码

bool vis[MAX_NODE];
int dis[MAX_NODE];
int dijkstra(int s, int t) {
	fors(i, MAX_NODE) {
		vis[i] = false;
		dis[i] = INT_MAX;
	}
	dis[s] = 0;
	priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > > heap;
	heap.push(make_pair(0, s));
	while (true) {
		int u = heap.top().second;
		heap.pop();
		if (u == t) return dis[t];
		if (vis[u]) continue;
		vis[u] = true;
		for (int i = head[u]; i; i = edges[i].next) {
			int v = edges[i].to;
			if (dis[v] > dis[u] + edges[i].weight) {
				dis[v] = dis[u] + edges[i].weight;
				heap.push(make_pair(dis[v], v));
			}
		}
	}
}

简单说明一下

s为源点,t为终点,u、v含义同上述

堆使用了c++自带的priority_queue,因为标准库默认是最大堆(太坑了T^T),所以要自己设置比较结构体
这里直接用了greater>(又是一个坑点,大于比较是最小堆,真是反人类。。。),因为pair本身重载了 > 操作符,先比较 first 值再比较 second 值,这里就把dis放入first,点的标号放入second,堆就会自动对dis排序了

首先将源点设为边界点dis设为0,其余点为外部点。
将所有边界点加入堆中,每次取堆顶元素,注意,堆顶元素可能已经不是边界点而是内部点了,这是因为每个点的dis值可能会更新多次,每次都会入堆,最小的那个dis值最先出堆,出堆时更新其周围点的dis值,并将其加入到内部点中,之后出堆的就不用管了,直接continue.

这里只要找到终点的最短路径就提前退出了,如果想求所有点的最短路径的话,可以改成这样

bool vis[MAX_NODE];
int dis[MAX_NODE];
void dijkstra(int s) {
	fors(i, MAX_NODE) {
		vis[i] = false;
		dis[i] = INT_MAX;
	}
	dis[s] = 0;
	priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > > heap;
	heap.push(make_pair(0, s));
	while (!heap.empty()) {
		int u = heap.top().second;
		heap.pop();
		if (vis[u]) continue;
		vis[u] = true;
		for (int i = head[u]; i; i = edges[i].next) {
			int v = edges[i].to;
			if (dis[v] > dis[u] + edges[i].weight) {
				dis[v] = dis[u] + edges[i].weight;
				heap.push(make_pair(dis[v], v));
			}
		}
	}
}

算法结束后取dis[i]即为s到i最短路径

还没完!!!!
上述算法中,如果一个点的dis值被更新多次,那么它的dis值会被多次入堆。。
多次就多次呗,咱也不差那点内存。。
但是!!!!
就是又这种丧心病狂的题卡你内存。。。
比如这题。。。HYSBZ - 3040 链接

可以避免重复入堆吗?可以的,但是标准库不支持,所以要自己写
这里我写了一个配对堆,即如果一个点在堆中,直接在堆里更改它的dis值,并重排堆元素,时间也是O(logn)的,贴上代码:

template<typename V, typename C = std::less<V> >
class my_heap {
	typedef int Key;
	typedef pair<Key, V> node;
	deque<node> heap;
	int* poses;
	C cmp;
	void move(int from, int to) {
		heap[to] = heap[from];
		poses[heap[from].first] = to;
	}
	void update_down(int pos) {
		node temp = heap[pos];
		for (int i = pos * 2 + 1, end = heap.size(); i < end; pos = i, i = i * 2 + 1) {
			if (i + 1 < end && cmp(heap[i].second, heap[i + 1].second))i++;
			if (cmp(temp.second, heap[i].second)) move(i, pos);
			else break;
		}
		poses[temp.first] = pos;
		heap[pos] = temp;
	}
	void update_up(int pos) {
		node temp = heap[pos];
		for (int i = (pos + 1) / 2 - 1; i >= 0; pos = i, i = (i + 1) / 2 - 1) {
			if (cmp(heap[i].second, temp.second)) move(i, pos);
			else break;
		}
		poses[temp.first] = pos;
		heap[pos] = temp;
	}
public:
	my_heap(int key_cnt) {
		poses = new int[key_cnt + 1];
		for (int i = 0; i <= key_cnt; i++)
			poses[i] = -1;
	}
	~my_heap() { delete[] poses; }
	bool has(Key key) { return poses[key] != -1; }
	void push(Key key, const V & val) {
		heap.push_back(make_pair(key, val));
		poses[key] = heap.size() - 1;
		update_up(heap.size() - 1);
	}
	const node& top() { return heap[0]; }
	bool empty() { return heap.empty(); }
	void pop() {
		int temp_key = heap[0].first;
		move(heap.size() - 1, 0);
		poses[temp_key] = -1;
		heap.pop_back();
		if (!empty()) update_down(0);
	}
	void update(Key key, const V & new_val) {
		int pos = poses[key];
		heap[pos].second = new_val;
		update_up(pos);
		update_down(pos);
	}
	const V& get_val(Key key) { return heap[poses[key]].second; }
};

简单说明一下功能:
这里入堆使用push函数,第一个参数为key,第二个参数为value,堆中会对value值进行排序,默认最大堆,如果要用最小堆请传入大于比较结构体(跟标准库统一。。)
构造函数的参数为key的数量key_cnt,之后传入的key的值只能是 [0,key_cnt] 区间的整数(散列表或者key不是int型的话在可以在堆外部建立一个hash表来转化为key)
这里key即为点的标号
使用has函数查询key是否在堆中
如果保证一个key在堆中那么可以直接使用update函数在堆中更改相应的value。
get_val函数用来获得堆中key的value,前提也是key在堆中

于是dijkstra可以这样写

int pre[MAX_NODE];
int dijkstra(int s, int t) {
	fors(i, MAX_NODE) pre[i] = -1;
	pre[s] = s;
	my_heap<int, greater<int> > heap(MAX_NODE);
	heap.push(s, 0);
	while (true) {
		pair<int, int> top = heap.top();
		heap.pop();
		if (top.first == t) return top.second;
		for (int i = head[top.first]; i; i = edges[i].next) {
			int to = edges[i].to;
			if (heap.has(to) && heap.get_val(to) > top.second + edges[i].weight) {
				heap.update(to, top.second + edges[i].weight);
				pre[to] = top.first;
			}
			else if (pre[to] == -1) {
				heap.push(to, top.second + edges[i].weight);
				pre[to] = top.first;
			}
		}
	}
}

dis数组和vis数组不见了!
取而代之的是一个pre数组,pre[i]=k 表示目前的所有到点 i 的路径中,最短的一条的最后一步是从 k 到 i 的一条边
那么如何分辨三类点呢??

  1. 外界点的pre值为-1
  2. 边界点的pre值不为-1,且一定在堆中
  3. 内部点的pre值不为-1,且一定不在堆中

每个点的dis值都暂存于堆中,出堆时会丢失,也就是说出堆时是确定某个点的最短路径的唯一时机,这时如果时终点话,直接返回

当然也可以使用dis+vis的方法,这么做的优点是:可以确定最短路径具体是经过那些店到达终点的
只要从终点出发,沿着pre数组一直向前找,直至找到源点就行了。

下面是整题的ac代码

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace::std;
#define ll long long
#define fors(x,n) for(int x = 0,end = n; x < end; x++)


template<typename V, typename C = std::less<V> >
class my_heap {
	typedef int Key;
	typedef pair<Key, V> node;
	deque<node> heap;
	int* poses;
	C cmp;
	void move(int from, int to) {
		heap[to] = heap[from];
		poses[heap[from].first] = to;
	}
	void update_down(int pos) {
		node temp = heap[pos];
		for (int i = pos * 2 + 1, end = heap.size(); i < end; pos = i, i = i * 2 + 1) {
			if (i + 1 < end && cmp(heap[i].second, heap[i + 1].second))i++;
			if (cmp(temp.second, heap[i].second)) move(i, pos);
			else break;
		}
		poses[temp.first] = pos;
		heap[pos] = temp;
	}
	void update_up(int pos) {
		node temp = heap[pos];
		for (int i = (pos + 1) / 2 - 1; i >= 0; pos = i, i = (i + 1) / 2 - 1) {
			if (cmp(heap[i].second, temp.second)) move(i, pos);
			else break;
		}
		poses[temp.first] = pos;
		heap[pos] = temp;
	}
public:
	my_heap(int key_cnt) {
		poses = new int[key_cnt + 1];
		for (int i = 0; i <= key_cnt; i++)
			poses[i] = -1;
	}
	~my_heap() { delete[] poses; }
	bool has(Key key) { return poses[key] != -1; }
	void push(Key key, const V & val) {
		heap.push_back(make_pair(key, val));
		poses[key] = heap.size() - 1;
		update_up(heap.size() - 1);
	}
	const node& top() { return heap[0]; }
	bool empty() { return heap.empty(); }
	void pop() {

		int temp_key = heap[0].first;
		move(heap.size() - 1, 0);
		poses[temp_key] = -1;
		heap.pop_back();
		if (!empty()) update_down(0);
	}
	void update(Key key, const V & new_val) {
		int pos = poses[key];
		heap[pos].second = new_val;
		update_up(pos);
		update_down(pos);
	}
	const V& get_val(Key key) { return heap[poses[key]].second; }
};

struct Edge {
	int weight;
	int to;
	int next;
	Edge(int weight, int to, int next) :weight(weight), to(to), next(next) {}
	Edge() = default;
};
#define MAX_NODE 1000000+20
#define MAX_EDGE 10000000+20
int edge_cnt = 0;
int head[MAX_NODE];
Edge edges[MAX_EDGE];
void init() {
	memset(head, 0, sizeof head);
	edge_cnt = 0;
}
void add_edge(int from, int to, int weight) {
	edges[++edge_cnt] = Edge(weight, to, head[from]);
	head[from] = edge_cnt;
}

int pre[MAX_NODE];
int dijkstra(int s, int t) {
	fors(i, MAX_NODE) pre[i] = -1;
	pre[s] = s;
	my_heap<int, greater<int> > heap(MAX_NODE);
	heap.push(s, 0);
	while (true) {
		pair<int, int> top = heap.top();
		heap.pop();
		if (top.first == t) return top.second;
		for (int i = head[top.first]; i; i = edges[i].next) {
			int to = edges[i].to;
			if (heap.has(to) && heap.get_val(to) > top.second + edges[i].weight) {
				heap.update(to, top.second + edges[i].weight);
				pre[to] = top.first;
			}
			else if (pre[to] == -1) {
				heap.push(to, top.second + edges[i].weight);
				pre[to] = top.first;
			}
		}
	}
}

int main() {
	ll n, m, t, rxa, rxc, rya, ryc, rp, x, y, z, a, b;
	cin >> n >> m >> t >> rxa >> rxc >> rya >> ryc >> rp;
	x = y = z = 0;
	init();
	fors(i, t) {
		x = (x * rxa + rxc) % rp;
		y = (y * rya + ryc) % rp;
		a = min(x % n, y % n);
		b = max(x % n, y % n);
		add_edge(a, b, 1e8 - 100 * (a+1));
	}
	fors(i, m - t) {
		scanf("%lld%lld%lld", &x, &y, &z);
		add_edge(x - 1, y - 1, z);
	}
	cout << dijkstra(0, n - 1) << endl;
}

你可能感兴趣的:(图论)