[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 }

原文地址:https://www.cnblogs.com/ww3113306/p/9189087.html

时间: 2024-10-11 18:45:46

[SHOI2012]回家的路 最短路的相关文章

Bzoj 2834: 回家的路 dijkstra,堆优化,分层图,最短路

2834: 回家的路 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 62  Solved: 38[Submit][Status][Discuss] Description Input Output Sample Input 2 1 1 2 1 1 2 2 Sample Output 5 HINT N<=20000,M<=100000 Source dijkstra+堆优化+分层图 把所有的横向和纵向分开看.跑最短路即可. 注意:N这么大,不能写

看到故乡的方向,找到回家的路

朋友从故乡归来,闷闷不乐,问之,他说老家村庄的一棵古树被村里人卖了.他从小在这棵树下长大,对这棵树很有感情.现在这棵树没了,总感觉内心失落落的,故乡不再是完整的故乡了. 我理解他对这棵树的感情.如果说故乡是他储存童年.少年记忆的仓库,那么这棵树,就是一把钥匙,能打开这个仓库的门. 似乎每个从乡村走出来的人,心里都有这么一棵或者几棵树吧.反正我就有.是一棵柏树,古柏,两抱之粗.树龄究竟有几百年,村里没人能说得出来.即使村里如今最年长的康田大爷,也只是说,从他小的时候,树就这么粗. 古柏立在村口,守

你一定不要忘记了回家的路:自己的心灵世界

生活在今日的世界上,心灵的宁静不易得.这个世界既充满着机会,也充满着压力.机会诱惑人去尝试,压力逼迫人去奋斗,都使人静不下心来.我不主张年轻人拒绝任何机会,逃避一切压力,以闭关自守的姿态面对世界.年轻的心灵本不该静如止水,波澜不起.世界是属于年轻人的,趁着年轻到广阔的世界上去闯荡一番,原是人生必要的经历.所须防止的只是,把自己完全交给了机会和压力去支配,在世界上风风火火或浑浑噩噩,迷失了回家的路途. 每到一个陌生的城市,我的习惯是随便走走,好奇心驱使我去探寻这里的热闹的街巷和冷僻的角落.在这途中

记住回家的路

记住回家的路------周国平 生活在今日的世界上,心灵的宁静不易得.这个世界既充满着机会,也充满着压力.机会诱惑人去尝试,压力逼迫人去奋斗,都使人静不下心来.我不主张年轻人拒绝任何机会,逃避一切压力,以闭关自守的姿态面对世界.年轻的心灵本不该静如止水,波澜不起.世界是属于年轻人的,趁着年轻到广阔的世界上去闯荡一番,原是人生必要的经历.所须防止的只是,把自己完全交给了机会和压力去支配,在世界上风风火火或浑浑噩噩,迷失了回家的路途. 每到一个陌生的城市,我的习惯是随便走走,好奇心驱使我去探寻这里的

别陌生了回家的路

最让人不能放下的莫过于日渐老去的父母,最让人归心似箭的莫过于家.岁月默然远去,这种情怀总会历久热切的. 人生之旅,很像一只候鸟,南来北往,北往南来,远离亲人,浪迹天涯处.春日里忘我地耕耘,夏日里艰辛的劳作,期待能在人生之秋有所丰硕.漫漫人生之旅,无论何以繁华,何以鼎盛,总会在人生的渡口,不经意间,回首故土,回首老屋,萌生一种对父母的牵念.倘若岁月静好,父母康健,心里就多了一抹暖. 一个大雪纷纷的冬日,终告别了那一段贫瘠和苦涩的日子,踏上了北去的列车,成为一名空军战士.然而,家,在心田里从没有远去

【SHOI2012】回家的路

2046年OI城的城市轨道交通建设终于全部竣工,由于前期规划周密,建成后的轨道交通网络由2n条地铁 线路构成,组成了一个n纵n横的交通网.如下图所示,这2n条线路每条线路都包含n个车站,而每个车站 都在一组纵横线路的交汇处. 出于建设成本的考虑,并非每个车站都能够进行站内换乘,能够进行站内换乘的地铁站共有m个,在下图 中,标上方块标记的车站为换乘车站.已知地铁运行1站需要2分钟,而站内换乘需要步行1分钟. Serenade想要知道,在不中途出站的前提下,他从学校回家最快需要多少时间(等车时间忽略

ZOJ3088 Easter Holidays spfa 最长路 最短路 路径打印

题目链接: 3088 题意:一个滑雪胜地包含了n 个地点,m 个滑雪斜坡,k 架雪橇,其中2≤n≤1000.1≤m≤1000.1≤k≤1000.滑雪斜坡和雪橇总是从一个地点到另一个地点:滑雪斜坡是从高地点到低地点,而雪橇刚好相反(注意,雪橇不能下降).Per 是一个滑雪初学者,他很害怕雪橇,尽管他想滑得尽可能快.现在,他发现他可以选择不同的雪橇和滑雪斜坡.他现在想这样安排他的滑雪行程: 1) 从一架雪橇的起点出发并最终回到起点. 2) 这个过程分为两个阶段:第一阶段,乘坐一架或多架雪橇上升:第二

使用 Power BI 分析 “回家的路”

很多年以前,不知道怎么就听说,有个小小的转换器,能够把汽车CAN总线转接为串口,然后,就出现了再转成蓝牙或者WIFI接口的小设备.这个小设备能够获取OBD II标准的数据.OBD II的标准广泛用在各种汽车控制器上,能够通过这些数据获得汽车的各种状态数据和告警. 于是很多牛人基于这个小玩意,在电脑特别是手机上开发了不少应用.通过蓝牙或者WIFI获得数据后,处理显示出来.数据的使用多种多样,例如自定义仪表,然后利用挡风玻璃实现HUD抬头显示,利用手机对简单的故障告警进行清楚(消码)-我当时也很有兴

BZOJ 2834 回家的路

分层图最短路.边要开够. #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<queue> #define maxv 300500 #define maxe 2000500 #define inf 2147483647 using namespace std; struct pnt { int x,y,id; }p[maxv]; st