跳跳棋
跳跳棋( 思维题\(\star\star\star \))
- 时限:\(1s\) 内存:\(256M\)
Descrption
-
跳跳棋是在一条数轴上进行的。棋子只能摆在整点上。每个点不能摆超过一个棋子。
-
我们用跳跳棋来做一个简单的游戏:棋盘上有 \(3\) 颗棋子,分别在 \(a,b,c\) 这三个位置。我们要通过最少的跳动把它们的位置移动成 \(x,y,z\)。(棋子是没有区别的)
-
跳动的规则很简单,任意选一颗棋子,对一颗中轴棋子跳动。跳动后两颗棋子距离不变。一次只允许跳过 \(1\) 颗棋子。
-
写一个程序,首先判断是否可以完成任务。如果可以,输出最少需要的跳动次数。
Input
- 第一行包含三个整数,表示当前棋子的位置 \(a\ b\ c\)。(互不相同)
- 第二行包含三个整数,表示目标位置 \(x\ y\ z\)。(互不相同)
Output
- 如果无解,输出一行 \(NO\)。
- 如果可以到达,第一行输出 \(YES\),第二行输出最少步数。
Sample Input
1 2 3
0 3 5
Sample Output
YES
2
Hint
- \(20\%\) 输入整数的绝对值均不超过 \(10\)。
- \(40\%\) 输入整数的绝对值均不超过 \(10000\) 。
- \(100\%\) 绝对值不超过 \(10^9\) 。
- 来源:\(luogup1852\)
分析
- 此题神级建模!
- 假设三颗棋子的初始位置是 \(x,y,z\ (x<y<z)\) ,三个棋子跳的情况可以分两种情况:
- 两端往中间跳:令 \(d1=y-x,d2=z-y\),此时分三种情况:
- \(d1<d2\) :只能 \(x\) 跳到 \(y\) 与 \(z\) 之间,实际上相当于 \(x,y\) 同时向右平移了 \(d1\)。
- \(d1>d2\):只能 \(z\) 跳到 \(x\) 与 \(y\) 之间,实际上相当于 \(y,z\) 同时向左平移了 \(d2\)。
- \(d1==d2\):\(x,z\) 均无法往内跳,此状态是终极状态。
- 中间往两边跳,此时没有限制,\(y\) 可以随便选一边跳,没有限制。
- 两端往中间跳:令 \(d1=y-x,d2=z-y\),此时分三种情况:
- 上面两种情况分析,当两端往里跳时,三个棋子间的距离是收敛的,而且跳一步的状态有唯一性,这跟树的性质一样,一个点的父节点只有一个,在往内跳的终极状态必然会出现 \(x,z\) 到 \(y\) 的距离相等,此时无法再往内跳,此种状态正好对应着树根。
- 所以,我们可以把每一个往内跳的状态建成一颗树,只是跟一般树不同的是节点状态是三个点的坐标的结构体。
- 能建出树来,问题就回到了我们熟悉的模型,如果两个状态的树根,即内敛的最终状态如果不同,显然无论怎么跳都不可能重合。
- 如果树根相同显然两种状态是可以重合的,跳的最小步数即为两个状态间的距离,到这一步可能好多同学就开始兴奋了,这不就是我们熟悉的 \(lca\) 吗?不过老姚提醒你,可不要太天真,让我们来看看数据范围:\(10^9\) ,这意味着什么呢?
- 假设有这么一个状态:\(1,2,10^9\) ,这个状态如果要收敛到最终状态,需要跳 \(10^9\) 次,如果要建树,也要建这么大,这显然是不可能,所以,我们不能建树。
- 不建树,我们也可以快速的收敛到终极状态,即 我们不需要一步步的跳,因为在这种情况下,每一次跳的距离是一样的,所以我们可以求出连续跳 \(k=(d2-1)/d1\) 次后 \(d1>=d2\) 。减一的目的是为了避免重合。
- 那我们解决了不建树也能快速的求出当前状态收敛到根状态时的步数,那不建树如何求出起始状态和目标状态的最近公共祖先呢?
- 我们可以二分其祖先,具体写法见代码
Code
#include <bits/stdc++.h>
const int Inf=0x3f3f3f3f;
struct Node{
int x,y,z;
void init(){
scanf("%d%d%d",&x,&y,&z);
if(x>y)std::swap(x,y);
if(x>z)std::swap(x,z);
if(y>z)std::swap(y,z);
}
}a,b,p,q;
int step,k;
bool Equal(Node u,Node v){
return u.x==v.x && u.y==v.y && u.z==v.z;
}
Node GetRoot(Node t,int s){//s表示往上跳多少步,找根时s==Inf
for(step=0;s;step+=k){
int d1=t.y-t.x,d2=t.z-t.y;
if(d1==d2) return t;//两边倒中间的距离相等
if(d1<d2){//x以y为轴往右跳
k=std::min((d2-1)/d1,s);//d2-1是避免d2是d1的倍数,跳重合了
t.x+=k*d1;t.y+=k*d1;s-=k;//跳的过程相当于x,y向右平移了k*d1
}
else{//z以y为轴向左跳
k=std::min((d1-1)/d2,s);//k表示跳的次数
t.y-=k*d2,t.z-=k*d2,s-=k;
}
}
return t;
}
void Solve(){
a.init();b.init();//读入并排序
p=GetRoot(a,Inf);int step1=step;//a状态往内跳直到距离相等即到根
q=GetRoot(b,Inf);int step2=step;//b状态往内跳直到距离相等即到根
if(!Equal(p,q)){//根的状态不一样,无论如何跳都不可能重合
printf("NO\n");return;
}
if(step1<step2){//a状态离根近就交换
std::swap(a,b);
std::swap(step1,step2);
}
a=GetRoot(a,step1-step2);//a状态往内跳,直到a,b离根距离一样,类似lca
int l=0,r=step2,mid;//二分找到a,b的最近公共祖先
while(l<r){
mid=(l+r)>>1;
if(Equal(GetRoot(a,mid),GetRoot(b,mid))) r=mid;
else l=mid+1;
}//跳出循环时,往上跳l步正好是最近公共祖先
printf("YES\n%d\n",(l<<1)+step1-step2);
}
int main(){
Solve();
return 0;
}
hzoi