这是一篇经过翻新的文章,最早写于 2015-11-19 23:44:00,那是一个风雨交加的夜晚,是我逝去的青春!
本文比原文增加了更多图解,并且将示例代码写得更加通俗易懂,争取把每个函数控制在五到十行,以前一些描述的较为晦涩的内容也进行了改革,有兴趣涉足图论的小伙伴可以从这篇文章进行入门,如有描述不清或者错误的地方也希望能加以指正,共同成长,齐头并进!
【例题1】给定四个小岛以及小岛之间的有向距离,问从第 0 个岛到第 3 个岛的最短距离。如图二-1-1所示,带箭头的线段代表两个小岛之间的有向边,箭头上的数字代表有向边的权值。
vector<Edge> edges[maxn];
edge[maxm]
,maxm
表示边的总数,所有边都存储在这个结构体数组中,并且用head[i]
来指向 i i i 结点的第一条边。struct Edge {
int u, v, w, next;
Edge() {
}
Edge(int _u, int _v, int _w, int _next) :
u(_u), v(_v), w(_w), next(_next)
{
}
}edge[maxm];
head[i] = -1
,当前边总数 edgeCount = 0
;addEdge(u, v, w)
,具体函数的实现如下:void addEdge(int u, int v, int w) {
edge[edgeCount] = Edge(u, v, w, head[u]);
head[u] = edgeCount++;
}
head[i]
就能访问到由 i i i 出发的第一条边的编号,通过编号到edge
数组进行索引可以得到边的具体信息,然后根据这条边的next
域可以得到第二条边的编号,以此类推,直到 next
域为 -1 为止。for (int e = head[u]; ~e; e = edges[e].next) {
int v = edges[e].v;
ValueType w = edges[e].w;
...
}
~e
等价于 e != -1
,是对e
进行二进制取反的操作(-1 的的补码二进制全是 1,取反后变成全 0,这样就使得条件不满足跳出循环)。【例题1】给出一个 n ( n ≤ 1000 ) n(n \le 1000) n(n≤1000) 个结点, m ( m ≤ 10000 ) m(m \le 10000) m(m≤10000) 条边的有向图,边权都为正数,求从编号 0 到 n n n-1 的最短路。
具体算法描述如下:对于图 G = < V , E > G =
G=<V,E>,源点为 s s s, d [ i ] d[i] d[i] 表示 s s s 到 i i i 的最短路, v i s i t e d [ i ] visited[i] visited[i] 表示 d [ i ] d[i] d[i] 是否已经确定(布尔值),即 s s s 到 i i i 的最短路 是否已经确定。
1) 初始化 所有顶点 d [ i ] = i n f d[i] = inf d[i]=inf, v i s i t e d [ i ] = f a l s e visited[i] = false visited[i]=false,令 d [ s ] = 0 d[s] = 0 d[s]=0;
2) 从所有 v i s i t [ i ] visit[i] visit[i] 为 f a l s e false false 的顶点中找到一个 d [ i ] d[i] d[i] 值最小的,令 x = i x = i x=i;如果找不到,算法结束;
3) 标记 v i s i t e d [ x ] = t r u e visited[x] = true visited[x]=true, 更新和 x x x 直接相邻的所有顶点 y y y 的最短路: d [ y ] = min ( d [ y ] , d [ x ] + w ( x , y ) ) d[y] = \min( d[y], d[x] + w(x, y) ) d[y]=min(d[y],d[x]+w(x,y));回到 2);
void Dijkstra(int n, int st, ValueType *dist) {
// 1)
Dijkstra_Init(n, st, dist); // 2)
while (true) {
int u = Dijkstra_FindMin(n, dist); // 3)
if (u == inf) break;
Dijkstra_Update(u, dist); // 4)
}
}
n
代表结点个数,st
代表起点,dist[]
用来存储当前最短路的数组,即 dist[i]
代表 初始点 st
到 i
的最短路;Dijkstra_Init
对算法进行初始化:dist[i]
当且仅当 i = s t i=st i=st 时取 0,否则为无穷大,visited[i]
标记所有点都未被访问过,时间复杂度 O ( n ) O(n) O(n),c++代码实现如下:void Dijkstra_Init(int n, int st, ValueType *dist) {
for (int i = 0; i < n; ++i) {
dist[i] = (st == i) ? 0 : inf;
visited[i] = false;
}
}
Dijkstra_FindMin
返回尚未访问过的离起点最近的点 u u u,如果不存在则返回 INVALID_INDEX
,这个函数最多执行 n − 1 n-1 n−1 次,所以总的时间复杂度 O ( n 2 ) O(n^2) O(n2),c++ 代码实现如下:int Dijkstra_FindMin(int n, ValueType *dist) {
int u = INVALID_INDEX;
for (int i = 0; i < n; ++i) {
if (visited[i])
continue;
if (u == INVALID_INDEX || dist[i] < dist[u]) {
u = i;
}
}
return u;
}
Dijkstra_Update
用点 u u u 更新和它相邻点的最短路,因为 u u u 只会被访问一次(访问完被visited
数组标记后下次不会再访问到),所以所有更新操作的时间复杂度总和等于总边数,时间复杂度 O ( m ) O(m) O(m),c++ 代码实现如下:void Dijkstra_Update(int u, ValueType *dist) {
visited[u] = true;
for (int e = head[u]; ~e; e = edges[e].next) {
int v = edges[e].v;
int w = edges[e].w;
dist[v] = min(dist[v], (dist[u] + w));
}
}
【例题2】给出一个 n ( n ≤ 100000 ) n(n \le 100000) n(n≤100000) 个结点, m ( m ≤ 100000 ) m(m \le 100000) m(m≤100000) 条边的有向图,边权都为正数,求从编号 0 到 n n n-1 的最短路。
Dijkstra_FindMin
是在 n n n 个点中找 d i s t [ i ] dist[i] dist[i] 最小的点,并且最多找 n n n 次,所以算法的瓶颈是在这里,如果有一种数据结构,能够在 O ( l o g 2 n ) O(log_2n) O(log2n) 或者 O ( 1 ) O(1) O(1) 的时间复杂度下,支持 插入元素、询问最小值、删除最小值 的操作,那么这个问题就能很好的被解决了。void DijkstraHeap(int n, int st, ValueType *dist) {
Heap heap; // 1)
Dijkstra_Init(n, st, heap, dist); // 2)
while (!heap.empty()) {
int u = Dijkstra_FindMin(heap); // 3)
Dijkstra_Update(u, heap, dist); // 4)
}
}
Dist
,并且用距离作为排序关键字,重载小于运算符,c++ 实现如下:struct Dist {
int u;
ValueType w;
Dist(){
}
Dist(int _u, ValueType _w) : u(_u), w(_w) {
}
bool operator < (const Dist& d) const {
return w > d.w; // 小顶堆
}
};
typedef priority_queue<Dist> Heap;
Dijkstra_Init
需要比之前的实现多一行代码,即往堆插入一个起点,c++ 代码实现如下:void Dijkstra_Init(int n, int st, Heap& heap, ValueType *dist) {
for (int i = 0; i < n; ++i) {
dist[i] = (st == i) ? 0 : inf;
visited[i] = false;
}
heap.push(Dist(st, 0));
}
Dijkstra_FindMin
函数实现就很简单了,直接调用堆的 API,弹出堆顶元素,c++ 代码实现如下:int Dijkstra_FindMin(Heap& heap) {
Dist s = heap.top();
heap.pop();
return s.u;
}
Dijkstra_Update
的实现多了一步插入堆的操作,c++ 代码实现如下:void Dijkstra_Update(int u, Heap& heap, ValueType *dist) {
if (visited[u]) {
return;
}
visited[u] = true;
for (int e = head[u]; ~e; e = edges[e].next) {
int v = edges[e].v;
int w = edges[e].w;
dist[v] = min(dist[v], (dist[u] + w));
if (dist[u] + w == dist[v])
heap.push(Dist(v, dist[v]));
}
}
heap.top()
的时间复杂度为 O ( 1 ) O(1) O(1),heap.pop()
的时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n),Dijkstra_FindMin
函数弹出元素的数量级是 O ( n ) O(n) O(n) ,所以这部分的时间复杂度就是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。Dijkstra_Update
这个函数,每次取距离最小的点,对于有多个相同点的情况,如果那个点已经出过一次队列了,下次同一个点出队列的时候它对应的距离一定比之前的大,不需要用它去更新其它点,因为一定不可能更新成功,所以真正执行更新操作的点的个数其实只有 n n n 个,即每个点的每条出边只会被执行一次,时间复杂度就是 O ( m ) O(m) O(m) ,head.push
是 O ( l o g 2 n ) O(log_2n) O(log2n)的,所以这个函数的总时间复杂度为 O ( m l o g 2 n ) O(mlog_2n) O(mlog2n)。【例题3】 n ( n ≤ 500 ) n (n \le 500) n(n≤500) 个点 和 m ( m ≤ 3000 ) m (m \le 3000) m(m≤3000) 条边的图上,边有两种类型,一种是正常的路,一种是虫洞,正常的路行走时花费时间,虫洞行走时能让时间倒退,问是否存在某个点出发,并且在过去的某个时间回到该点。
具体算法描述如下:对于图 G = < V , E > G =
G=<V,E>,源点为 s s s, d [ i ] d[i] d[i] 表示 s s s 到 i i i 的最短路。
1) 初始化 所有顶点 d [ i ] = i n f d[i] = inf d[i]=inf, 令 d [ s ] = 0 d[s] = 0 d[s]=0,计数器 c = 0 c = 0 c=0;
2) 枚举每条边 w ( u , v ) w(u, v) w(u,v),如果 d [ u ] ≠ i n f d[u] \neq inf d[u]=inf ,则更新 d [ v ] = min ( d [ v ] , d [ u ] + w ( u , v ) ) d[v] = \min(d[v], d[u] + w(u, v)) d[v]=min(d[v],d[u]+w(u,v));
3) 计数器 c c c 自增 1,当 c = n − 1 c = n - 1 c=n−1 时算法结束,否则继续重复2)的步骤;
bool BellmanFord(int n) {
BellmanFordInit(n); // 1)
for (int i = 0; i < n - 1; ++i) {
if (!BellmanFordUpdate()) {
// 2)
return false;
}
}
return BellmanFordUpdate(); // 3)
}
n
代表了结点总数,BellmanFordInit
对整个图进行初始化,假设有个虚拟起始点,并且它到所有点的距离都为 0,c++ 代码实现如下:void BellmanFordInit(int n) {
for (int i = 0; i < n; ++i) {
dist[i] = 0;
}
}
BellmanFordUpdate
是执行上文提到的松弛操作,这里有一个小优化,因为每次迭代都是做同一件事情,也就是说如果第 k ( k ≤ n − 1 ) k(k \le n-1) k(k≤n−1) 次迭代的时候没有任何的最短路发生更新,即所有的 d [ i ] d[i] d[i] 值都未发生变化,那么第 k + 1 k+1 k+1 次必定也不会发生变化了,也就是说这个算法提前结束了。所以可以在开始的时候记录一个flag
标志,标志初始为 false
,如果有一条边发生了松弛,那么标志置为 true
,所有边枚举完毕如果标志还是 false
则提前结束算法。“松弛” 操作总共迭代 n − 1 n-1 n−1 次,理论上如果没有负权圈的话,函数应该会在某次 BellmanFordUpdate()
返回 false
,c++ 代码实现如下:bool BellmanFordUpdate() {
bool flag = false;
for (int i = 0; i < edgeCount; ++i) {
Edge &edge = edges[i];
if (dist[edge.u] + edge.w < dist[edge.v]) {
flag = true;
dist[edge.v] = dist[edge.u] + edge.w;
}
}
return flag;
}
类似 Bellman-Ford 的做法,我们用数组 d [ i ] d[i] d[i] 记录每个结点的最短路径估计值,并用链式前向星来存储图 G G G。
1) 利用一个先进先出的队列用来保存待松弛的结点,每次取出队首结点 u u u,并且枚举从 u u u 出发的所有边 ( u , v ) (u, v) (u,v),更新 d [ v ] = min ( d [ v ] , d [ u ] + w ( u , v ) ) d[v] = \min(d[v], d[u] + w(u, v)) d[v]=min(d[v],d[u]+w(u,v))
2) 然后判断 v v v 点在不在队列中,如果不在就将 v v v 点放入队列。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
bool SPFA(int n, int st, ValueType *dist) {
queue <int> que;
SPFAInit(n, st, que, dist); // 1)
while (!que.empty()) {
int u = que.front();
que.pop();
inqueue[u] = false; // 2)
if (visited[u] ++ > n) {
// 3)
return true;
}
SPFAUpdate(u, que, dist); // 4)
}
return false;
}
dist[u]
代表起点到结点 u u u 的最短路估值,visited[u]
代表结点 u u u 的出队次数,inqueue[u]
是个布尔数组,代表结点 u u u 是否在队列中。并且将起点插入到队列中,c++代码实现如下:void SPFAInit(int n, int st, queue <int>& que, ValueType *dist) {
for (int i = 0; i < n; ++i) {
dist[i] = (st == i) ? 0 : inf;
inqueue[i] = (st == i);
visited[i] = 0;
}
que.push(st);
}
inqueue[u] = false
;SPFAUpdate
进行松弛操作,并且将松弛的点继续入队,c++代码实现如下:void SPFAUpdate(int u, queue <int>& que, ValueType *dist) {
for (int e = head[u]; ~e; e = edges[e].next) {
int v = edges[e].v;
if (dist[u] + edges[e].w < dist[v]) {
dist[v] = dist[u] + edges[e].w;
if (!inqueue[v]) {
inqueue[v] = true;
que.push(v);
}
}
}
}
void FloydWarshall(int n) {
int i, j, k;
for (k = 0; k < n; ++k)
for (i = 0; i < n; ++i)
for (j = 0; j < n; ++j)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
d[i][j] = 0
代表 i i i 不能到 j j j,用 d[i][j] = 1
代表 i i i 能到 j j j,同样可以用 Floyd-Warshall 算法来求所有点之间的连通关系,即有向图的 “传递闭包”。c++ 代码实现如下:void FloydWarshall(int n) {
int i, j, k;
for (k = 0; k < n; ++k)
for (i = 0; i < n; ++i)
for (j = 0; j < n; ++j)
d[i][j] = d[i][j] || (d[i][k] && d[k][j]);
}
题目链接 | 难度 | 解析 |
---|---|---|
HDU 1874 畅通工程续 | ★☆☆☆☆ | Dijkstra |
HDU 2066 一个人的旅行 | ★☆☆☆☆ | Dijkstra |
HDU 2112 HDU Today | ★☆☆☆☆ | Dijkstra |
HDU 2544 Shortest Path | ★☆☆☆☆ | Dijkstra |
HDU 3790 Shortest Path Problem | ★☆☆☆☆ | Dijkstra |
HDU 1546 Idiomatic Phrases Game | ★☆☆☆☆ | Dijkstra |
HDU 2722 Here We Go(relians) Again | ★☆☆☆☆ | Dijkstra |
HDU 1596 find the safest road | ★☆☆☆☆ | Dijkstra |
HDU 1245 Saving James Bond | ★☆☆☆☆ | Dijkstra |
HDU 1548 A strange lift | ★☆☆☆☆ | Dijkstra |
HDU 2377 Bus Pass | ★★☆☆☆ | Dijkstra |
HDU 2145 zz’s Mysterious Present | ★★☆☆☆ | Dijkstra |
HDU 2354 Another Brick in the Wall | ★★☆☆☆ | Dijkstra |
HDU 4522 湫湫系列故事——过年回家 | ★★☆☆☆ | Dijkstra |
HDU 1224 Free DIY Tour | ★★☆☆☆ | Dijkstra + 路径还原 |
HDU 1385 Minimum Transport Cost | ★★☆☆☆ | Dijkstra + 路径还原 |
HDU 1142 A Walk Through the Forest | ★★☆☆☆ | Dijkstra + 路径数 |
HDU 2680 Choose the best route | ★★☆☆☆ | Dijkstra + 预处理 |
HDU 1599 find the mincost route | ★★☆☆☆ | Dijkstra + 枚举删边 |
HDU 1595 find the longest of the shortest | ★★☆☆☆ | Dijkstra + 枚举删边 |
HDU 2363 Cycling | ★★☆☆☆ | 二分枚举 + Dijkstra |
HDU 1535 Invitation Cards | ★★☆☆☆ | Dijkstra/堆优化 |
HDU 2962 Trucking | ★★☆☆☆ | 二分枚举 + Dijkstra/堆优化 |
HDU 4001 To Miss Our Children Time | ★★☆☆☆ | 构图 + Dijkstra/堆优化 |
HDU 1839 Delay Constrained Maximum Capacity | ★★☆☆☆ | 二分枚举 + Dijkstra/堆优化 |
HDU 1690 Bus System | ★★★☆☆ | Dijkstra |
HDU 4885 TIANKENG’s travel | ★★★☆☆ | 极角排序 + Dijkstra/堆优化 |
HDU 3768 Shopping | ★★★☆☆ | 状态压缩 + Dijkstra/堆优化 |
题目链接 | 难度 | 解析 |
---|---|---|
PKU 1860 Currency Exchange | ★★☆☆☆ | Bellman-Ford 判正权环 |
HDU 1217 Arbitrage | ★★☆☆☆ | Bellman-Ford 判正权环 |
HDU 2388 Playground Hideout | ★★☆☆☆ | Bellman-Ford |
PKU 3259 Wormholes | ★★☆☆☆ | Bellman-Ford 判负权环 |
HDU 1317 XYZZY | ★★☆☆☆ | Bellman-Ford 判正权环 + Floyd-Warshall |
题目链接 | 难度 | 解析 |
---|---|---|
HDU 1535 Invitation Cards | ★★☆☆☆ | SPFA |
HDU 3339 In Action | ★★☆☆☆ | SPFA + 背包 |
HDU 2833 WuKong | ★★★☆☆ | SPFA / 最短路径公共点 |
HDU 2782 The Worm Turns | ★★☆☆☆ | 四向图最长路 / 深搜 |
HDU 2992 Hotel booking | ★★★☆☆ | 两次最短路 |
HDU 3499 Flight | ★★★☆☆ | SPFA |
HDU 2482 Transit search | ★★★★☆ | SPFA + 坐标转换 + 字符串哈希 |
HDU 3873 Invade the Mars | ★★★★☆ | SPFA |
题目链接 | 难度 | 解析 |
---|---|---|
HDU 2807 The Shortest Path | ★★☆☆☆ | Floyd-Warshall |
HDU 2923 Einbahnstrasse | ★★☆☆☆ | Floyd-Warshall |
HDU 3179 Effective Government Spokesman | ★★☆☆☆ | Floyd-Warshall |
HDU 3631 Shortest Path | ★★★☆☆ | Floyd-Warshall |
HDU 4034 Graph | ★★★☆☆ | Floyd-Warshall |
HDU 3021 Tree Fence | ★★★★☆ | Floyd-Warshall + 凸包 |