uva 10806 Dijkstra, Dijkstra.
题意:固定起点1和终点n,从1到n,再从n回到1,去和回的路上相同的边只能用一次,求两次的和最短,如果去的时候不能去到终点或者回的时候回不到起点那么就输出Back to jail,否则输出两次和的最小值(此图是无向图,不会有重边,边的权值在大于1小于1000)
两种理解
一种思路是最小费用最大流
另一种是最短路不唯一且两条或者多条最短路由共用边 ,或者最短路与次短路有共用边
我先写的是第二种思路,最小费用最大流还没写,明天补上,并且补上思路
第二种做法是,把无向图当做有向图处理,拆成两条边,先从1到n做一次最短路,并且要记录这条有向路径。最短路结束后,记录下最短路的值d[t],如果为d[t]=INF(t=n)说明图都不连通,从起点娶不到终点。
如果连通能去到,那么把这条路径上的边的值都取相反数,并且删除它的相反边,即赋值为INF
即 u--->v 是去的时候路径的其中有向边,那么g[u][v]=-g[u][v]; g[v][u]=INF;
然后从n到1再运行一遍最短路,因为这次有些边是负值,所以dij不能用,可以用spfa,所以我的代码两次都是spfa,写一个就行了,有些人是先写一个dij再写一个spfa也可以的
如果这次最短路d[t]=INF(此时的t=1),说明回不去了,那么失败
很多人见到题目,就是直接1到n一次最短路,n到1一次最短路,再求和,但是这样是错的,我也是这样做,WA
那么这个算法的正确性是什么呢
如果从1到n有两(多)条最短路,并且有些最短路没有共用边,即完全分离的,那么去的时候用 一条,回的时候用一条,互不干扰,最后的和就是最短路*2
同样的,如果只有1条最短路和次短路(1条和多条都无所谓),而且他们没有共用边,那么去的时候用最短路,回的时候用次短路,互不干扰。最后的和是最短路+次短路
但是,不幸的是,可能有两次最短路,但是却有共用边,那么去的时候肯定会用掉这条共用边,因为回来的时候不能再用这条共用边,那么是不是应该完全放弃另一条没有用过的最短路而另寻路径呢?不是的,而是可以用一个最好的方法,就是消去那条共用边的权(想想为什么来时候的边权要取反)
可以这样理解,两条最短路,都可以看成 前部分+共用边+后部分 , 这里前部分和后部分都是独立的,没有共用的,那么综合两次的走法,其实可以变为 最短路1的前部分+最短路2的后部分,为去的路径,最短路2的后部分+最短路1的前部分,为回的路径,这样子,相当于交换了路径,但是我们并不关心路径,我们只关心两次和最小,这样并不改变和,而且还消掉了共用边的权,其实相当于两次走都没有进过共用边
所以这样的和为 最短路*2-所有共用边的权
同样,所过最短路和次短路有共用边,那么同样是相当于交叉了两次的路径,但是并不改变权和,而且消去了共用边的权
代码
#include <cstdio> #include <cstring> #include <queue> #define INF 0x3f3f3f3f #define N 220 using namespace std; int g[N][N],d[N],path[N],vis[N],n,m,s,t; int sum; void spfa() { queue <int> q; for(int i=1; i<=n; i++) {d[i]=INF; vis[i]=0; path[i]=s;} d[s]=0; q.push(s); vis[s]=1; while(!q.empty()) { int u; u=q.front(); q.pop(); vis[u]=0; for(int v=1; v<=n; v++) if(d[u]+g[u][v]<d[v]) { d[v]=d[u]+g[u][v]; path[v]=u; if(!vis[v]) { q.push(v); vis[v]=1; } } } return ; } void change(int v) { int u=path[v]; g[u][v]=-g[u][v]; //来时候的路径权取反 g[v][u]=INF; //消除方向边 if(u==s) return ; change(u); } int main() { while(scanf("%d",&n)!=EOF && n) { scanf("%d",&m); memset(g,0x3f,sizeof(g)); for(int i=1; i<=m; i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); g[u][v]=g[v][u]=w; } s=1; t=n; spfa(); sum=d[t]; if(d[t]==INF) //第一次都不能去到终点,说明图不连通 { printf("Back to jail\n"); continue; } change(t); //修改路径上的权和消除反向路径 s=n; t=1; spfa(); //第二次运行最短路 if(d[t]==INF) printf("Back to jail\n"); else printf("%d\n",sum+d[t]); } return 0; }
明天再补上最小费用最大流的思想和代码
最小费用最大流:由于题目规定了起点和终点分别为1和n,所以我们另外设置一个源点和汇点0和n+1,0到1有一条有向边,n到n+1有一条有向边,这两条边的容量都是2,单位费用都是0
另外,题目给的边(无向边),全部拆成两条有向边,容量都是1,单位费用就是原本给的边权。然后求新源点0到新汇点n+1的最小费用最大流,如果最后最大流量为2,那么原问题成功,输出最小费用,最小费用即原问题的解。如果最大流量小于2,那么原问题失败,输出Back to jail
这个算法的正确性是什么呢?其实,原图的边,容量都设置为1,就起到了“每条边只走一次的要求”,因为每次增广后,某些边一定会满流,不会再经过,而从0出发到n+1,如果最后流量为2,那说明其实有两条路径能到达n+1,而从0到n+1,是必须经过1和n的,(别忘了我们怎么设置的那两条特殊的有向边和他们的容量)。而最小费用最大流是用费用来求解最短路,所以我们把边权设置为费用,这样一求,就等价于原问题了
#include <cstdio> #include <cstring> #include <queue> using namespace std; #define N 110 #define M 10010*4 #define INF 0x3f3f3f3f struct edge {int u,v,cap,cost,f,next;}e[M]; //一条有向边的信息有两个顶点,容量,费用,流量,指针 int d[N],first[N],p[M]; int C,F,n,m; void add(int k,int u,int v,int cap,int cost,int f) { e[k].u=u; e[k].v=v; e[k].cap=cap; e[k].cost=cost; e[k].f=f; e[k].next=first[u]; first[u]=k; } void spfa(int s ,int t) {//在还没有达到边的容量的条件下,以边权作为费用来寻找源点到汇点的最短路 queue<int>q; int vis[N]; memset(d,0x3f,sizeof(d)); d[s]=0; memset(vis,0,sizeof(vis)); vis[s]=1; memset(p,-1,sizeof(p)); q.push(s); while(!q.empty()) { int u,v,cap,cost,f; u=q.front(); q.pop(); vis[u]=0; for(int k=first[u]; k!=-1; k=e[k].next) { v=e[k].v; cap=e[k].cap; cost=e[k].cost; f=e[k].f; if(f<cap && d[u]+cost<d[v]) { d[v]=d[u]+cost; p[v]=k; if(!vis[v]) { q.push(v); vis[v]=1; } } } } return ; } void mincost_maxflow() { int s,t; s=0; t=n+1; //求从0到n+1的最小费用最大流 C=F=0; while(1) { spfa(s,t); //找最短路 if(d[t]==INF) //汇点不可达,说明已经达到了最小费用最大流 break; int min=INF; //保存最小残余流量 for(int i=p[t]; i!=-1; i=p[e[i].u]) //沿路径返回 { int cap=e[i].cap , f=e[i].f; min=min<cap-f ? min : cap-f; } for(int i=p[t]; i!=-1; i=p[e[i].u]) //增广 { e[i].f+=min; e[i^1].f-=min; } F+=min; C+=d[t]*min; } } int main() { while(scanf("%d",&n) && n) { scanf("%d",&m); m*=4; memset(first,-1,sizeof(first)); for(int i=0; i<m; i+=4) { int u,v,w; scanf("%d%d%d",&u,&v,&w); //无向图最小费用最大流,1条边要当做4条处理 add(i,u,v,1,w,0); add(i+1,v,u,0,-w,0); add(i+2,v,u,1,w,0); add(i+3,u,v,0,-w,0); } //一条有向边0-->1 add(m,0,1,2,0,0); add(++m,1,0,0,0,0); //一条有向边n-->n+1 add(++m,n,n+1,2,0,0); add(++m,n+1,n,0,0,0); mincost_maxflow(); if(F==2) printf("%d\n",C); else printf("Back to jail\n"); } return 0; }