hdu 3830 Checkers
LCA + 二分(很好的题目,思维难度和编程技巧兼具的一题,但是写起来又不会太麻烦,好题!)
思路参考了网上
题意:略,就是点间可以跳跃,但是不能越过两个点,每次跳跃的距离就是两点的距离 * 2
1.一般很容易想到,可以把一个状态看成一个点,那么状态间的转移就可以看做点间的连边,而且应该是无向边,应该两个状态是可以转化的。但是想到这里还不够,如果能想到这个图其实是个二叉树那么就完美了,而且应该说是一个无限深的二叉树,而且每个节点都有两个儿子,不会只有1个
为什么会是一个二叉树,是因为对于每个状态,它都一定只会有3种或2种转移可能。
对于一个状态,我们将3个点排好序,x < y < z
如果y - x = z - y , 那么只有2种跳跃,就是y向两边跳,能产生两个新的状态
如果 y - x < z - y , 那么有3种跳跃,就是y向两边跳,x向y和z之间跳
如果 y - x > z - y , 那么有3种跳跃,就是y向两边跳,z向y和x之间他跳
可以发现,上面的描述,已经包含了所有可能的状态。想想什么图,每个点的度只可能是2或者3,不可能是0或1,也不可能大于3,那就是二叉树!
二叉树除开根,所有点的度都是3,根的度是2。
对于y向两边跳,任何状态都可以做,那么就相当于二叉树每个节点都有2个儿子。而后两种状态,还能向中间跳的选择,这就好比二叉树中每个点都可以朝双亲移动(除开根!)
说到这里很直白了,得到一些结论
1.对于第1类状态,即 y - x = z - y,相当于二叉树的根
2.对于第2,3类状态,相当于二叉树的非根节点
3.对于y向两边跳跃并产生的新状态,就看做二叉树的节点向其两个儿子移动
这下子,问题状态为,得到两个状态,对应二叉树上的两个点,求两个点的距离,那就是LCA
1.首先,我们很容易想到,整个图,可能是不连通的,也就是不止一棵二叉树,如果起始和终点状态不在一个树上,也就是树根状态不同的话,那么它们是不可互达的,就输出NO
2.如果两点在一棵树上,那么它们一定可以使它们互达,输出YES,剩下就是怎么找到LCA
1.怎么判断两点在不在一棵树上,方法很简单,就是找到两个状态的树根,然后看看它们是不是相同的
一个显而易见的方法是模拟,就是从当前状态一步一步沿着双亲走回到根,直到状态满足 y - x = z - y才停止,但是可知,这样模拟的时间为O(n),已经超时了
好像1 2 10^8 , 这样的数据,回到树根,已经超时了,所以我们不能一步一步模拟,要用数学的办法
用几个例子说明问题
1 3 9 ----》 3 5 9 -----》 5 7 9 ,回到树根
1 3 10 -----》 3 5 ------》5 7 10 ----------》 7 9 10
7 9 10 ---------》7 8 9,回到树根
case 1 :1 3 9 ----》 3 5 9 , 实际上是1跳到了5的位置,走了1步,但是我们完全可以看做是1和3同时跳到了3和5,一样是用了1步,
所以从1 3 9变到5 7 9,可以看做1跳到5,3跳到7,每次跳2格,走了2步
case 2 :1 3 9 -----》 7 9 10 , 可以看做是1跳到7,3跳到9,每次跳2格,条了3次,然后再从7 9 10 ------》 7 8 9
这启示我们可以把跳跃过程看成一个一个部分,每个部分,可以用数学直接计算出来走了几步,并且算出后来的状态
再看case 1: len = 3 - 1 = 2 __len = 9 - 3 = 6 6/3 - 1 = 2 ,所以从1 3 9 到 5 7 9 用了2步
再看case 2: len = 3 - 1 =2 __len = 10 - 3 - 7 7/3 = 3 , 所以从1 3 10 到 7 9 10,用了3步
上面的公式,其实做了判断,就是有没有整除,如果整除了商要减1,没有整除商不用减1
所以可以用技巧避免掉判断 c(步数) = (__len - 1) / len ,就避开了判断,这个公式很容易想嘛
(上面的部分,属于编程技巧,避开了讨论,其本身并不影响题目的求解,对于case 2 , 7 9 10 到 7 8 9也是使用相同的办法)
这样我们又解决了一个问题,就是怎么快速算出一个状态的根,并且知道用了多少步,其实就是这状态在树中的深度(根的深度为0)
这种方法,类似于 “辗转相除(减)”
接下来是怎么找到LCA
首先我们刚才在寻找起始和终点状态的时候已经保存了这两个状态在树中的深度,首先我们做一个处理,就是把这两个点中深度较深的点往上移动,直到两点的深度一样,这样做,是为了后面的二分做准备。但是记得,这个调整的步数,是要算入答案了。
接下来两点在同一个深度了,那么二分寻找LCA所在的深度!(也不是直接二分寻找LCA的状态,事实上我们也不需要知道它的状态)
如果当前两点的深度为dep,LCA的深度为d,那么两点间的距离就是 2 * (dep – d),别忘了之前的调整,所以答案应该是 2 * (dep – d) + 调整的步数
怎么二分?
两点深度为dep,根深度为0,所以LCA的深度一定在[0 , dep](双闭区间)以内,然后我们二分答案
二分出一个结果d,那么深度差值 delta = dep – d , 然后看从起始状态往上走delta步,和从终止状态往上走delta步,然后产生的两个新状态是否一样
如果不一样,说明还没走到LCA,也就是说在LCA下面,所以delta应该更大一些,而d已经更小一些(LCA在更上面,深度更小)
所以二分去到更上面的位置
如果两个新状态相同,那么说明这个新状态可能就是LCA或者是LCA的祖先(因为从LCA开始再往上走,它们都是共用那些祖先的,只不过LCA是最近的而已),所以delta应该小一些,而d应该大一些(LCA可能在更下面,深度更大),所以二分在更下面的位置
这样二分的过程就解决了,但是还要实现代码实现的问题
我们计算出了delta,从当前状态向上走delta步,并且得到新状态,这个要怎么做?同样地我们可以模拟,一步一步往上走,但是结果和之前讲的一样,是会超时的,因为迭代次数太多了,这个问题完全可以由数学方法快速解决,方法就是上面说的“辗转相除(减)”一样的,不过是个逆过程,这里不详说了,看代码的updata()函数吧
inline void SORT(State &a) :对一个状态a的三个点排序保证 x < y < z
State Root(State &a) :给定一个状态a,算出它的根状态并返回,另外计算出a的深度,更新在a里面
void updata(State &a ,ll delta) : 给定一个状态a和向上走的步数delta,求出a向上走了delta步后产生的新状态,返回
#include <iostream> #include <cstdio> #include <cstring> #include <vector> #include <algorithm> using namespace std; #define ll __int64 struct State { ll x,y,z; ll dep; }; State S,T; inline bool cmp_state(State a , State b) { if(a.x == b.x && a.y == b.y && a.z == b.z) return true; return false; } inline ll Abs(ll x) { return x > 0LL ? x : -x; } inline void SORT(State &a) { if(a.y < a.x) swap(a.x , a.y); if(a.z < a.x) swap(a.x , a.z); if(a.y > a.z) swap(a.y , a.z); } State Root(State &a) { State tmp = a; tmp.dep = 0; ll dep = 0; while(Abs(tmp.x - tmp.y) != Abs(tmp.y - tmp.z)) { ll len = Abs(tmp.x - tmp.y); ll __len = Abs(tmp.y- tmp.z); if(__len > len) { ll c = (__len - 1)/ len; //巧妙,避开判断 dep += c; tmp.y += c * len; tmp.x += c * len; } else { ll c = (len - 1) / __len; dep += c; tmp.y -= c * __len; tmp.z -= c * __len; } // printf("%d %d %d\n",tmp.x , tmp.y , tmp.z); } a.dep = dep; return tmp; } void updata(State &a ,ll delta) { ll count = 0; while(count < delta) { ll len = Abs(a.x - a.y); ll __len = Abs(a.y - a.z); ll k = Abs(count - delta); //还差多少步 if(len < __len) { ll c = (__len - 1) / len; //将要移动多少步 ll Min = min(k , c); a.x += Min * len; a.y += Min * len; count += Min; if(Min == k) break; } else { ll c = (len - 1) / __len; ll Min = min(k , c); a.y -= Min * __len; a.z -= Min * __len; count += Min; if(Min == k) break; } } a.dep -= delta; } ll solve() { State tS,tT; ll low = 0 , high = S.dep; while(low <= high) { ll mid = (low + high) >> 1; ll delta = S.dep - mid; tS = S; tT = T; updata(tS , delta); //SORT(tS); updata(tT , delta); //SORT(tT); if(!cmp_state(tS , tT)) high = mid - 1; else low = mid + 1; } return 2 * (S.dep - high); } int main() { //while(cin >> S.x >> S.y >> S.z >> T.x >> T.y >> T.z) while( scanf("%I64d%I64d%I64d",&S.x,&S.y,&S.z) != EOF) { scanf("%I64d%I64d%I64d",&T.x,&T.y,&T.z); S.dep = T.dep = 0; SORT(S); SORT(T); State RS = Root(S); State RT = Root(T); // printf("%d %d %d %d\n",RS.x , RS.y , RS.z , RS.dep); // printf("%d %d %d %d\n",RT.x , RT.y , RT.z , RT.dep); if(!cmp_state(RS,RT)) { //cout << "N0" << endl; printf("NO\n"); continue; } ll tmpr = Abs(S.dep - T.dep); //调整的步数记得记录 if(S.dep > T.dep) updata(S , S.dep - T.dep); else updata(T , T.dep - S.dep); // printf("%d %d %d %d\n",S.x , S.y , S.z , S.dep); // printf("%d %d %d %d\n",T.x , T.y , T.z , T.dep); ll res = solve(); //cout << "YES" << endl; //cout << tmpr + res << endl; printf("YES\n"); printf("%I64d\n",res + tmpr); } return 0; }