我可以这么说:spfa不仅仅是bellman_ford的队列优化版本,而且是魔改!
题目描述
给定一张有向有权图,求出点1到点n的最短路长度和最短路的数量. 可能无解,但是不会出现负权边和自环,若有重边请忽略影响,只算一条.
题目分析
之前我们有做过无权图的最短路算法,那时候我们直接对 s p f a spfa spfa进行操作就好了.
然鹅这道题, s p f a spfa spfa就不能直接操作,但是 d i j k s t r a dijkstra dijkstra就可以.
为什么呢?我们不妨从算法的角度出发.
我们知道,dij基于的思想是贪心,每次我们从起点每次新拓展一个到起点距离最短的点,再以这个点为中间点,更新起点到其他所有点的距离.
所以已经被操作的这个“当前到起点距离最短的点”,是不会再一次被操作的,
也就是说,每个点都只会在 p r i o r i t y   q u e u e priority\:queue priorityqueue中操作一次,在它被操作前最后一次更新后dis和sum一定是固定的.
但是 s p f a spfa spfa算法不一样,它并没有所谓“某次更新后就不会改变”的情况, s p f a spfa spfa的最短路和最短路数在 q u e u e queue queue为空之前一直有被更新的可能.
那么为什么在无权图上, s p f a spfa spfa算法成立呢?那是因为 s p f a spfa spfa算法同时基于三角形性质:
dis[u]+e[u][v]>=dis[v],其中dis[u],dis[v]为已经确定的最短路.
我们发现,在无权图上,不等式左边始终大于右边,而在有权图上,这是不一定的.
故而在有权图上,一个点可能进入 q u e u e queue queue多次,从而受到前一个节点的影响,导致重复计算等一系列不可预知的问题.
这也是我为什么说“魔改”,就是因为它的 q u e u e queue queue进入顺序实在太不可确定了!
那有没有解决方法呢?也是有的,只要消除了前面的节点的影响就行.
1.只有当一个点到起点的路径数不为0且未入队时,我们才将它放入队列.
2.如果搜索到的一条边的终点为n,那么我们应该跳过这一条边的搜索,而不是直接结束程序.(why???)
3.每次对取出来的点进行完搜索后,一定要将它的路径数清0,不然会出现重复统计.(这是为了满足1,实现再次进队)
总而言之,很麻烦的样子。。。而且不是很能理解. 若是要求所有节点的最短路数,也不合适.
所以以后遇上还是老老实实写dij吧,毕竟dij中更新是有穷的,可预知确定的操作简单,而
s
p
f
a
spfa
spfa不是.
程序实现
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
#define maxn 2010
using namespace std;
struct edge{
int v,w,next;
}e[maxn*maxn];
int head[maxn],tot;
void add(int u,int v,int w){
e[++tot].v =v;
e[tot].w =w;
e[tot].next=head[u];
head[u]=tot;
}
int ed[maxn][maxn];
struct node{
int dis,pos;
bool operator <(node nd)const{
return dis>nd.dis ;
}
};//好像经常容易忘记这个分号
priority_queue<node> q;
bool vis[maxn];
int dis[maxn],sum[maxn];
void dijkstra(int s,int t){
memset(dis,inf,sizeof dis);
q.push((node){0,1});
dis[1]=0;
sum[1]=1;
while(!q.empty()){
node now=q.top();
q.pop();
int u=now.pos ;
if(vis[u])continue;
vis[u]=true;//注意vis表示最短路已确定的点
for(int i=head[u];i;i=e[i].next ){
int v=e[i].v ;
if(dis[v]>dis[u]+e[i].w ){
dis[v]=dis[u]+e[i].w ;
sum[v]=sum[u];//核心操作
if(!vis[v])q.push((node){dis[v],v});
}
else if(dis[v]==dis[u]+e[i].w )sum[v]+=sum[u];//
}
}
}
int n,m;
int main(){
memset(ed,inf,sizeof ed);
scanf("%d%d",&n,&m);
for(int i=1,u,v,w;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
if(ed[u][v]>w)add(u,v,w),ed[u][v]=w;//防重边(完全一样的边)
}
dijkstra(1,n);
if(dis[n]==inf)printf("No answer\n");//特判
else printf("%d %d\n",dis[n],sum[n]);
return 0;
}
题后总结
对基本算法的理解要足够透彻,否则会在需要灵活使用时不知所措.