三化二叉树trick
三选一化二叉
套路概述
这个套路是针对某一建模题的。
三选一其实可以扩展到N选一,模型具体如下。
发现某种状态可以扩展出\(N\)个状态,且有一个状态相较而言比较特殊(如其他状态都是扩张,仅有这个是收缩)的时候,可以考虑建立起一棵树,以当前状态为节点,特殊状态为父节点,其余状态为子节点。将问题转化到树上,而三选一就是转为二叉树的基本模型。如果没有特殊状态也可以考虑建立一张图进行处理。
建模-跳跳棋
跳跳棋是在一条数轴上进行的。棋子只能摆在整点上。每个点不能摆超过一个棋子。我们用跳跳棋来做一个简单的游戏:棋盘上有三颗棋子,分别在 \(a,b,c\)这三个位置。我们要通过最少的跳动把他们的位置移动成\(x,y,z\) (注意:棋子是没有区别的)。
跳动的规则很简单,任意选一颗棋子,对一颗中轴棋子跳动。跳动后两颗棋子距离不变。一次只允许跳过一颗棋子。
写一个程序,首先判断是否可以完成任务。如果可以,输出最少需要的跳动次数。
分析
考虑一个三元组\((a,b,c)(a<b<c)\)可以怎么跳:
- \((2a-b,a,c)\)
- \((a,c,2c-b)\)
- \((b,2b-a,c)(|a-b|<|b-c|)\)
- \((a,2b-c,b)(|a-b|>|b-c|)\)
这四种操作里面,容易发现三四操作最多只能存在一种。
容易发现,操作具有可逆性。而操作1,2都导致\((a',b',c')\)边界向外扩张,而只有3,4向内收缩。
突然发现,如果\(|a-b|=|b-c|\),此时只能进行1,2操作,也即进行扩张。而操作1,2可以分为\(b\)向左/右跳的结果。由于是LCA的题,不难想到需要建模。
我们可以由一组\((a,b,2b-a)\)开始不断向外扩展,不妨扩展看看,记\(c=2b-a\)
1次:\((2a-b,a,c),(a,c,2c-b)\)
2次:\((3a-2b,2a-b,c),(2a-b,c,2c-a),(2a-c,2a-b,2c-a),(a,2c-b,3c-2b)\)
好像,它形成了一个树形结构?也即中间棋子向左跳为左儿子,向右跳为右儿子,向内跳的情况为父亲,这样就由\((a,b,c)\)能够到达的所有情况变成了一颗满二叉树。
此时,由于操作的可逆性,容易发现初始状态和终态有解当且仅当他们所在树的树根相同。然后最少操作次数就是二者在树上的距离。
但是容易发现这棵树好像无穷大,根本不可能建立出来好吧。我们先来考虑如何快速求三元组\((a,b,c)\)的根,亦或者说,我们考虑如何快速求解三元组\((a,b,c)\)的第\(k\)级祖先。
数据范围\(10^9\),显然可以卡掉所有复杂度不正确的算法。
考虑这样一个过程,我们优化掉连续向某个方向跳的操作,这个可以通过公式计算:
设\(x_1=b-a,x_2=c-b\),则若\(x_1<x_2\),则可以不断向右跳\(\left\lfloor\frac{x_2-1}{x_1}\right\rfloor\)步。
然后\(x_1'=x_2-x_1\left\lfloor\frac{x_2-1}{x_1}\right\rfloor,x_2'=x_1\)
这是一个类似于计算欧几里得算法的过程,到最终肯定会有一个终态。于是这部分复杂度被优化到了\(O(\log n)\)。
再接着,由于计算树上路径需要LCA,而这棵树的性质决定了我们不可能使用常用LCA算法优化这个过程。但倍增的思想告诉我们,我们可以自底向上地倍增!具体的,先将两个节点调整到同一高度。由于这个算法的存在,可以在\(O(\log n)\)的时间里求出某个节点的\(k\)级祖先,此时就可以食用倍增优化了。总体复杂度\(O(\log^2 n)\)。
事实上,这个复杂度应该远远跑不满才对。
#include<iostream>
#include<cstdio>
using namespace std;
struct node{
int a,b,c;
void init(){
cin>>a>>b>>c;
if(a>b)swap(a,b);if(a>c)swap(a,c);if(b>c)swap(b,c);
}
bool operator==(node x){
return a==x.a&&b==x.b&&c==x.c;
}
}a,b,fa,fb;
int cnt1,cnt2,ans,l1,l2;
void bezero(node &x){
if(x.a>x.b)swap(x.a,x.b);
if(x.a>x.c)swap(x.a,x.c);
if(x.b>x.c)swap(x.b,x.c);
}
node get(node a,int k,int &cnt){//得到a的k级父亲
node ans=a;
cnt=0;
while(k){
bezero(ans);
int x1=ans.b-ans.a,x2=ans.c-ans.b,s;
if(x1<x2){
s=min(k,(x2-1)/x1);
ans.a+=s*x1;
ans.b+=s*x1;
k-=s;
}
else if(x1>x2){
s=min(k,(x1-1)/x2);
ans.b-=s*x2;
ans.c-=s*x2;
k-=s;
}
else {
return ans;
}
cnt+=s;
}
return ans;
}
int main(){
a.init();b.init();
fa=get(a,0x3f3f3f3f,cnt1);
fb=get(b,0x3f3f3f3f,cnt2);
if(!(fa==fb)){
cout<<"NO\n";
return 0;
}
cout<<"YES\n";
if(cnt1>cnt2)swap(a,b),swap(cnt1,cnt2);
ans+=cnt2-cnt1;
b=get(b,cnt2-cnt1,cnt2);
if(a==b){
cout<<ans;
return 0;
}
for(int i=29;i>=0;--i){
node x=get(a,1<<i,l1),y=get(b,1<<i,l2);
if(!(x==y)){
a=x,b=y;
ans+=1<<(i+1);
}
}
cout<<ans+2;
}
清华集训的题都是神题