zoukankan      html  css  js  c++  java
  • [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
    ", 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
    ",abs(ss.y - tt.y) * 2);
     78         exit(0);
     79     }
     80     else if(ss.y == tt.y)
     81     {
     82         printf("%d
    ",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
    ",dis[tt.id]);
    124     else printf("-1
    ");
    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 }
    View Code

    第二种:时间复杂度与空间复杂度稍大,但实现简单,细节很少,思路易懂

    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
    ", 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
    ",abs(ss.y - tt.y) * 2);
     79         exit(0);
     80     }
     81     else if(ss.y == tt.y)
     82     {
     83         printf("%d
    ",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
    ",dis[tt.id] - 1);//这里要-1,因为把这里当中转站的时候在这里也强制转折了一次
    114     else printf("-1
    ");
    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 }
    View Code
  • 相关阅读:
    Apache 虚拟主机 VirtualHost 配置
    EAX、ECX、EDX、EBX寄存器的作用
    Python中文文档 目录(转载)
    八度
    POJ 3268 Silver Cow Party (最短路)
    POJ 2253 Frogger (求每条路径中最大值的最小值,Dijkstra变形)
    2013金山西山居创意游戏程序挑战赛——复赛(1) HDU 4557 非诚勿扰 HDU 4558 剑侠情缘 HDU 4559 涂色游戏 HDU 4560 我是歌手
    HDU 4549 M斐波那契数列(矩阵快速幂+欧拉定理)
    UVA 11624 Fire! (简单图论基础)
    HDU 3534 Tree (树形DP)
  • 原文地址:https://www.cnblogs.com/ww3113306/p/9189087.html
Copyright © 2011-2022 走看看