题目

描述

题目大意

给你平面直角坐标系上的nnn个起点和nnn个终点,(x,y)(x,y)(x,y)每次只能走到(x,y+x)(x+y,y)(x,y−x)(x−y,y)(x,y+x)(x+y,y)(x,y-x)(x-y,y)(x,y+x)(x+y,y)(x,y−x)(x−y,y)这些点,每次花费的时间为111。

在走的过程中必须一直在第一象限。

起点和终点没有对应,即可以从某个起点到达任意终点。

问花费的最小的时间和。

题目保证所有的起点都可以到达所有的终点。


思考历程

看到题目的时候很绝望啊……

数据范围这么大,你究竟想干嘛……

这些点走的方式很奇怪,于是我试着往那个方向思考一下,然后没有什么卵用……

于是我把希望寄托在部分分上,最多的步数是500500500。

我突然发现,就算是求出了两点之间的距离,我还是不能快速地匹配,只会暴力全排列……

然后得分的范围继续缩小,我去考虑n=1n=1n=1的情况。

然而我发现,如果暴力宽搜,m=500m=500m=500的情况还是过不去。

似乎可以双向宽搜,但是实现复杂并且没有什么卵用(要打哈希表或者map)。

所以我只会101010分。

感觉分值太小,都不想打了,于是放弃。


正解

这题的正解真的很巧妙。

我们从它们走路的方式入手:看看(x,y−x)(x−y,y)(x,y-x)(x-y,y)(x,y−x)(x−y,y),有没有想到gcdgcdgcd相关的东西?

我比赛时想到了,但根本不知道有什么卵用。

再换一种方式思考一下。其实,假设终点会动,那么起点走到(x,y+x)(x+y,y)(x,y+x)(x+y,y)(x,y+x)(x+y,y)是不是相当于终点走到(x,y−x)(x−y,y)(x,y-x)(x-y,y)(x,y−x)(x−y,y)?

对于每个点(x,y)(x,y)(x,y),我们发现只能去到(x,y−x)(x,y-x)(x,y−x)和(x−y,y)(x-y,y)(x−y,y)其中的一个点,因为另一个点不在第一象限。

发现这个东西像是一棵二叉树,它到父亲走的是(x,y−x)(x,y-x)(x,y−x)或(x−y,y)(x-y,y)(x−y,y),到儿子走的是(x,y+x)(x,y+x)(x,y+x)或(x+y,y)(x+y,y)(x+y,y)。

可以画张图理解一下,这里就不画了……

数据保证所有的起点都可以到达所有的终点,所以所有的点(x,y)(x,y)(x,y)的gcd(x,y)gcd(x,y)gcd(x,y)都是一样的。

想一想如果不一样,那还有到达的可能吗?显然没有。

现在所有的起点和终点都在二叉树上面,想让时间和最小,那么在匹配起点和终点的时候,如果在同一棵子树中就尽量匹配,然后没有匹配完的就伸出去,这样显然最优。

数据范围比较小的时候,暴力将这棵二叉树建出来,在起点上打111的标记,在终点上打−1-1−1的标记,对于每一条边,经过它的点数就是它下面的点中标记的和的绝对值。

这样子就可以过707070分了。可是数据并不友好,如果暴力建树,那么将会特别大。

我们发现这棵树上面有很多点是没有什么卵用的,不妨将它们缩在一起。

看看(x,y)(x,y)(x,y)到(x,y−x)(x,y-x)(x,y−x)或(x−y,y)(x-y,y)(x−y,y),其实很容易联想到gcdgcdgcd。

求gcdgcdgcd的过程就是(x,y)(x,y)(x,y)变成(x,ymod  x)(x,y \mod x)(x,ymodx)或(xmod  y,y)(x \mod y,y)(xmody,y)。

其实gcdgcdgcd就是缩减版的这样的操作。

gcdgcdgcd的时间复杂度是lg⁡\lglg级别的,考虑用gcdgcdgcd过程中的点建成一棵缩减版的二叉树。

对于一个点,求一遍gcdgcdgcd,就可以形成一条链。很容易发现这条链上的每一个点都是转折点,如果一个点是父亲的右儿子,那么它的儿子就是左儿子,反之同理。

为什么?因为在求gcdgcdgcd的过程中,它们的大小关系一定变化的。上一次x&gt;yx&gt;yx>y,下一次x&lt;yx&lt;yx<y。

对于每个点,我们都搞出了这样的一条链,然后我们将这些链合并起来,这棵二叉树就建好了,点数最多为nlg⁡nn \lg nnlgn。

但是,有的时候两条链相交的最低点(也就是两点的LCALCALCA)不在建出来的这棵缩减版的二叉树上,也就如同下图:



黑色的边表示实际二叉树上面的链,红色的边表示利用gcdgcdgcd求出的链上的边。

在用gcdgcdgcd求链之后,因为链上的父亲可能是儿子的好多代祖先,所以在不同的链合并之后,就可能出现一个父亲有很多个儿子的情况,但实际上,这些儿子都是在二叉树中的同一条链上的。

如果它们不在同一条链上,那就是这样:



然而在前面的时候我们已经说过了,在转角的地方应该出现一个点,使得这个点在求gcdgcdgcd的链上。所以说,这样的情况是不存在的。

当我们见到那样的情况的时候该怎么做?

可以将每个点挂在它链上的父亲处,然后建树的时候在父亲那里对于这些点排序,然后依次相连。

建完缩减版二叉树之后就类似之前的做法来做就好了。

时间复杂度O(nlg⁡n∗60)O(n \lg n*60)O(nlgn∗60),可以过。


具体实现

这题的正解理解起来其实还是比较容易的,重点是实现复杂。

YMQ大佬打了6k的代码……

这题的细节特别多。

首先,先把所有gcdgcdgcd路径上的点存起来,并且给予它们一个编号,丢在一个数组里面。

边做gcdgcdgcd边建树会很复杂,所以我们先不要考虑。

其次对它们排个序,然后去重。去重的时候用指针来指向第一个和它一样的点,再将它删去(因为在后面要给它们打标记,所以不能仅仅将其删去)。

接着就是建树:

上面说将点挂在父亲那里,然后排序,依次相连。这样的方法比较复杂,并且要用动态数组或者是链表,感觉上实现很不方便。

我们左右儿子分别处理,由这棵二叉树的性质可得左儿子的xxx坐标和自己相等,右儿子的yyy坐标和自己相等。

现在我们只考虑处理左儿子(右儿子同理):

首先我们按照xxx坐标为第一关键字排序,按照yyy坐标为第二关键字排序。

我们发现xxx相同的所有点是连在一起的,前面的x&gt;yx&gt;yx>y,后面的x≤yx \leq yx≤y。

思考一下这个二叉树的性质:

如果当前的这个点作为左儿子,那么x≤yx\leq yx≤y。

如果当前的这个点作为右儿子,那么x&gt;yx&gt;yx>y。

我们只考虑左儿子,所以只有x≤yx \leq yx≤y的时候才有必要将其挂在链上父亲上,然后排序,连边。

但是,xxx相等的连在一起,并且yyy是递增的,不妨考虑另一种想法:

实际上,yyy已经被排序好了,现在就是要接在父亲那里。

显然这个点的ymod&ThinSpace;&ThinSpace;xy\mod xymodx是父亲的yyy。

链上父亲的编号怎么找?哈希?不(我对哈希总是抱有一种排斥的心理,因为我总觉得它会挂。)。

在xxx相等的这一段中,前面的那一段x&gt;yx&gt;yx>y,链上父亲必定在前面那一段之内。

所以二分就可以了!

对于每个链上父亲,也就是x&gt;yx&gt;yx>y的点,可以记录一个接口,表示如果有左儿子挂在他上面,就应当从接口向这个儿子连一条边。这个接口的初值就是自己。

所以找到父亲之后连到接口上,并且将接口更新为自己。

做完这些操作之后树就建好了。

剩下的直接暴力递归去搞……

有一个特别重要的细节:求完gcdgcdgcd之后,最后一个点是在坐标轴上的。

但是按照二叉树本来的样子,根节点就应该是(gcd,gcd)(gcd,gcd)(gcd,gcd)。

我在打程序的时候一直在思考着这个东西该怎么处理,需要特殊处理吗?

后来我决定将这个在坐标轴上的点保留下来,并且在点的数组当中加入(gcd,gcd)(gcd,gcd)(gcd,gcd)。

在建树的时候,由于(gcd,gcd)(gcd,gcd)(gcd,gcd)一定是最小的,其它不相同的点本来认(gcd,0)(gcd,0)(gcd,0)或(0,gcd)(0,gcd)(0,gcd)为父亲,但由于(gcd,gcd)(gcd,gcd)(gcd,gcd)的存在,它们都会认(gcd,gcd)(gcd,gcd)(gcd,gcd)为父亲。

于是递归的时候就可以愉快地从(gcd,gcd)(gcd,gcd)(gcd,gcd)开始了!


代码

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 50000
#define ALL 3000000
int n;
struct DOT{
long long x[2];
};
inline bool operator==(const DOT &a,const DOT &b){
return a.x[0]==b.x[0] && a.x[1]==b.x[1];
}
DOT dog[N+1],hom[N+1];
int dn[N+1],hn[N+1];
DOT ad[ALL];
int cnt;
int p[ALL];
inline void getdot(DOT d){//将gcd路径上的点找出来
while (d.x[0] && d.x[1]){
ad[++cnt]=d;
if (d.x[0]>d.x[1])
d.x[0]%=d.x[1];
else
d.x[1]%=d.x[0];
}
ad[++cnt]=d;
}
bool M;//表示点的标准,按照左儿子排序还是右儿子排序,建左链还是右链……
inline bool cmp(int a,int b){
return ad[a].x[M]<ad[b].x[M] || ad[a].x[M]==ad[b].x[M] && ad[a].x[!M]<ad[b].x[!M];
}
int num[ALL];//表示指针(指向第一个和自己相同的点)
int itf[ALL];//接口
int to[ALL][2];//边的指向
long long len[ALL][2];//边权
inline void build(){
for (int i=1,l=0,r=0;i<=cnt;++i){//l,r表示x>y的区间,也就是作为链上父亲的区间
if (ad[p[i]].x[M]==0){//其实这种第一关键字为0的情况只有一个……要判掉,不然可能RE
itf[i]=p[i];
continue;
}
if (ad[p[i]].x[M]!=ad[p[i-1]].x[M])
l=i;
if (ad[p[i]].x[M]>ad[p[i]].x[!M])
itf[i]=p[i],r=i;
else{
long long dv=ad[p[i]].x[!M]/ad[p[i]].x[M],md=ad[p[i]].x[!M]-ad[p[i]].x[M]*dv;
int lb=l,rb=r,q=r+1;
while (lb<=rb){
int mid=lb+rb>>1;
if (ad[p[mid]].x[!M]>=md)
rb=(q=mid)-1;
else
lb=mid+1;
}
to[itf[q]][M]=p[i];//连边
len[itf[q]][M]=(ad[p[i]].x[!M]-ad[itf[q]].x[!M])/ad[itf[q]].x[M];//计算边权
itf[q]=p[i];
}
}
}
int sum[ALL];
long long ans;
int root;
void dfs(int x){//遍历二叉树计算答案
if (!x)
return;
dfs(to[x][0]),dfs(to[x][1]);
if (x!=num[root])
ans+=abs(sum[to[x][0]])*len[x][0]+abs(sum[to[x][1]])*len[x][1];
sum[x]+=sum[to[x][0]]+sum[to[x][1]];
}
int main(){
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
scanf("%d",&n);
for (int i=1;i<=n;++i){
scanf("%lld%lld",&dog[i].x[0],&dog[i].x[1]);
dn[i]=cnt+1;//表示它的编号,下面的同理
getdot(dog[i]);
}
for (int i=1;i<=n;++i){
scanf("%lld%lld",&hom[i].x[0],&hom[i].x[1]);
hn[i]=cnt+1;
getdot(hom[i]);
}
long long g=ad[cnt].x[0]|ad[cnt].x[1];
ad[++cnt]={g,0},ad[++cnt]={0,g};
ad[++cnt]={g,g};//将(g,g)加入,另外加入(g,0)(0,g),防止出现没有这些点而出现的莫名错误
root=cnt;
for (int i=1;i<=cnt;++i)
p[i]=i;
M=0,sort(p+1,p+cnt+1,cmp);
for (int i=1;i<=cnt;++i)
if (ad[p[i]]==ad[p[i-1]])
num[p[i]]=num[p[i-1]];//指针指向第一个和它一样的
else
num[p[i]]=p[i];
int tmp=0;
for (int i=1;i<=cnt;++i)
if (num[p[i]]==p[i])
p[++tmp]=p[i];//去重
cnt=tmp;
build();
M=1,sort(p+1,p+cnt+1,cmp);
build();
for (int i=1;i<=n;++i)
sum[num[dn[i]]]++;
for (int i=1;i<=n;++i)
sum[num[hn[i]]]--;
dfs(num[root]);
printf("%lld\n",ans);
return 0;
}

总结

在见到匹配一类的题的时候,有时要往树上想一想。

当数据过大的时候,将一些没必要的东西忽略或者缩在一起也是一种比较好的选择。

然后就是代码一定要打得优美!像我一样!

[JZOJ6011] 【NOIP2019模拟1.25A组】天天爱跑步的更多相关文章

  1. 【NOIP2014模拟10.25A组】画矩形

    题目 分析 由于要求按时间顺序来操作,考虑整体二分: 对于一段二分出来的区间,将左区间的修改和右区间的查询取出来,每次更新每个查询的答案,正确性显然. 现在有一对修改和查询的操作(保证所有的查询都在修 ...

  2. [NOIp2016提高组]天天爱跑步

    题目大意: 有一棵n个点的树,每个点上有一个摄像头会在第w[i]秒拍照. 有m个人再树上跑,第i个人沿着s[i]到t[i]的路径跑,每秒钟跑一条边. 跑到t[i]的下一秒,人就会消失. 问每个摄像头会 ...

  3. P1600 [NOIP2016 提高组] 天天爱跑步 (树上差分)

    对于一条路径,s-t,位于该路径上的观察员能观察到运动员当且仅当以下两种情况成立:(d[ ]表示节点深度) 1.观察员x在s-lca(s,t)上时,满足d[s]=d[x]+w[x]就能观察到,所以我们 ...

  4. JZOJ 4273. 【NOIP2015模拟10.28B组】圣章-精灵使的魔法语

    4273. [NOIP2015模拟10.28B组]圣章-精灵使的魔法语 (File IO): input:elf.in output:elf.out Time Limits: 1000 ms  Mem ...

  5. JZOJ 3509. 【NOIP2013模拟11.5B组】倒霉的小C

    3509. [NOIP2013模拟11.5B组]倒霉的小C(beats) (File IO): input:beats.in output:beats.out Time Limits: 1000 ms ...

  6. JZOJ 3508. 【NOIP2013模拟11.5B组】好元素

    3508. [NOIP2013模拟11.5B组]好元素(good) (File IO): input:good.in output:good.out Time Limits: 2000 ms  Mem ...

  7. JZOJ 4272. 【NOIP2015模拟10.28B组】序章-弗兰德的秘密

    272. [NOIP2015模拟10.28B组]序章-弗兰德的秘密 (File IO): input:frand.in output:frand.out Time Limits: 1000 ms  M ...

  8. FreeRTOS 任务通知模拟事件标志组

    实验 //设置事件位的任务 void eventsetbit_task(void *pvParameters) { u8 key; while(1) { if(EventGroupTask_Handl ...

  9. 2017.1.16【初中部 】普及组模拟赛C组总结

    2017.1.16[初中部 ]普及组模拟赛C组 这次总结我赶时间,不写这么详细了. 话说这次比赛,我虽然翻了个大车,但一天之内AK,我感到很高兴 比赛 0+15+0+100=115 改题 AK 一.c ...

随机推荐

  1. 全局唯一标识符(GUID,Globally Unique Identifier)

    全局唯一标识符(GUID,Globally Unique Identifier)是一种由算法生成的二进制长度为128位的数字标识符.GUID主要用于在拥有多个节点.多台计算机的网络或系统中.在理想情况 ...

  2. https://segmentfault.com 一个学习网站

    https://segmentfault.com一个学习网站

  3. RN书签

    Bookmarks 书签栏 门户 配置reactNative(RN)过程中 出现react-native:command not... - 简书 * React-NativeChannel - sma ...

  4. (转)Python之路,Day6 - 面向对象学习

    本节内容:   面向对象编程介绍 为什么要用面向对象进行开发? 面向对象的特性:封装.继承.多态 类.方法.     引子 你现在是一家游戏公司的开发人员,现在需要你开发一款叫做<人狗大战> ...

  5. solr 如何实现精确查询

    第一条和第三条不应该出现的. 解决办法

  6. SimpleDateFormat日期格式

    前言 java中使用SimpleDateFormat类的构造函数SimpleDateFormat(String str)构造格式化日期的格式,通过format(Date date)方法将指定的日期对象 ...

  7. HDFS API 操作实例(二) 目录操作

    1. 递归读取文件名 1.1 递归实现读取文件名(scala + listFiles) /** * 实现:listFiles方法 * 迭代列出文件夹下的文件,只能列出文件 * 通过fs的listFil ...

  8. Linux 实用指令(9)--进程管理

    目录 进程管理 1 进程的基本介绍 2 显示系统执行的进程 2.1 说明: 2.2 ps指令详解 2.3 应用实例 3 终止进程kill和killall 3.1 介绍 3.2 基本语法 3.3 常用选 ...

  9. redis集群的学习(一)

    redis配置文件详解 redis默认是不作为守护进程来运行的,你可以把这个设置为yes,让它作为守护进程来运行 注意,当作为守护进程的时候,redis 会把进程ID 写到/var/run/redis ...

  10. CSIC_716_20191107【深拷贝、文件的编码解码、文件的打开模式】

    深拷贝和浅拷贝 列表的拷贝,用copy方法浅拷贝,新列表和被拷贝列表的id是不一样的. list1 = [1, 'ss', (5, 6), ['p', 'w','M'], {'key1': 'valu ...