图论——最短路径问题
两点间最短路径问题:
例如求城市A到城市B之间最短距离
固定起始点,求最短路径
可以使用DFS,或者BFS
任意两点间的最短路径问题:
已知求解固定两点间最短路径的方法,如果要求任意两点间的最短路径,可以使用n^2次dfs或者bfs,但是时间复杂度较高
观察会发现,如果要让两点 i , j 间的路程变短,只能通过第三个点 k 的中转。比如上面第一张图,从 1->5 距离为10,但 1->2->5 距离变成9了。事实上,每个顶点都有可能使另外两个顶点间的路程变短。这种通过中转变短的操作叫做松弛。
当任意两点间不允许经过第三个点时,这些从城市之间的最短路程就是初始路程
例如有如下矩阵
假如现在允许经过1号顶点的中转,求任意两点间的最短路,这时候就可以遍历每一对顶点,试试看通过1号能不能缩短他们的距离。
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
{
if(e[i][j] > e[i][1]+e[1][j]) e[i][j] = e[i][1]+e[1][j];
}
更新后,3->2,4->2,4->3的路径都变短了
扩展一下,先允许1号顶点作为中转给所有两两松弛一波,再允许2号、3号...n号都做一遍,就能得到最终任意两点间的最短路了。
这就是Floyd算法,时间复杂度是O(n^3),但核心代码只有五行,实现起来非常容易
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(e[i][j] > e[i][k]+e[k][j])
e[i][j] = e[i][k]+e[k][j];
单源最短路径问题
即,指定源点,求它到其余各个结点的最短路径
比如给出这张图,假设把1号结点作为源点。
还是用数组dis来存1号到其余各点的初始路程:
既然是求最短路径,那先选一个离起点1号最近的结点,也就是2号结点。这时候,dis[2]=1 就固定了,它就是1到2的最短路径。因为目前离1号最近的是2号,且这个图的所有边都是正数,那就不可能能通过第三个结点中转使得距离进一步缩短了。因为从1号出发已经找不到哪条路比直接到达2号更短了。
选好了2号结点,现在看看2号的出边,有2->3和2->4。先讨论通过2->3这条边能否让1号到3号的路程变短,也即比较dis[3]和dis[2]+e[2][3]的大小。发现是可以的,于是dis[3]从12变为新的更短路10。同理,通过2->4也条边也更新下dis[4]。
松弛完毕后dis数组变为:
接下来,继续在剩下的 3 4 5 6 结点中选一个离1号最近的结点。发现当前是4号离1号最近,于是dis[4]确定了下来,然后继续对4的所有出边看看能不能做松弛。
这样一直做下去直到已经没有“剩下的”结点,算法结束。
这就是Dijkstra算法,整个算法的基本步骤是:
- 所有结点分为两部分:已确定最短路的结点集合P、未知最短路的结点集合Q。最开始,P中只有源点这一个结点。(可用一个book数组来维护是否在P中)
- 在Q中选取一个离源点最近的结点u(dis[u]最小)加入集合P。然后考察u的所有出边,做松弛操作。
- 重复第二步,直到集合Q为空。最终dis数组的值就是源点到所有顶点的最短路。
for(int i = 1; i <= n; i++) dis[i] = e[1][i]; //初始化dis为源点到各点的距离
for(int i = 1; i <= n; i++) book[i] = 0;
book[1] = 1; //初始时P集合中只有源点
for(int i = 1; i <= n-1; i++) //做n-1遍就能把Q遍历空
{
int min = INF;
int u;
for(int j = 1; j <= n; j++) //寻找Q中最近的结点
{
if(book[j] == 0 && dis[j] < min)
{
min = dis[j];
u = j;
}
}
book[u] = 1; //加入到P集合
for(int v = 1; v <= n; v++) //对u的所有出边进行松弛
{
if(e[u][v] < INF)
{
if(dis[v] > dis[u] + e[u][v])
dis[v] = dis[u] + e[u][v];
}
}
}
Dijkstra是一种基于贪心策略的算法。每次新扩展一个路径最短的点,更新与它相邻的所有点。当所有边权为正时,由于不会存在一个路程更短的没扩展过的点,所以这个点的路程就确定下来了,这保证了算法的正确性。
但也正因为这样,这个算法不能处理负权边,因为扩展到负权边的时候会产生更短的路径,有可能破坏了已经更新的点路程不会改变的性质。