POJ3255 Roadblocks 这是今天遇到的第一个求次短路问题(要是来学具体实现的就不需要看我的这篇啦~,这篇偏向于数学证明)
题意:某街区共有R条道路、N个路口。道路可以双向通行。问1号路口到N号路口的次短路长度是多少? 次短路指的是比最短路长度长的次短的路径。同一条边可以经过多次。
目前掌握两种解法,现在总结一下。
总结中disx[y]代表x到y的最短距离。
解法一:先求出起点到所有点的最短距离dis1[]和终点到所有点的最短距离disn[], 然后枚举每条边(双向边),即假定这条边在次短路中,记录每次的结果,这样最后就能得出次短的距离。
ans = min(dis1[u] + disn[v] + value[u][v]) e(v,u)∈ E,每次松弛ans时要判断dis1[u] + disn[v] + value[u][v]!=最短路 才行。这个解法是基于模型:次短路必有一条有向边(u->v)和最短路不同,而且1到u的必须是最短路,v到n的必须是最短路,这个不难理解。而且包括这种情况(因为是有向边):最短路(A->C->D->B)当枚举到的有向边是D->C时,那么用
dis1[D]+(D->C)+disn[C]来松弛ans,也就是这条(A->C->D->C->D->B)也就是CD这条无向边走了一次,有向边D->C走了一次。仍然符合模型。
解法二:用Dijkstra的思想一步到位求出次短路数组dis2[n]的大小,这个方法比较难,但是可以加深理解Dijkstra的原理。(下面把1到各点的最短路数组用dis[]表示,1到各点次短路的数组用dis2[]来表示)
首先我们仍是考虑松弛思想,一步一步地松弛dis2数组,松弛的时候是dis[v]=dis[u]+value[u][v]这个公式,所以我们先把次短路换一个模型。
构造成那个公式的样子:1到任何一个顶点的次短路dis2要么(情况一)是1到某个顶点u的最短路(dis[u])+u->v的边,要么(情况二)是(2)1到u的次短路(dis2[u])+u->v的边。
现在用解法一的模型来转换成这个模型来验证正确性:当模型一的value[x][y]是此处的u->v边,那么1到v的最短路是dis1[u]+value[x][y]+disv[v]=dis1[u]+value[x][y],也就是对应了情况一。当模型一的value[x][y]不是此处的u->v边,就对应了情况二:因为此时1到u的距离是dis1[x]+value[x][y]+disu[y],所以1到u的距离是1到u的次短路dis2[u],而且u->v边枚举总会松弛成最短路,所以对应了情况二。
所以用代码实现此解法的过程不仅要把最短路每次松弛后添加到优先队列中,而且要把松弛成功的次短路pair也添加到优先队列中,因为有情况二的存在。
for(int i=0;i<G[u].size();i++){
edge e=G[u][i];
int d2=d+e.cost;
if(d2<dis[e.to]){
swap(dis[e.to],d2);
que.push(make_pair(dis[e.to],e.to));
}
if(d2<dis2[e.to]&&d2>dis[e.to]){
dis2[e.to]=d2;
que.push(make_pair(dis2[e.to],e.to));
}
} 这是核心代码
这时候又有疑问了,既然生成dis2次短路数组的时候用到了情况二,也就是要保证“1到u的次短路(dis2[u])+u->v的边”中的dis2[u]的确是次短路才行,当然,一开始的dis2数组当然全部都不是次短路,所以我们还要证明通过这个方法最终会松弛成次短路才行,同样用Dijkstra的贪心思想:1到某一个顶点的次短路dis2[x]是dis2数组中的最小值,那么dis2[x](情况一)是1到某个顶点u的最短路(dis[u])+u->v的边。这时候就没有情况二了,因为dijkstra算法的贪心保证了情况二不会对此时的dis2[x]产生松弛,所以求这个dis2[x]的时候没用到dis2数组,只用到了dis数组,这个时候必然正确,因为Dijkstra必然正确,所以情况一必然最终会把dis2[x]松弛成次短路。
一旦求出了这条次短路dis[x],然后就可以用两种情况+利用dis数组和dis2[x]的值松弛出第二条最短路,dp思想,最终求出所有dis2数组的值。
方法一代码实现:
//2584K 219MS
#include<cstdio>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
const int maxn = 5000+100;
const int inf = 0x7fffffff;
typedef pair<int,int> P;
struct edge{
int to,cost;
};
vector<edge> G[maxn];
int n,m,ans;
int dis1[maxn],disn[maxn];
void Dijkstra(int src,int dis[]) //src指源点,求单源最短路
{
priority_queue< P,vector<P>,greater<P> >que;
dis[src]=0;
que.push(P(dis[src],src));
while(!que.empty()){
P p=que.top();
que.pop();
int u=p.second,d=p.first;
if(d>dis[u]) continue;
for(int i=0;i<G[u].size();i++){
edge e=G[u][i];
if(e.cost+d<dis[e.to]){
dis[e.to]=d+e.cost;
que.push(P(dis[e.to],e.to));
}
}
}
}
void solve()
{
fill(dis1+1,dis1+1+n,inf);
fill(disn+1,disn+1+n,inf);
Dijkstra(1,dis1);
Dijkstra(n,disn);
ans=0x7fffffff;
for(int i=1;i<=n;i++) //枚举所有的u->v边
for(int j=0;j<G[i].size();j++){
int v=G[i][j].to,d=G[i][j].cost;
if(dis1[i]+d+disn[v]>dis1[n]){
ans=min( ans , dis1[i]+ d + disn[v] );
}
}
}
int main()
{
scanf("%d%d",&n,&m);
int u,v,w;
edge e;
for(int i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
e.to=v,e.cost=w;
G[u].push_back(e);
e.to=u;
G[v].push_back(e);
}
solve();
printf("%d\n",ans);
return 0;
}
方法二代码实现:
//2784K 235MS
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
#define inf 0x7fffffff
#define maxn 5000+100
using namespace std;
typedef pair<int,int> P;
struct edge{
int to,cost;
};
vector<edge> G[maxn];
int dis[maxn],dis2[maxn];
int n,m;
void ini()
{
fill(dis+1, dis+n+1, inf);
fill(dis2+1, dis2+n+1, inf);
dis[1]=0;
}
void solve()
{
priority_queue<P,vector<P>,greater<P> >que;
que.push(P(0,1));
while(!que.empty()){
P p=que.top();
que.pop();
int u=p.second,d=p.first;
if(dis2[u]<d) continue; //一步剪枝,贪心思想,不说了
for(int i=0;i<G[u].size();i++){
edge e=G[u][i];
int d2=d+e.cost;
if(d2<dis[e.to]){
swap(dis[e.to],d2); //交换,也就是把次短路从最短路dis数组里取出,当然整个过程并不是所以的次短路数值都是从dis中取来的,还有从由一条次短路松弛得到另一条次短路的,也就是下一个条件判断
que.push(P(dis[e.to],e.to)); //既把松弛成功的最短路pair入队
}
if(d2<dis2[e.to]&&d2>dis[e.to]){
dis2[e.to]=d2;
que.push(P(dis2[e.to],e.to));//又把松弛成功的次短路pair入队
}
}
}
}
int main()
{
scanf("%d%d",&n,&m);
ini();
int u,v,w;
edge e;
for(int i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
e.to=v,e.cost=w;
G[u].push_back(e);
e.to=u,e.cost=w;
G[v].push_back(e);
}
solve();
printf("%d\n",dis2[n]);
return 0;
}
总结:
方法一简单易懂,优势在于很容易拓展成求1到n的第K短路即{dis1[u] + disn[v] + value[u][v]}这个集合中的第K小值即可(如果k大,会导致动态规划的思路不完全,因为可能包含两个或三个value[][]),不需要把都储存在数组里然后来个快排,直接用K个元素的最小堆,每次算出一个就和堆顶比较,然后答案就是堆顶元素,复杂度O(NlogK),但是这是固定两端的这不是单源次短路或者是K短路,方法二用的dijkstra思想算出了单源次短路。而且用类似的方法也可以求单源第三短路,一旦超过三个,代码的实现会很困难,但仍不得不说方法二是Dijkstra算法的升华应用。