~~~题面~~~

题解:

吐槽:找了好久的错,换了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了

 #include<bits/stdc++.h>
using namespace std;
#define R register int
#define getchar() *o++
#define AC 100100
#define ac 502000
#define inf 2139062143
char READ[], *o = READ;
/*因为如果没有换乘站的话,是无法改变路线的,因此在没有换乘的情况下,
如果不能一次性到达家,那就是进入了死胡同,,,,
所以一开始先判断一下,如果不能一下到达家,那就必然要经过中转站,
将同行同列且最近的中转站连边,因为中转只需要一秒,所以可以直接记录一个last,
记录最近的路径是从哪里转移的,如果可以从两边转移,那么last记为0,
因为要用到last[x]的情况只有用x更新别人一种,所以不用初始化last,在x更新别人之前它肯定会被别人更新,
所以更新时判断,如果需要转弯,那么时间+1,否则就是板子。
当然这样合法是建立在中转只需要一秒的情况下的,不然就要记录2个方向的情况了,
但是n可以到20000, 这样的话编号不够用???用map???
没事,,,直接存id就好了,不过跳着连边是无意义的,所以只能连最近的边*/
int n, m, cnt;
int dis[AC];
int Next[ac], length[ac], Head[AC], tot;
int head, tail;
bool z[AC];
struct node{
int x, y, id;//直接把id和node绑在一起,就可以不用map了?
}ss, tt, s[AC], q[ac], last[AC], date[ac];//error!!!队列啊啊啊啊啊啊 inline int read()
{
int x = ; char c = getchar();
while(c > '' || c < '') c = getchar();
while(c >= '' && c <= '') x = x * + c -'', c = getchar();
return x;
} bool operator == (node a, node b)
{
if(a.x == b.x && a.y == b.y) return true;
else return false;
} inline bool cmp(node a, node b)
{
if(a.x != b.x) return a.x < b.x;
else return a.y < b.y;
} inline bool cmp1(node a, node b)
{
if(a.y != b.y) return a.y < b.y;
else return a.x < b.x;
} inline void add(node f, node w, int S)
{
date[++tot] = w, Next[tot] = Head[f.id], length[tot]= S, Head[f.id] = tot;
date[++tot] = f, Next[tot] = Head[w.id], length[tot]= S, Head[w.id] = tot;
//printf("(%d, %d) ---> (%d, %d) : %d\n", f.x, f.y, w.x, w.y, S);
} void pre()
{
n = read(), m = read();
cnt = m;
for(R i = ; i <= m; i++)
s[i].x = read(), s[i].y = read();
ss.x = read(), ss.y = read(), tt.x = read(), tt.y = read();
/*for(R i=1;i<=m;i++)//去重(可能和ss or tt重复)
if(s[i] == ss || s[i] == tt)//还是直接就在这里处理干净吧,后面处理太麻烦
{
for(R j=i;j<cnt;j++) s[j] = s[j + 1];//类似与插排
--cnt;
}*///在spfa中加入判断之后就不用去重了
ss.id = cnt + , tt.id = cnt + ;
if(ss.x == tt.x)
{
printf("%d\n",abs(ss.y - tt.y) * );
exit();
}
else if(ss.y == tt.y)
{
printf("%d\n",abs(ss.x - tt.x) * );
exit();
}
memset(dis, , sizeof(dis));
} void spfa()
{
node x, now; int go;
q[++tail] = ss, dis[ss.id] = , z[ss.id] = true;
while(head < tail)
{
x = q[++head];
z[x.id] = false;
for(R i = Head[x.id]; i ; i = Next[i])
{
now = date[i];
go = dis[x.id] + length[i];
if(last[x.id].id && (x.x != now.x || x.y != now.y))//如果需要中转则时间+1,error要特别注意重复元素的处理,,,,重复元素可以看错距离为0的中转。。。
if((x.x == now.x && last[x.id].x != x.x) || (x.y == now.y && last[x.id].y != x.y)) ++go;
if(dis[now.id] > go)
{
last[now.id] = x;//记录点
dis[now.id] = go;
if(!z[now.id])//error!!!只有没有进队列的才加入队列,不然会导致last统计错误
{//因为last统计的正确性正是基于如果一个点x被last[x]更新,那么下次last[x]更新它必然是因为找到了更优解(不然last[x]不会入队)
z[now.id] = true;//但没有这个判断就会导致没有找到更优解却还是二次进入,那么重复进入就会导致下方的else判断错误
q[++tail] = now;//(因为可能被2次更新,但一直没有轮到这个点)
}
}
else if(dis[now.id] == go && x.x != last[now.id].x && x.y != last[now.id].y)
{
last[now.id].id = ;//如果相等的话则需要判断
if(!z[now.id])//error!!!相等也需要加入队列,因为本来可以双向到达而省去中转费的站,可能因为加入队列时机不对而错过
{
z[now.id] = true;
q[++tail] = now;
}
}
}
}
if(dis[tt.id] != inf) printf("%d\n",dis[tt.id]);
else printf("-1\n");
} void build()
{//所以上放那种建图是错误的,,,,,,,特判ss和tt反而会错过一些东西
for(R i = ; i <= cnt; i++) s[i].id = i;//定好编号
s[++cnt] = ss, s[++cnt] = tt;//直接将这两个点加进来岂不是更好,,,,
sort(s + , s + cnt + , cmp);//先按x排序(注意上方的加入s和t要放在确定编号后,因为这两个点的编号是之前就确定了的)
for(R i = ; i <= cnt; i++)//因为连双向边,所以只要判断后面就可以了
if(s[i].x == s[i+].x) add(s[i], s[i+], (s[i+].y - s[i].y) * );
sort(s + , s + cnt + , cmp1);//再按y排一次
for(R i = ; i <= cnt; i++)
if(s[i].y == s[i+].y) add(s[i], s[i+], (s[i+].x - s[i].x) * );
} int main()
{
// freopen("in.in","r",stdin);
fread(READ, , , stdin);
pre();
build();
spfa();
//fclose(stdin);
return ;
}

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

1,建图方法:

  对于一个点,我们将与它同行or同列的所有点连边,边权为距离*2(题目要求) + 1(强制转折)

2,为什么可以这样连边呢?

  因为可以观察到一个转折点如果不转折,那么实际上它是没有任何意义的,因此我们可以当做没有经过它,在图上表现为跳过它直接向那个要转折的点连边,

  由于不知道在哪个点转折,所以只要是同行or同列,每个点都要连边。

3,最后直接跑最短路就可以了,注意一下因为终点也被强制转折了,所以我们输出的时候答案要-1.

 #include<bits/stdc++.h>
using namespace std;
#define R register int
#define getchar() *o++
#define AC 100100
#define ac 1002000
#define inf 2139062143
char READ[], *o = READ;
/*因为如果没有换乘站的话,是无法改变路线的,因此在没有换乘的情况下,
如果不能一次性到达家,那就是进入了死胡同,,,,
所以一开始先判断一下,如果不能一下到达家,那就必然要经过中转站,
将同行同列且最近的中转站连边,因为中转只需要一秒,所以可以直接记录一个last,
记录最近的路径是从哪里转移的,如果可以从两边转移,那么last记为0,
因为要用到last[x]的情况只有用x更新别人一种,所以不用初始化last,在x更新别人之前它肯定会被别人更新,
所以更新时判断,如果需要转弯,那么时间+1,否则就是板子。
当然这样合法是建立在中转只需要一秒的情况下的,不然就要记录2个方向的情况了,
但是n可以到20000, 这样的话编号不够用???用map???
没事,,,直接存id就好了,不过跳着连边是无意义的,所以只能连最近的边*/
int n, m, cnt;
int dis[AC];
int Next[ac], length[ac], Head[AC], tot;
int head, tail;
bool z[AC];
struct node{
int x, y, id;//直接把id和node绑在一起,就可以不用map了?
}ss, tt, s[AC], q[ac], last[AC], date[ac]; inline int read()
{
int x = ; char c = getchar();
while(c > '' || c < '') c = getchar();
while(c >= '' && c <= '') x = x * + c -'', c = getchar();
return x;
} bool operator == (node a, node b)
{
if(a.x == b.x && a.y == b.y) return true;
else return false;
} inline bool cmp(node a, node b)
{
if(a.x != b.x) return a.x < b.x;
else return a.y < b.y;
} inline bool cmp1(node a, node b)
{
if(a.y != b.y) return a.y < b.y;
else return a.x < b.x;
} inline void add(node f, node w, int S)
{
date[++tot] = w, Next[tot] = Head[f.id], length[tot]= S, Head[f.id] = tot;
date[++tot] = f, Next[tot] = Head[w.id], length[tot]= S, Head[w.id] = tot;
//printf("(%d, %d) ---> (%d, %d) : %d\n", f.x, f.y, w.x, w.y, S);
} void pre()
{
n = read(), m = read();
cnt = m;
for(R i = ; i <= m; i++)
s[i].x = read(), s[i].y = read();
ss.x = read(), ss.y = read(), tt.x = read(), tt.y = read();
/*for(R i=1;i<=m;i++)//去重(可能和ss or tt重复)
if(s[i] == ss || s[i] == tt)//还是直接就在这里处理干净吧,后面处理太麻烦
{
for(R j=i;j<cnt;j++) s[j] = s[j + 1];//类似与插排
--cnt;
}*///因为添加了去重的步骤,所以这里的去重也变得不必要了
//在新的建图方式下,,,,,可以直接暴力跑,相当于在枚举那个点作为转折点了
ss.id = cnt + , tt.id = cnt + ;
if(ss.x == tt.x)
{
printf("%d\n",abs(ss.y - tt.y) * );
exit();
}
else if(ss.y == tt.y)
{
printf("%d\n",abs(ss.x - tt.x) * );
exit();
}
memset(dis, , sizeof(dis));
} void spfa()
{
node x, now; int go;
q[++tail] = ss, dis[ss.id] = , z[ss.id] = true;
while(head < tail)
{
x = q[++head];
z[x.id] = false;
for(R i = Head[x.id]; i ; i = Next[i])
{
now = date[i];
go = dis[x.id] + length[i];
if(dis[now.id] > go)
{
last[now.id] = x;//记录点
dis[now.id] = go;
if(!z[now.id])//error!!!只有没有进队列的才加入队列,不然会导致last统计错误
{//因为last统计的正确性正是基于如果一个点x被last[x]更新,那么下次last[x]更新它必然是因为找到了更优解(不然last[x]不会入队)
z[now.id] = true;//但没有这个判断就会导致没有找到更优解却还是二次进入,那么重复进入就会导致下方的else判断错误
q[++tail] = now;//(因为可能被2次更新,但一直没有轮到这个点)
}
}
}
}
if(dis[tt.id] != inf) printf("%d\n",dis[tt.id] - );//这里要-1,因为把这里当中转站的时候在这里也强制转折了一次
else printf("-1\n");
}
//可能我需要更加暴力的做法,,,
//不再向最近的连边,而是向所有同列,同行的都连边。
//因为一个转折点如果不转折的话,那就是无效的,于是在这种方法中它体现为,
//直接跳过了这些点。连到了转折的那个点。
//也就是说强制每个转折点都转折,而不转折的转折点就当做没有经过
//这样虽然可能会多建很多边,但是可以保证正确性, 也不用在额外判断是否是转折点了
//代码复杂度--。。。。。。
void build()
{//所以上放那种建图是错误的,,,,,,,特判ss和tt反而会错过一些东西
for(R i = ; i <= cnt; i++) s[i].id = i;//定好编号
s[++cnt] = ss, s[++cnt] = tt;//直接将这两个点加进来岂不是更好,,,,
sort(s + , s + cnt + , cmp);//先按x排序(注意上方的加入s和t要放在确定编号后,因为这两个点的编号是之前就确定了的)
for(R i = ; i <= cnt; i++)//因为连双向边,所以只要判断后面就可以了
{
int l = i + ;
while(s[i].x == s[l].x)
{
add(s[i], s[l], (s[l].y - s[i].y) * + );
++l;
}
}
sort(s + , s + cnt + , cmp1);//再按y排一次
for(R i = ; i <= cnt; i++)
{
int l = i + ;
while(s[i].y == s[l].y)
{
add(s[i], s[l], (s[l].x - s[i].x) * + );
++l;
}
}
} int main()
{
// freopen("in.in","r",stdin);
fread(READ, , , stdin);
pre();
build();
spfa();
//fclose(stdin);
return ;
}

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

  1. P3831 [SHOI2012]回家的路

    P3831 [SHOI2012]回家的路 分层图基础题,就是建图稍有麻烦   #include<cstdio> #include<algorithm> #include< ...

  2. [SHOI2012]回家的路

    题目背景 SHOI2012 D2T1 题目描述 2046 年 OI 城的城市轨道交通建设终于全部竣工,由于前期规划周密,建成后的轨道交通网络由2n2n条地铁线路构成,组成了一个nn纵nn横的交通网.如 ...

  3. BZOJ.2834.回家的路(最短路Dijkstra 拆点)

    题目链接 对于相邻的.处在同在一行或一列的车站连边,然后用dis[x][0/1](或者拆点)分别表示之前是从横边还是竖边到x的,跑最短路. 我选择拆点.. //13028kb 604ms #inclu ...

  4. 题解 P3831 [SHOI2012]回家的路

    什么叫分层图最短路,我不会/kk 感觉自己做法和其他题解不大一样所以过来发篇题解了. 未刻意卡常拿下最优解 题目大意 就是说给你一个 \(n \times n\) 的网格图和 \(m\) 个可换乘点, ...

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

    2834: 回家的路 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 62  Solved: 38[Submit][Status][Discuss] D ...

  6. 分层图最短路【bzoj2834】: 回家的路

    分层图最短路[bzoj2834]: 回家的路 题目链接:https://www.lydsy.com/JudgeOnline/problem.php?id=2834 这道题难在建边. 自己写的时候想到了 ...

  7. bzoj 2834: 回家的路

    题目 F.A.Qs Home Discuss ProblemSet Status Ranklist Contest 入门OJ ModifyUser  DCOI Logout 捐赠本站 Notice:1 ...

  8. 【bzoj2834】回家的路 分层图最短路

    题目描述 输入 输出 样例输入 2 1 1 2 1 1 2 2 样例输出 5 题解 分层图最短路 dis[i][0]表示到i为横向时起点到i的最短路,dis[i][1]表示到i为纵向时起点到i的最短路 ...

  9. 洛谷P3831 回家的路

    题目背景 SHOI2012 D2T1 题目描述 \(2046\) 年 \(OI\) 城的城市轨道交通建设终于全部竣工,由于前期规划周密,建成后的轨道交通网络由\(2n\)条地铁线路构成,组成了一个\( ...

随机推荐

  1. springboot jpa操作redis

    SpringBoot使用Redis缓存   (1)pom.xml引入jar包,如下: <dependency> <groupId>org.springframework.boo ...

  2. ruby 技巧 根据函数的返回

    一般语言中,函数必须有返回值,即要带个return关键字.但在ruby中,return不是必须的,如果不写会默认返回最终计算的结果.举例 def add(a,b) # 省去了return a + b ...

  3. Docker - 常用命令集

    启动容器 docker run -d -p 58080:8080 -p 58000:8000 --name mytomcat1.0 -v /root/webapps/:/opt/apache-tomc ...

  4. 基于Redis+Kafka的首页曝光过滤方案

    本文来自网易云社区 作者:李勇 背景 网易美学首页除了banner和四个固定位,大部分都是通过算法推荐获取的内容,其中的内容包括心得.合辑.视频及问答等.现在需要实现的是当推荐内容在用户屏幕曝光后(即 ...

  5. Manual install on Windows 7 with Apache and MySQL

    These are instructions for installing on Windows 7 desktop (they may also be useful for a server ins ...

  6. docker in docker

    docker run --rm可以从一个镜像启动容器,并在容器执行完成后自动删除,这在计算任务中非常有用. 例如,我们通过以下步骤完成计算任务容器的启动: 1 将输入数据通过卷挂载方式连接到计算任务容 ...

  7. Charles使用及mock数据

    1.下载charles 3.9.2[破解版地址:https://download.csdn.net/my] 下方有一种方法可破解4.2以前的版本 // Charles Proxy License // ...

  8. Linux命令应用大词典-第12章 程序编译

    12.1 gcc:GNU项目的C和C++编译器 12.2 gdberver:为GNU调试的远程服务器 12.3 cmake:跨平台的Makefile生成工具 12.4 indent:更改通过插入或删除 ...

  9. 352[LeetCode] Data Stream as Disjoint Intervals

    Given a data stream input of non-negative integers a1, a2, ..., an, ..., summarize the numbers seen ...

  10. Simple Pipelined Function

    SELECT * FROM TABLE(PKG_TEST.FN_DIC_DB_TAB) CREATE OR REPLACE PACKAGE PKG_TEST IS   TYPE OBJ_DICDB_R ...