将所有的顶点分为两部分:已知最短路程的顶点集合P和未知最短路径的顶点集合Q。最开始,已知最短路径的顶点集合P中只有源点一个顶点。我们这里用一个book[ i ]数组来记录哪些点在集合P中。例如对于某个顶点i,如果book[ i ]为1则表示这个顶点在集合P中,如果book[ i ]为0则表示这个顶点在集合Q中。
设置源点s到自己的最短路径为0即dis=0。若存在源点有能直接到达的顶点i,则把dis[ i ]设为e[s][ i ]。同时把所有其它(源点不能直接到达的)顶点的最短路径为设为∞。
在集合Q的所有顶点中选择一个离源点s最近的顶点u(即dis[u]最小)加入到集合P。并考察所有以点u为起点的边,对每一条边进行松弛操作。例如存在一条从u到v的边,那么可以通过将边u->v添加到尾部来拓展一条从s到v的路径,这条路径的长度是dis[u]+e[u][v]。如果这个值比目前已知的dis[v]的值要小,我们可以用新值来替代当前dis[v]中的值。
重复第3步,如果集合Q为空,算法结束。最终dis数组中的值就是源点到所有顶点的最短路径。
接下来是算法模板的部分。由于用到了“链式前向星”变相存储邻接矩阵,记忆有点模糊了的话戳讲解连接:https://blog.csdn.net/acdreamers/article/details/16902023
【dijkstra算法模板(省略主函数)】这里的注释有我自己的补充,对自己的理解很关键,看一看!
const int MAX_N = 10000; //边的max
const int MAX_M = 100000; //点的max
const int inf = 0x3f3f3f3f; //最大距离
struct edge { //v为边起点,u为边终点,w为边的权重
int v, w, next;
} e[MAX_M];
int p[MAX_N], eid , n; //p数组就是讲解中的head数组,eid就是讲解中的cnt计数用的。n是图的结点个数。
void mapinit() { //初始化图,和原来一样
memset(p, -1, sizeof(p)); //注意这里初始化-1,关系到dijkstra算法中的更新(松弛)if语句条件
eid = 0;
}
void insert(int u, int v, int w) { // 插入带权有向边,多了个权 (链式前向星存储)
e[eid].v = v;
e[eid].w = w;
e[eid].next = p[u];
p[u] = eid++;
}
void insert2(int u, int v, int w) { // 插入带权双向边
insert(u, v, w);
insert(v, u, w);
}
int dist[MAX_N]; // 存储单源最短路的结果
bool vst[MAX_N]; // 标记每个顶点是否在集合 U 中
bool dijkstra(int s) {
memset(vst, 0, sizeof(vst));
memset(dist, 0x3f, sizeof(dist));
dist[s] = 0;
for (int i = 0; i < n; ++i) {
int v, min_w = inf; // 记录 dist 最小的顶点编号和 dist 值
for (int j = 0; j < n; ++j) {
if (!vst[j] && dist[j] < min_w) {//没有访问并且满足要求
min_w = dist[j];
v = j;//更新
}
}
if (min_w == inf) { // 所有点都满足vst[v]==true了,都确定最短路长度了,而循环未结束,说明有顶点是源点无法到达的,anyway,算法该结束了
return false; //flase的意思是源点不能到达所有顶点
}
vst[v] = true; // 将顶点 v 加入集合 U 中,也就是说,源点到该顶点的最短路长度由估计值变为了确定值,存在dist[v]中。
for (int j = p[v]; j != -1; j = e[j].next) {
// 如果和 v 相邻的顶点 x 满足 dist[v] + w(v, x) < dist[x] 则更新 dist[x],这一般被称作“松弛”操作
int x = e[j].v;
if (!vst[x] && dist[v] + e[j].w < dist[x]) {//没有访问,满足提议
dist[x] = dist[v] + e[j].w;//更新
}
}
}
return true; // 源点可以到达所有顶点,算法正常结束
}
然而,其实你会发现这个算法复杂度挺高的,O(n^2),仔细一看,第一层循环肯定不能简化了,因为要求源点到每一个点的最短路长度;而第二层循环是可以简化的!因为其实遍历是为了求估计值中的最小值,把它确定下来,而如果用小根堆或者优先队列来存储dist的话,就可以直接取到最小值而无需遍历比较了!
【小根堆优化模板】
这里的小根堆是用set来伪实现的(谁去手写堆啊我c!),注意其中的元素是pair
然后还要注意对set进行插入时用到了make_pair:
然后还要注意用到了auto的“指针”去指向set.begin() (其实就是set
其他就是迪杰斯特拉算法的常规步骤了。
const int MAX_N = 10000;
const int MAX_M = 100000;
const int inf = 0x3f3f3f3f;
struct edge {
int v, w, next;
} e[MAX_M];
int p[MAX_N], eid, n;
void mapinit() {
memset(p, -1, sizeof(p));
eid = 0;
}
void insert(int u, int v, int w) { // 插入带权有向边
e[eid].v = v;
e[eid].w = w;
e[eid].next = p[u];
p[u] = eid++;
}
void insert2(int u, int v, int w) { // 插入带权双向边
insert(u, v, w);
insert(v, u, w);
}
typedef pair PII;
set > min_heap;
/*用 set 来伪实现一个小根堆,并具有映射二叉堆的功能。堆中 pair 的 second 表示顶点下标,first 表示该顶点的 dist 值,注意,只要写上less就会一first为标准排序,C++,内部实现,不需要管*/
int dist[MAX_N]; // 存储单源最短路的结果
bool vst[MAX_N]; // 标记每个顶点是否在集合 U 中
bool dijkstra(int s) {
// 初始化 dist、小根堆和集合 U
memset(vst, 0, sizeof(vst));
memset(dist, 0x3f, sizeof(dist));
min_heap.insert(make_pair(0, s));
dist[s] = 0;
for (int i = 0; i < n; ++i) {
if (min_heap.size() == 0) { // 如果小根堆中没有可用顶点,说明有顶点无法从源点到达,算法结束
return false;
}
// 获取堆顶元素,并将堆顶元素从堆中删除
auto iter = min_heap.begin(); //auto的“指针!”数据类型都用上了,为C++11打call!
int v = iter->second;
min_heap.erase(*iter);
vst[v] = true;
// 进行和普通 dijkstra 算法类似的松弛操作
for (int j = p[v]; j != -1; j = e[j].next) {
int x = e[j].v;
if (!vst[x] && dist[v] + e[j].w < dist[x]) { //对未确定的估计值进行更新。
// 先将对应的 pair 从堆中删除,再将更新后的 pair 插入堆
min_heap.erase(make_pair(dist[x], x));
dist[x] = dist[v] + e[j].w;
min_heap.insert(make_pair(dist[x], x));
}
}
}
return true; // 存储单源最短路的结果
}
优先队列优化模板的代码可见:https://blog.csdn.net/ergedathouder/article/details/52439438 (会一个就行了,原理都一样,最多代码填空可能考到这个)