hdu 1385 Minimum Transport Cost
最短路入门经典题
题意是输入n表示n个点从1到n标号,n=0结束程序
然后给出n*n的邻接矩阵,g[i][j]=-1表示i->j没有通路
然后有多个查询,输入u,v,输出u->v的最短路并且打印字典序最小的路径,查询以-1 -1结束
//除了边的权值之外每个点还附带一个权值,所以在松弛操作的时候要把点的权值也计算进去
//另外在总费用最小的情况下要输出字典序最小的路径,同样是在松弛操作那里处理
//如果能更新d[i]使d[i]变小则直接更新
//如果是与d[i]相同则判断一下如果更新的话会不会使路径的字典序更小,如果能才更新否则不更新
//因为由多个查询,显然是用Floy来处理更好,当然也可以写一个对所有源点求最短路的dij
//分别实现
Floy实现
//用Floy实现 #include <stdio.h> #include <string.h> #define N 110 #define INF 1000000000 int d[N][N],path[N][N],c[N]; int n,cost; int s,t; void input() { int i,j,w; for(i=1; i<=n; i++) for(j=1; j<=n; j++) { scanf("%d",&d[i][j]); if(d[i][j]==-1) d[i][j]=INF; path[i][j]=j; } for(i=1; i<=n; i++) scanf("%d",&c[i]); return ; } void Floy() { int i,j,k; for(k=1; k<=n; k++) //中转站k for(i=1; i<=n; i++) //起点和终点i,j for(j=1; j<=n; j++) { if( d[i][j] > d[i][k]+d[k][j]+c[k] ) { d[i][j]=d[i][k]+d[k][j]+c[k]; path[i][j]=path[i][k]; } else if( d[i][j] == d[i][k]+d[k][j]+c[k] ) { if( path[i][j] > path[i][k]) { d[i][j]=d[i][k]+d[k][j]+c[k]; path[i][j]=path[i][k]; } } } return ; } void print_path(int u , int v) //u是起点v是终点 { int k; if(u==v) { printf("%d",v); return ; } k=path[u][v]; printf("%d-->",u); print_path(k,v); } int main() { while(scanf("%d",&n)!=EOF && n) { input(); Floy(); while(scanf("%d%d",&s,&t)) { if( s==-1 && t==-1) break; cost=d[s][t]; if(s==t) //起点和终点相同 { printf("From %d to %d :\n",s,t); printf("Path: %d\n",s); printf("Total cost : %d\n\n",cost); continue; } printf("From %d to %d :\n",s,t); printf("Path: "); print_path(s,t); printf("\n"); printf("Total cost : %d\n\n",cost); } } return 0; }
SPFA实现
#include <cstdio> #include <cstring> #include <queue> using namespace std; #define N 110 #define INF 1000000000 queue <int> q; bool vis[N]; int g[N][N],c[N]; int path[N][N],d[N][N]; int n; int chang(int s , int v ,int u) { int p1[N],p2[N],tmp,len1,len2; len1=len2=0; memset(p1,0,sizeof(p1)); memset(p2,0,sizeof(p2)); p1[len1]=v; tmp=path[s][v]; while(tmp!=s) { p1[++len1]=tmp; tmp=path[s][tmp]; } p1[++len1]=tmp; p2[len2]=v; tmp=u; while(tmp!=s) { p2[++len2]=tmp; tmp=path[s][tmp]; } p2[++len2]=tmp; while(p1[len1]==p2[len2]) { len1--; len2--; } return p2[len2]<p1[len1]; } void spfa(int s) { for(int i=1; i<=n; i++) //初始化 { d[s][i]=g[s][i]; path[s][i]=s; vis[i]=0; } d[s][s]=0; while(!q.empty()) q.pop(); for(int i=1; i<=n; i++) if(d[s][i]!=INF) { q.push(i); vis[i]=1; } while(!q.empty()) { int u; u=q.front(); //读取队头元素 q.pop(); //队头元素出队 vis[u]=0; //消除标记 for(int v=1; v<=n; v++) //对所有与u相连的点v进行松弛操作 if( d[s][u]+g[u][v]+c[u] < d[s][v]) //可以更新路径 { d[s][v]=d[s][u]+g[u][v]+c[u]; path[s][v]=u; if(!vis[v]) { q.push(v); vis[v]=1; } } else if( d[s][u]+g[u][v]+c[u] == d[s][v]) //不能更新估计值但是有可能能改变路径 { if( chang(s,v,u) ) path[s][v]=u; } } return ; } void print_path(int s ,int t) { int u; if(t==s) { printf("%d",s); return ; } u=path[s][t]; print_path(s,u); printf("-->%d",t); } int main() { while(scanf("%d",&n)!=EOF && n) { for(int i=1 ;i<=n; i++) for(int j=1; j<=n; j++) { scanf("%d",&g[i][j]); if(g[i][j]==-1) g[i][j]=INF; } for(int i=1; i<=n; i++) scanf("%d",&c[i]); for(int i=1; i<=n; i++) //枚举所有源点 spfa(i); //对该源点进行单源最短路径 int s,t; while(scanf("%d%d",&s,&t)!=EOF) { if(s==-1 && t==-1) break; if(s==t) //起点和终点相同 { printf("From %d to %d :\n",s,t); printf("Path: %d\n",s); printf("Total cost : %d\n\n",d[s][t]); continue; } printf("From %d to %d :\n",s,t); printf("Path: "); print_path(s,t); printf("\n"); printf("Total cost : %d\n\n",d[s][t]); } } return 0; }
Dijkstra实现
//用dij实现 //WA了有5,6次就是因为路径字典序处理不好 #include <stdio.h> #include <string.h> #define N 110 #define INF 1000000000 int g[N][N],d[N][N],path[N][N],c[N],cost; bool cov[N][N]; int n; int V0,V; void input() { int i,j; for(i=1; i<=n; i++) for(j=1; j<=n; j++) { scanf("%d",&g[i][j]); if(g[i][j]==-1) g[i][j]=INF; } for(i=1; i<=n; i++) scanf("%d",&c[i]); return ; } int chang(int ss , int jj , int kk) //比较路径 { int p1[N],p2[N],len1,len2,tmp; memset(p1,0,sizeof(p1)); memset(p2,0,sizeof(p2)); len1=len2=0; p1[len1]=jj; len1++; tmp=path[ss][jj]; while(tmp!=ss) { p1[len1]=tmp; len1++; tmp=path[ss][tmp]; } p1[len1]=ss; p2[len2]=jj; len2++; tmp=kk; while(tmp!=ss) { p2[len2]=tmp; len2++; tmp=path[ss][tmp]; } p2[len2]=ss; while(p1[len1]==p2[len2]) { len1--; len2--;} return p2[len2] < p1[len1] ; } void dij(int s) //源点是s { int i,j,k,nn,min; memset(cov,0,sizeof(cov)); //在这一轮中要清0 for(i=1; i<=n; i++) //初始化 { d[s][i]=g[s][i]; path[s][i]=s; //初始化路径,所有点的前驱都是源点s } cov[s][s]=1; d[s][s]=0; //本来应该赋值为0的,不过每个点都附带了权值所以赋初值为c[s] for(nn=1; nn<n; nn++) //nn是个数,还有求出源点到其余n-1个点的最短路 { min=INF; k=s; for(i=1; i<=n; i++) if( !cov[s][i] && d[s][i] < min) { min=d[s][i]; k=i; } cov[s][k]=1; //得到了i点的最短路 for(i=1; i<=n; i++) if(!cov[s][i])//松弛操作 { if( min+g[k][i]+c[k] < d[s][i]) //如果以k点为准来松弛,要把k点附带的权值算进去 { d[s][i]=d[s][k]+g[k][i]+c[k]; path[s][i]=k; } else if( min+g[k][i]+c[k] == d[s][i] ) { if( chang(s , i , k) ) //比较路径成功后才可以替换 { d[s][i]=d[s][k]+g[k][i]+c[k]; path[s][i]=k; } } } } return ; } void print_path(int s , int t) { int k; if(t==s) { printf("%d",s); return ; } k=path[s][t]; print_path(s,k); printf("-->%d",t); } int main() { int i,j; while(scanf("%d",&n)!=EOF && n) { input(); for(V0=1; V0<=n; V0++) //枚举所有的源点用dij去求源点到所有点的最短路 { dij(V0); /* printf("***********\n"); printf("D: "); for(i=1; i<=n; i++) printf("%d ",d[V0][i]); printf("\n"); printf("Path: "); for(i=1; i<=n; i++) printf("%d ",path[V0][i]); printf("\n"); printf("***********\n"); */ } while(scanf("%d%d",&V0,&V)) //查询 { if(V0==-1 && V==-1) break; if(V0==V) { printf("From %d to %d :\n",V0,V); printf("Path: %d\n",V0); printf("Total cost : %d\n\n",d[V0][V]); } else { printf("From %d to %d :\n",V0,V); printf("Path: "); print_path(V0,V); printf("\n"); printf("Total cost : %d\n\n",d[V0][V]); } } } return 0; }
由上面的代码可以看到Floy算法实现比Dijkstra算法实现方便很多,代码量少,通俗易懂,细节地方少不易出错,最重要的是这道题输出字典序最小的路径,这个要求正好满足Floy而不太满足Dijkstra,如果非要用Dijkstra实现的在更新路径的时候如果额外判断,实际上时间就增加了
一:关于Dijkstra的初始化也会对这道题有影响,因为字典序最小路径,并且算最后的总费用的时候,起点和终点附带的权值是不能计算进去的,初始化问题将会影响这两个问题的解决。这个问题用第一种初始化才好
//dij算法初始化有两种其实是一样的只是写法不同,但是发现,不同的问题用不同的初始化 //会影响后面的代码,适合的初始化能提高效率并且精简代码提高代码可读性 //初始化1 memset(cov,0,sizeof(cov)); for(i=1; i<=n; i++) { d[s][i]=g[s][i]; //一开始所有点的最短路都看作是和源点直接相连的边的权值 path[s][i]=s; //自然所有点的前驱就是源点s包括源点自己 } cov[s][s]=1; //源点到源点的最短路不用计算 d[s][s]=0; //源点到源点的最短路为0 //初始化2 memset(cov,0,sizeof(cov)); for(i=1; i<=n; i++) { d[s][i]=INF; //所有点的最短路都还没算而且不知道是否存在所有全部初始化为INF path[s][i]=0; //当然也就不知道点i的前驱是谁,点是从1开始标号的所以所有点的前驱都标记为0表示没有 } //注意这里是没有 cov[s][s]=1; 因为并没有求出源点s的最短路虽然我们知道是不用求的为0 d[s][s]=0; //源点的最短路不用求已知为0所以赋值为0,这个赋值是为了后面的代码可以运行做准备的 path[s][s]=s; //源点的前驱我们标记为源点,当然也可以保留为0的,在打印路径的时候判断做出些微的改变即可
二:关于Dij和Floy记录路径的问题
在Floy中,输出路径时,path[u][v]=k , 表示从u到v的路径中经过点k,然后就用点k来替换u,注意是替换u,变为path[k][v] ,直到k=v , 即不断替换起点
所以Floy的路径初始化为path[u][v]=v; 在进行松弛操作的时候更新路径是 path[u][v]=path[u][k];
在Dij中,path[s][t]=k , 也表示从s到t的路径经过点k,然后就用点k替换t,注意是替换t,变为path[s][k],直到s=k , 即不断替换终点
所以这道题在更新字典序最小的路径的时候,不能单单判断一个点,要把整条路径全部拿出来,从源点开始比较直到找到第一个不同的点,也就是代码中的chang()函数
一开始wa了很多次,就是因为判断路径的那句代码写为
if(path[s][i] > k) //就更新
这样是不对,因为path[s][i]只是点s到点i的路径中在点i前面的那个点,是靠最后的点,而比较字典序是从头开始比较直到第一个不同的元素为止,如果直接就这样比较相当于是从后面开始比较,是错误的
但是在Floy中就可以直接比较,代码中的比较是
if(path[i][j] > path[i][k]) //就更新
因为path[i][j]记录的就是 i-->j 路径中就靠点i的点,是最开始的点 ,而path[i][k]也是最靠近点i的点,所有是可以更新的,是符合字典序的比较原则的
而SPFA算法的路径问题和DIJ是一样的,所以两者处理方法一样