BZOJ-2144 跳跳棋(LCA+二分答案)
题目描述
跳跳棋是在一条数轴上进行的。棋子只能摆在整点上。每个点不能摆超过一个棋子。我们用跳跳棋来做一个简单的游戏:棋盘上有 \(3\) 颗棋子,分别在 \(a,b,c\) 这三个位置。我们要通过最少的跳动把他们的位置移动成 \(x,y,z\)(棋子是没有区别的)。跳动的规则很简单,任意选一颗棋子,以中间的棋子为对称轴跳动。跳动后两颗棋子距离不变。一次只允许跳过 \(1\) 颗棋子。
分析
用三元组 \((a,b,c)\) 表示初始三个棋子的位置(其中 \(a<b<c\))。
如果 \(b-a<c-b\),最左边棋子向右跳,可以转移到 \((b,2b-a,c)\);如果 \(b-a>c-b\),最右边棋子向左跳,可以转移到 \((a,2b-c,b)\)。除此之外,如果 \(b-a<c-b\),中间棋子向右边跳,可以转移到 \((2a-b,a,c)\);如果 \(b-a>c-b\),中间棋子向右边跳,可以转移到 \((a,c,2c-b)\)。
可以发现,中间棋子向两边跳和两边棋子向中间跳是互逆的,不妨只考虑两边棋子向中间跳,那么每个状态能转移到的状态是唯一的,比如 \((2a-b,a,c)\) 和 \((a,c,2c-b)\) 都只能转移到 \((a,b,c)\)。
假设 \((a,b,c)\) 是 \((2a-b,a,c)\) 和 \((a,c,2c-b)\) 的父节点,那么从父节点走向根节点就是从中间向两边跳,从根节点走向父节点就是从两边向中间跳。每个状态看成树中的一个节点,所有状态就会构成森林。
如果两个状态不在同一棵树中,无解;如果在同一棵树中,最少跳跃次数就是树上两点间的距离。
考虑如何找根,不断向上跳,直到不能再向上跳,即 \(b-a=c-b\)。
考虑节点 \((a,b,c)\),令 \(x=b-a,y=c-b\)。如果一开始 \(x<y\) 时,每次 \(a\) 会跳到 \(b,c\) 之间,直到 \(x>y\)。则每次跳跃会使最左边的棋子的坐标增加 \(b-a\),即 \(y\) 的值减少了 \(x\),假设跳了 \(k\) 次,则 \(k=(y-1)/x\),此时转移到了状态 \((a+kx,b+kx,c)\),模拟欧几里得算法的过程,直到 \(x=y\);如果一开始 \(x>y\),则每次 \(c\) 会跳到 \(a,b\) 之间,直到 \(x<y\)。则每次跳跃会使最右边的棋子的坐标减少 \(c-b\),即 \(x\) 的值减少了 \(y\),假设跳了 \(k\) 次,则 \(k=(x-1)/y\),此时转移到了 \((a,b-ky,c-ky)\),模拟欧几里得算法的过程,直到 \(x=y\)。
寻找两点的 $\text{LCA} $ 也是不断向上跳的过程,找到根之后处理出 \((a,b,c)\) 和 \((x,y,z)\) 的深度,先把两个点调整至同一深度,然后二分跳的步数,能跳到同一个点就减少步数,反之增大步数。
代码
#include<bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
int a[5],b[5];
struct node
{
int a,b,c;
}A,B,rootA,rootB;
bool check(node X,node Y)
{
return X.a==Y.a&&X.b==Y.b&&X.c==Y.c;
}
int step,k;//step为跳到根节点需要多少步
node get(node T,int S)//状态T向上跳S步
{
for(step=0;S!=0;step=step+k)
{
int x=T.b-T.a,y=T.c-T.b;
if(x==y)
return T;
if(x<y)
{
k=min((y-1)/x,S);
T.a=T.a+k*x;
T.b=T.b+k*x;
S=S-k;
}
if(x>y)
{
k=min((x-1)/y,S);
T.c=T.c-k*y;
T.b=T.b-k*y;
S=S-k;
}
}
return T;
}
int main()
{
cin>>a[1]>>a[2]>>a[3];
sort(a+1,a+4);
A.a=a[1],A.b=a[2],A.c=a[3];
cin>>b[1]>>b[2]>>b[3];
sort(b+1,+b+4);
B.a=b[1],B.b=b[2],B.c=b[3];
rootA=get(A,INF);
int cnt1=step;
rootB=get(B,INF);
int cnt2=step;
if(check(rootA,rootB)==false)
{
puts("NO");
return 0;
}
if(cnt1<cnt2)
{
swap(A,B);
swap(cnt1,cnt2);
}//cnt1为离根远的
A=get(A,cnt1-cnt2);
int l=0,r=cnt2;
while(l<r)//>=x的数中最小的一个
{
int mid=(l+r)/2;
if(check(get(A,mid),get(B,mid)))
r=mid;
else
l=mid+1;
}
puts("YES");
cout<<2*l+cnt1-cnt2<<endl;
return 0;
}
posted on 2020-11-28 21:08 DestinHistoire 阅读(196) 评论(0) 收藏 举报