[SHOI2012]回家的路 最短路
题解:
吐槽:找了好久的错,换了n种方法,重构一次代码,,,,
最后发现,,,
数组开小了,其实一开始尝试开大了数组,但唯独没有尝试开大手写队列的数组。。。。
思路:
有两种方法,这里都介绍一下吧,分别在时间复杂度和代码复杂度上各有优势。
第一种:时间复杂度更优,代码复杂
观察到转弯时需要多消耗1的费用,不转弯则不用。因此我们记录一个last表示这个点的最短路是从哪走来的。(其实就是记录路径)
然后注意到A ---> C 与A ---> B ---> C是等效的,因此我们可以直接向最近的转折点连边。
跑最短路即可。
洛谷题解里面有一篇跟这个思路基本相同,但有些细节没有注意到,于是我发现了一组数据可以hack掉这篇题解。。。
这种方法细节很多,下面来总结一下一些可能会陷入的误区(要注意的细节):
1,重复元素的处理。
有两种选择:去重 or 特判;
由于去重无论在时间复杂度还是代码复杂度上都占劣势,这里选择特判,方法就是在spfa判断是否需要转折的时候加一句x 和 now必须不同就可以了
2,last的统计
last统计的应是当前找到的最短路径上节点u的上一个节点,这样就可以判断转折了。
但我们注意到有这么一种情况,最短路可能有2条,而因为一个点只会向最近的两个点连边,所以一旦最短路超过一条,就代表当前找到的最小权值可以从两种方向不同的方案得到。
也就意味了无论去往哪个方向,都有无需转折的方案。所以一旦我们找到一条长度与当前路径相同的路径,且转移点不与被转移点相同(x != now),那么我们可以判定这个点不需要转折的费用。
但这样就够了吗?这也是很容易陷入的一个误区。
因为可能有这样一种情况:
出现了这么一个点,到达它的最短路径有2条,即符合无需转折的条件,但是找到这两条最短路的时间不同,在找到第一条最短路并将其入队后,在找到第二条最短路前,
它已经成功出队并且更新了一个必经节点(即正确答案所需节点),但由于需要转折,它给到达这个必经节点的路径长增加了1,但是实际上这个点是无需转折的,
所以它给这个必经节点新增的1就是不需要的,于是我们就得到了一个比正确答案大1的答案(如果这种情况多次出现则可能不止相差1),这也是我认为的上面那篇题解会被hack的原因。
那么如何解决这个问题呢?
其实很简单,我们观察到之所以会出现这样的情况,是因为只有dis[x]被更新时才会将x加入队列以更新其他点,这在普通的spfa中当然是正确的,因为dis[x]不被更新,x当然无法找到更短的路来更新别人,
但这里是不同的,因为它多了一个是否转折的影响,因此当这个条件被改变时,我们也应将其加入队列,因为它现在又有可能更新别人了。所以我们在找到第二条路径时也将其加入队列即可。
3,点编号的记录问题
当我码到一半的时候,,,发现直接用行列来计算的编号由于n很大,将会变得非常大,这时用数组肯定是存不下的,那怎么办呢?用map?
其实不用,我们可以直接新建结构体,在记录一个节点的位置信息时,顺便记录id,然后在之后用到这个点的过程中,都直接用结构体储存相关信息(包括链式前向星)。
于是我们就可以很方便的得知一个点的id了
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define R register int 4 #define getchar() *o++ 5 #define AC 100100 6 #define ac 502000 7 #define inf 2139062143 8 char READ[7000100], *o = READ; 9 /*因为如果没有换乘站的话,是无法改变路线的,因此在没有换乘的情况下, 10 如果不能一次性到达家,那就是进入了死胡同,,,, 11 所以一开始先判断一下,如果不能一下到达家,那就必然要经过中转站, 12 将同行同列且最近的中转站连边,因为中转只需要一秒,所以可以直接记录一个last, 13 记录最近的路径是从哪里转移的,如果可以从两边转移,那么last记为0, 14 因为要用到last[x]的情况只有用x更新别人一种,所以不用初始化last,在x更新别人之前它肯定会被别人更新, 15 所以更新时判断,如果需要转弯,那么时间+1,否则就是板子。 16 当然这样合法是建立在中转只需要一秒的情况下的,不然就要记录2个方向的情况了, 17 但是n可以到20000, 这样的话编号不够用???用map??? 18 没事,,,直接存id就好了,不过跳着连边是无意义的,所以只能连最近的边*/ 19 int n, m, cnt; 20 int dis[AC]; 21 int Next[ac], length[ac], Head[AC], tot; 22 int head, tail; 23 bool z[AC]; 24 struct node{ 25 int x, y, id;//直接把id和node绑在一起,就可以不用map了? 26 }ss, tt, s[AC], q[ac], last[AC], date[ac];//error!!!队列啊啊啊啊啊啊 27 28 inline int read() 29 { 30 int x = 0; char c = getchar(); 31 while(c > '9' || c < '0') c = getchar(); 32 while(c >= '0' && c <= '9') x = x * 10 + c -'0', c = getchar(); 33 return x; 34 } 35 36 bool operator == (node a, node b) 37 { 38 if(a.x == b.x && a.y == b.y) return true; 39 else return false; 40 } 41 42 inline bool cmp(node a, node b) 43 { 44 if(a.x != b.x) return a.x < b.x; 45 else return a.y < b.y; 46 } 47 48 inline bool cmp1(node a, node b) 49 { 50 if(a.y != b.y) return a.y < b.y; 51 else return a.x < b.x; 52 } 53 54 inline void add(node f, node w, int S) 55 { 56 date[++tot] = w, Next[tot] = Head[f.id], length[tot]= S, Head[f.id] = tot; 57 date[++tot] = f, Next[tot] = Head[w.id], length[tot]= S, Head[w.id] = tot; 58 //printf("(%d, %d) ---> (%d, %d) : %d\n", f.x, f.y, w.x, w.y, S); 59 } 60 61 void pre() 62 { 63 n = read(), m = read(); 64 cnt = m; 65 for(R i = 1; i <= m; i++) 66 s[i].x = read(), s[i].y = read(); 67 ss.x = read(), ss.y = read(), tt.x = read(), tt.y = read(); 68 /*for(R i=1;i<=m;i++)//去重(可能和ss or tt重复) 69 if(s[i] == ss || s[i] == tt)//还是直接就在这里处理干净吧,后面处理太麻烦 70 { 71 for(R j=i;j<cnt;j++) s[j] = s[j + 1];//类似与插排 72 --cnt; 73 }*///在spfa中加入判断之后就不用去重了 74 ss.id = cnt + 1, tt.id = cnt + 2; 75 if(ss.x == tt.x) 76 { 77 printf("%d\n",abs(ss.y - tt.y) * 2); 78 exit(0); 79 } 80 else if(ss.y == tt.y) 81 { 82 printf("%d\n",abs(ss.x - tt.x) * 2); 83 exit(0); 84 } 85 memset(dis, 127, sizeof(dis)); 86 } 87 88 void spfa() 89 { 90 node x, now; int go; 91 q[++tail] = ss, dis[ss.id] = 0, z[ss.id] = true; 92 while(head < tail) 93 { 94 x = q[++head]; 95 z[x.id] = false; 96 for(R i = Head[x.id]; i ; i = Next[i]) 97 { 98 now = date[i]; 99 go = dis[x.id] + length[i]; 100 if(last[x.id].id && (x.x != now.x || x.y != now.y))//如果需要中转则时间+1,error要特别注意重复元素的处理,,,,重复元素可以看错距离为0的中转。。。 101 if((x.x == now.x && last[x.id].x != x.x) || (x.y == now.y && last[x.id].y != x.y)) ++go; 102 if(dis[now.id] > go) 103 { 104 last[now.id] = x;//记录点 105 dis[now.id] = go; 106 if(!z[now.id])//error!!!只有没有进队列的才加入队列,不然会导致last统计错误 107 {//因为last统计的正确性正是基于如果一个点x被last[x]更新,那么下次last[x]更新它必然是因为找到了更优解(不然last[x]不会入队) 108 z[now.id] = true;//但没有这个判断就会导致没有找到更优解却还是二次进入,那么重复进入就会导致下方的else判断错误 109 q[++tail] = now;//(因为可能被2次更新,但一直没有轮到这个点) 110 } 111 } 112 else if(dis[now.id] == go && x.x != last[now.id].x && x.y != last[now.id].y) 113 { 114 last[now.id].id = 0;//如果相等的话则需要判断 115 if(!z[now.id])//error!!!相等也需要加入队列,因为本来可以双向到达而省去中转费的站,可能因为加入队列时机不对而错过 116 { 117 z[now.id] = true; 118 q[++tail] = now; 119 } 120 } 121 } 122 } 123 if(dis[tt.id] != inf) printf("%d\n",dis[tt.id]); 124 else printf("-1\n"); 125 } 126 127 void build() 128 {//所以上放那种建图是错误的,,,,,,,特判ss和tt反而会错过一些东西 129 for(R i = 1; i <= cnt; i++) s[i].id = i;//定好编号 130 s[++cnt] = ss, s[++cnt] = tt;//直接将这两个点加进来岂不是更好,,,, 131 sort(s + 1, s + cnt + 1, cmp);//先按x排序(注意上方的加入s和t要放在确定编号后,因为这两个点的编号是之前就确定了的) 132 for(R i = 1; i <= cnt; i++)//因为连双向边,所以只要判断后面就可以了 133 if(s[i].x == s[i+1].x) add(s[i], s[i+1], (s[i+1].y - s[i].y) * 2); 134 sort(s + 1, s + cnt + 1, cmp1);//再按y排一次 135 for(R i = 1; i <= cnt; i++) 136 if(s[i].y == s[i+1].y) add(s[i], s[i+1], (s[i+1].x - s[i].x) * 2); 137 } 138 139 int main() 140 { 141 // freopen("in.in","r",stdin); 142 fread(READ, 7000000, 1, stdin); 143 pre(); 144 build(); 145 spfa(); 146 //fclose(stdin); 147 return 0; 148 }
第二种:时间复杂度与空间复杂度稍大,但实现简单,细节很少,思路易懂
1,建图方法:
对于一个点,我们将与它同行or同列的所有点连边,边权为距离*2(题目要求) + 1(强制转折)
2,为什么可以这样连边呢?
因为可以观察到一个转折点如果不转折,那么实际上它是没有任何意义的,因此我们可以当做没有经过它,在图上表现为跳过它直接向那个要转折的点连边,
由于不知道在哪个点转折,所以只要是同行or同列,每个点都要连边。
3,最后直接跑最短路就可以了,注意一下因为终点也被强制转折了,所以我们输出的时候答案要-1.
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define R register int 4 #define getchar() *o++ 5 #define AC 100100 6 #define ac 1002000 7 #define inf 2139062143 8 char READ[7000100], *o = READ; 9 /*因为如果没有换乘站的话,是无法改变路线的,因此在没有换乘的情况下, 10 如果不能一次性到达家,那就是进入了死胡同,,,, 11 所以一开始先判断一下,如果不能一下到达家,那就必然要经过中转站, 12 将同行同列且最近的中转站连边,因为中转只需要一秒,所以可以直接记录一个last, 13 记录最近的路径是从哪里转移的,如果可以从两边转移,那么last记为0, 14 因为要用到last[x]的情况只有用x更新别人一种,所以不用初始化last,在x更新别人之前它肯定会被别人更新, 15 所以更新时判断,如果需要转弯,那么时间+1,否则就是板子。 16 当然这样合法是建立在中转只需要一秒的情况下的,不然就要记录2个方向的情况了, 17 但是n可以到20000, 这样的话编号不够用???用map??? 18 没事,,,直接存id就好了,不过跳着连边是无意义的,所以只能连最近的边*/ 19 int n, m, cnt; 20 int dis[AC]; 21 int Next[ac], length[ac], Head[AC], tot; 22 int head, tail; 23 bool z[AC]; 24 struct node{ 25 int x, y, id;//直接把id和node绑在一起,就可以不用map了? 26 }ss, tt, s[AC], q[ac], last[AC], date[ac]; 27 28 inline int read() 29 { 30 int x = 0; char c = getchar(); 31 while(c > '9' || c < '0') c = getchar(); 32 while(c >= '0' && c <= '9') x = x * 10 + c -'0', c = getchar(); 33 return x; 34 } 35 36 bool operator == (node a, node b) 37 { 38 if(a.x == b.x && a.y == b.y) return true; 39 else return false; 40 } 41 42 inline bool cmp(node a, node b) 43 { 44 if(a.x != b.x) return a.x < b.x; 45 else return a.y < b.y; 46 } 47 48 inline bool cmp1(node a, node b) 49 { 50 if(a.y != b.y) return a.y < b.y; 51 else return a.x < b.x; 52 } 53 54 inline void add(node f, node w, int S) 55 { 56 date[++tot] = w, Next[tot] = Head[f.id], length[tot]= S, Head[f.id] = tot; 57 date[++tot] = f, Next[tot] = Head[w.id], length[tot]= S, Head[w.id] = tot; 58 //printf("(%d, %d) ---> (%d, %d) : %d\n", f.x, f.y, w.x, w.y, S); 59 } 60 61 void pre() 62 { 63 n = read(), m = read(); 64 cnt = m; 65 for(R i = 1; i <= m; i++) 66 s[i].x = read(), s[i].y = read(); 67 ss.x = read(), ss.y = read(), tt.x = read(), tt.y = read(); 68 /*for(R i=1;i<=m;i++)//去重(可能和ss or tt重复) 69 if(s[i] == ss || s[i] == tt)//还是直接就在这里处理干净吧,后面处理太麻烦 70 { 71 for(R j=i;j<cnt;j++) s[j] = s[j + 1];//类似与插排 72 --cnt; 73 }*///因为添加了去重的步骤,所以这里的去重也变得不必要了 74 //在新的建图方式下,,,,,可以直接暴力跑,相当于在枚举那个点作为转折点了 75 ss.id = cnt + 1, tt.id = cnt + 2; 76 if(ss.x == tt.x) 77 { 78 printf("%d\n",abs(ss.y - tt.y) * 2); 79 exit(0); 80 } 81 else if(ss.y == tt.y) 82 { 83 printf("%d\n",abs(ss.x - tt.x) * 2); 84 exit(0); 85 } 86 memset(dis, 127, sizeof(dis)); 87 } 88 89 void spfa() 90 { 91 node x, now; int go; 92 q[++tail] = ss, dis[ss.id] = 0, z[ss.id] = true; 93 while(head < tail) 94 { 95 x = q[++head]; 96 z[x.id] = false; 97 for(R i = Head[x.id]; i ; i = Next[i]) 98 { 99 now = date[i]; 100 go = dis[x.id] + length[i]; 101 if(dis[now.id] > go) 102 { 103 last[now.id] = x;//记录点 104 dis[now.id] = go; 105 if(!z[now.id])//error!!!只有没有进队列的才加入队列,不然会导致last统计错误 106 {//因为last统计的正确性正是基于如果一个点x被last[x]更新,那么下次last[x]更新它必然是因为找到了更优解(不然last[x]不会入队) 107 z[now.id] = true;//但没有这个判断就会导致没有找到更优解却还是二次进入,那么重复进入就会导致下方的else判断错误 108 q[++tail] = now;//(因为可能被2次更新,但一直没有轮到这个点) 109 } 110 } 111 } 112 } 113 if(dis[tt.id] != inf) printf("%d\n",dis[tt.id] - 1);//这里要-1,因为把这里当中转站的时候在这里也强制转折了一次 114 else printf("-1\n"); 115 } 116 //可能我需要更加暴力的做法,,, 117 //不再向最近的连边,而是向所有同列,同行的都连边。 118 //因为一个转折点如果不转折的话,那就是无效的,于是在这种方法中它体现为, 119 //直接跳过了这些点。连到了转折的那个点。 120 //也就是说强制每个转折点都转折,而不转折的转折点就当做没有经过 121 //这样虽然可能会多建很多边,但是可以保证正确性, 也不用在额外判断是否是转折点了 122 //代码复杂度--。。。。。。 123 void build() 124 {//所以上放那种建图是错误的,,,,,,,特判ss和tt反而会错过一些东西 125 for(R i = 1; i <= cnt; i++) s[i].id = i;//定好编号 126 s[++cnt] = ss, s[++cnt] = tt;//直接将这两个点加进来岂不是更好,,,, 127 sort(s + 1, s + cnt + 1, cmp);//先按x排序(注意上方的加入s和t要放在确定编号后,因为这两个点的编号是之前就确定了的) 128 for(R i = 1; i <= cnt; i++)//因为连双向边,所以只要判断后面就可以了 129 { 130 int l = i + 1; 131 while(s[i].x == s[l].x) 132 { 133 add(s[i], s[l], (s[l].y - s[i].y) * 2 + 1); 134 ++l; 135 } 136 } 137 sort(s + 1, s + cnt + 1, cmp1);//再按y排一次 138 for(R i = 1; i <= cnt; i++) 139 { 140 int l = i + 1; 141 while(s[i].y == s[l].y) 142 { 143 add(s[i], s[l], (s[l].x - s[i].x) * 2 + 1); 144 ++l; 145 } 146 } 147 } 148 149 int main() 150 { 151 // freopen("in.in","r",stdin); 152 fread(READ, 7000000, 1, stdin); 153 pre(); 154 build(); 155 spfa(); 156 //fclose(stdin); 157 return 0; 158 }