[JZOJ6011] 【NOIP2019模拟1.25A组】天天爱跑步

题目

描述

在这里插入图片描述

题目大意

给你平面直角坐标系上的 n n n个起点和 n n n个终点, ( 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,yx)(xy,y)这些点,每次花费的时间为 1 1 1
在走的过程中必须一直在第一象限。
起点和终点没有对应,即可以从某个起点到达任意终点。
问花费的最小的时间和。
题目保证所有的起点都可以到达所有的终点。


思考历程

看到题目的时候很绝望啊……
数据范围这么大,你究竟想干嘛……
这些点走的方式很奇怪,于是我试着往那个方向思考一下,然后没有什么卵用……
于是我把希望寄托在部分分上,最多的步数是 500 500 500
我突然发现,就算是求出了两点之间的距离,我还是不能快速地匹配,只会暴力全排列……
然后得分的范围继续缩小,我去考虑 n = 1 n=1 n=1的情况。
然而我发现,如果暴力宽搜, m = 500 m=500 m=500的情况还是过不去。
似乎可以双向宽搜,但是实现复杂并且没有什么卵用(要打哈希表或者map)。
所以我只会 10 10 10分。
感觉分值太小,都不想打了,于是放弃。


正解

这题的正解真的很巧妙。
我们从它们走路的方式入手:看看 ( x , y − x ) ( x − y , y ) (x,y-x)(x-y,y) (x,yx)(xy,y),有没有想到 g c d gcd gcd相关的东西?
我比赛时想到了,但根本不知道有什么卵用。
再换一种方式思考一下。其实,假设终点会动,那么起点走到 ( 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,yx)(xy,y)
对于每个点 ( x , y ) (x,y) (x,y),我们发现只能去到 ( x , y − x ) (x,y-x) (x,yx) ( x − y , y ) (x-y,y) (xy,y)其中的一个点,因为另一个点不在第一象限。
发现这个东西像是一棵二叉树,它到父亲走的是 ( x , y − x ) (x,y-x) (x,yx) ( x − y , y ) (x-y,y) (xy,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) g c d ( x , y ) gcd(x,y) gcd(x,y)都是一样的。
想一想如果不一样,那还有到达的可能吗?显然没有。

现在所有的起点和终点都在二叉树上面,想让时间和最小,那么在匹配起点和终点的时候,如果在同一棵子树中就尽量匹配,然后没有匹配完的就伸出去,这样显然最优。
数据范围比较小的时候,暴力将这棵二叉树建出来,在起点上打 1 1 1的标记,在终点上打 − 1 -1 1的标记,对于每一条边,经过它的点数就是它下面的点中标记的和的绝对值。
这样子就可以过 70 70 70分了。可是数据并不友好,如果暴力建树,那么将会特别大。

我们发现这棵树上面有很多点是没有什么卵用的,不妨将它们缩在一起。
看看 ( x , y ) (x,y) (x,y) ( x , y − x ) (x,y-x) (x,yx) ( x − y , y ) (x-y,y) (xy,y),其实很容易联想到 g c d gcd gcd
g c d gcd gcd的过程就是 ( x , y ) (x,y) (x,y)变成 ( x , y m o d    x ) (x,y \mod x) (x,ymodx) ( x m o d    y , y ) (x \mod y,y) (xmody,y)
其实 g c d gcd gcd就是缩减版的这样的操作。
g c d gcd gcd的时间复杂度是 lg ⁡ \lg lg级别的,考虑用 g c d gcd gcd过程中的点建成一棵缩减版的二叉树。
对于一个点,求一遍 g c d gcd gcd,就可以形成一条链。很容易发现这条链上的每一个点都是转折点,如果一个点是父亲的右儿子,那么它的儿子就是左儿子,反之同理。
为什么?因为在求 g c d gcd gcd的过程中,它们的大小关系一定变化的。上一次 x &gt; y x&gt;y x>y,下一次 x &lt; y x&lt;y x<y
对于每个点,我们都搞出了这样的一条链,然后我们将这些链合并起来,这棵二叉树就建好了,点数最多为 n lg ⁡ n n \lg n nlgn

但是,有的时候两条链相交的最低点(也就是两点的 L C A LCA LCA)不在建出来的这棵缩减版的二叉树上,也就如同下图:
在这里插入图片描述
黑色的边表示实际二叉树上面的链,红色的边表示利用 g c d gcd gcd求出的链上的边。
在用 g c d gcd gcd求链之后,因为链上的父亲可能是儿子的好多代祖先,所以在不同的链合并之后,就可能出现一个父亲有很多个儿子的情况,但实际上,这些儿子都是在二叉树中的同一条链上的。
如果它们不在同一条链上,那就是这样:
在这里插入图片描述
然而在前面的时候我们已经说过了,在转角的地方应该出现一个点,使得这个点在求 g c d gcd gcd的链上。所以说,这样的情况是不存在的。
当我们见到那样的情况的时候该怎么做?
可以将每个点挂在它链上的父亲处,然后建树的时候在父亲那里对于这些点排序,然后依次相连。

建完缩减版二叉树之后就类似之前的做法来做就好了。
时间复杂度 O ( n lg ⁡ n ∗ 60 ) O(n \lg n*60) O(nlgn60),可以过。


具体实现

这题的正解理解起来其实还是比较容易的,重点是实现复杂。
YMQ大佬打了6k的代码……
这题的细节特别多。

首先,先把所有 g c d gcd gcd路径上的点存起来,并且给予它们一个编号,丢在一个数组里面。
边做 g c d gcd gcd边建树会很复杂,所以我们先不要考虑。
其次对它们排个序,然后去重。去重的时候用指针来指向第一个和它一样的点,再将它删去(因为在后面要给它们打标记,所以不能仅仅将其删去)。

接着就是建树:
上面说将点挂在父亲那里,然后排序,依次相连。这样的方法比较复杂,并且要用动态数组或者是链表,感觉上实现很不方便。
我们左右儿子分别处理,由这棵二叉树的性质可得左儿子的 x x x坐标和自己相等,右儿子的 y y y坐标和自己相等。
现在我们只考虑处理左儿子(右儿子同理):
首先我们按照 x x x坐标为第一关键字排序,按照 y y y坐标为第二关键字排序。
我们发现 x x x相同的所有点是连在一起的,前面的 x &gt; y x&gt;y x>y,后面的 x ≤ y x \leq y xy
思考一下这个二叉树的性质:
如果当前的这个点作为左儿子,那么 x ≤ y x\leq y xy
如果当前的这个点作为右儿子,那么 x &gt; y x&gt;y x>y
我们只考虑左儿子,所以只有 x ≤ y x \leq y xy的时候才有必要将其挂在链上父亲上,然后排序,连边。
但是, x x x相等的连在一起,并且 y y y是递增的,不妨考虑另一种想法:
实际上, y y y已经被排序好了,现在就是要接在父亲那里。
显然这个点的 y m o d &ThinSpace;&ThinSpace; x y\mod x ymodx是父亲的 y y y
链上父亲的编号怎么找?哈希?不(我对哈希总是抱有一种排斥的心理,因为我总觉得它会挂。)。
x x x相等的这一段中,前面的那一段 x &gt; y x&gt;y x>y,链上父亲必定在前面那一段之内。
所以二分就可以了!
对于每个链上父亲,也就是 x &gt; y x&gt;y x>y的点,可以记录一个接口,表示如果有左儿子挂在他上面,就应当从接口向这个儿子连一条边。这个接口的初值就是自己。
所以找到父亲之后连到接口上,并且将接口更新为自己。

做完这些操作之后树就建好了。
剩下的直接暴力递归去搞……

有一个特别重要的细节:求完 g c d gcd gcd之后,最后一个点是在坐标轴上的。
但是按照二叉树本来的样子,根节点就应该是 ( g c d , g c d ) (gcd,gcd) (gcd,gcd)
我在打程序的时候一直在思考着这个东西该怎么处理,需要特殊处理吗?
后来我决定将这个在坐标轴上的点保留下来,并且在点的数组当中加入 ( g c d , g c d ) (gcd,gcd) (gcd,gcd)
在建树的时候,由于 ( g c d , g c d ) (gcd,gcd) (gcd,gcd)一定是最小的,其它不相同的点本来认 ( g c d , 0 ) (gcd,0) (gcd,0) ( 0 , g c d ) (0,gcd) (0,gcd)为父亲,但由于 ( g c d , g c d ) (gcd,gcd) (gcd,gcd)的存在,它们都会认 ( g c d , g c d ) (gcd,gcd) (gcd,gcd)为父亲。
于是递归的时候就可以愉快地从 ( g c d , g c d ) (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;
}

总结

在见到匹配一类的题的时候,有时要往树上想一想。
当数据过大的时候,将一些没必要的东西忽略或者缩在一起也是一种比较好的选择。
然后就是代码一定要打得优美!像我一样!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值