最短路问题作为图论中最基础的问题,一般有两种考查方式。一种为给定两个顶点,一个顶点作为起点,另一个顶点作为终点,求得能从起点至终点所有路径中,所有边权和最小的路径。
如:A->C->D->F->E->G,便是上图的最短路径
单源最短路径
单源最短路径问题是固定一个起点,求它到其他所有点的最短路问题。若是终点也固定,那么这种问题被称为两点之间的最短路问题。
一、Bellman-Ford算法
记从起点s出发到顶点i的最短距离为d[i],且d[i]满足d[i]=min{d[j]+从j到i的边的权值 | e=(j,i)∈E};设起点的距离初值为d[s]=0,d[i]=INF(INF为一个较大的常数),按着上述公式逐步递推,不断更新d的取值,只要图中不存在负边,则这样的更新操作就是有限的。最后所得到的d的值,就是所求的最短路径的结果。
//设置边的结构体,分别为出度顶点,权值,入度顶点
struct edge{
int from,cost,to;
};
edge es[MAX_E]; //边
int d[MAX_V];
int V,E;
//Bellman-Ford算法
void bellman(int s){
//初始化,将所有顶点间距离初始化为INF,表示未访问过
for(int i=0; i<V; i++){
d[i]=INF;
}
d[s] = 0;
while(true){
bool f = false;
for(int i=0; i<E; i++){
edge e = es[i];
//若以访问过,且之前到达入读边的距离大于现在入度距离+此时边的权值,则更新d的值
if(d[e.from]!=INF&&d[e.to]>d[e.from]+e.cost){
d[e.to] = d[e.from]+e.cost;
f = true;
}
}
if(!f)break;
}
}
最短路径不会经过同一个顶点两次(没有负边的情况下),也就是说最多其最多通过|V|-1条边,while(true)的过程最多执行|V|-1次,因此算法复杂度为O(|V|*|E|)。反之,若是图中有负边,则会持续更新d的值,即在第|V|次仍然会执行操作,因此我们可以利用这个性质来检测负环。
二、负环的检测
//若返回为true,则有负环
bool find_negative_loop() {
//初始化所有d值为0
for(int i=0; i<V; i++) {
d[i] = 0;
}
for(int i=0; i<V; i++) {
for(int j=0; j<E; j++) {
edge e = es[j];
if(d[e.to]>d[e.from]+e.cost) {
d[e.to]=d[e.from]+e.cost;
if(i==V-1) return true;//若在第V次更新,则说明有负环
}
}
}
return false;
}
三、Dijkstra算法(无负边)
在Bellman-Ford算法中,若d[i]还不是最短距离的话,那么即使进行d[j]=d[i]+(从顶点i到顶点j的权值)的操作,得到的也不会是最短距离,而且就算d[i]最终不会有变化,每次循环仍旧会遍历一遍所有从i出发的边,这是很浪费时间的。因此,我们可以对算法做出如下改进:
(1)找到最短距离已确定的顶点,从它出发更新相邻顶点的最点距离
(2)此后不再关心(1)中所找到的“最短距离已确定的顶点”
但我们要如何找到“最短距离已确定的顶点”呢?在最开始时,我们只能确定起点的距离,在尚未访问过的顶点中,距离d[i]最小的顶点,就是最短距离已确定的顶点。有因为Dijkstra算法的前提条件为无负边,因此d[i]会在后续的不断更新中变小,最终得出最短距离。
int cost[MAX_V][MAX_V];//cost[u][v]表示从顶点u到顶点v的权值
bool v_use[MAX_V];//表示顶点有无被使用过
int d[MAX_V];//从顶点s出发的最短距离
int V;//顶点个数
void Dijkstra(int s) {
fill(d,d+V,INF);
fill(v_use,v_use+V,false);
d[s] = 0;
while(true) {
int v=-1;
//从未使用过的顶点中选择一个最小的
for(int u=0; u<V; u++) {
if(!v_use[i]&&(v==-1||d[u]<d[v])) v=u;
}
if(v==-1) break;
v_use[v] = true;
for(int u=0; u<V; u++) {
d[u] = min(d[u],d[v]+cost[v][u]);
}
}
}
任意两点间的最短路径
一、Floyd-Warshall算法
求解图中所有两点间的最短距离的问题称之为任意两点间的最短距离,我们可以利用DP来进行求解任意两点间的最短距离问题。假设只使用顶点0~k和i,j的情况,记i到j的最短路的长度为d[k+1][i][j]。当k=-1时,认为只使用了i,j,所以d[0][i][j] = cost[i][j],那么接下来我们就可以把只使用0~k的问题归引到只使用0~k-1的问题上。
只使用0~k时,我们分i到j的最短路正好经过一次k和完全不经过k两种情况来讨论。
(1)不经过顶点k的情况下,d[k][i][j]=d[k-1][i][j]。
(2)通过顶点k一次的情况下,d[k][i][j]=min(d[k-1][i][j],d[k-1][i][k]+d[k-1][k][j])
上述递推式也可用同一个数组,不断进行d[i][j]=min(d[i][j],d[k][j]+d[i][k])的更新来实现。
int d[MAX_V][MAX_V]; //记忆化数组,d[u][v]表示边e=(u,v)的权值
int V;
void floyd(){
for(int k=0; k<V; k++){
for(int i=0; i<V; i++){
for(int j=0; j<V; j++){
d[i][j] = min(d[i][j],d[i][k]+d[k][j]);
}
}
}
}