BZOJ 2144 跳跳棋(神仙建模题,倍增 LCA,二分)BZOJ修复工程

Posted 繁凡さん

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了BZOJ 2144 跳跳棋(神仙建模题,倍增 LCA,二分)BZOJ修复工程相关的知识,希望对你有一定的参考价值。

整理的算法模板合集: ACM模板

点我看算法全家桶系列!!!

实际上是一个全新的精炼模板整合计划


题目链接

https://hydro.ac/d/bzoj/p/2144

hydro 的 BZOJ 修复工程 !(我也去领了一点题慢慢修着玩,这题就是我修的嘿嘿嘿)

题目描述

跳跳棋是在一条数轴上进行的。棋子只能摆在整点上。每个点不能摆超过一个棋子。

我们用跳跳棋来做一个简单的游戏:棋盘上有 3 3 3 颗棋子,分别在 a , b , c a,b,c a,b,c 这三个位置。我们要通过最少的跳动把他们的位置移动成 x , y , z x,y,z x,y,z。(棋子是没有区别的)跳动的规则很简单,任意选一颗棋子,对一颗中轴棋子跳动。跳动后两颗棋子距离不变。一次只允许跳过1颗棋子。 写一个程序,首先判断是否可以完成任务。如果可以,输出最少需要的跳动次数。

输入格式

第一行包含三个整数,表示当前棋子的位置 a , b , c a,b,c a,b,c。(互不相同)

第二行包含三个整数,表示目标位置 x , y , z x,y,z x,y,z。(互不相同)

输出格式

如果无解,输出一行 NO。如果可以到达,第一行输出 YES,第二行输出最少步数。

输入样例

1 2 3
0 3 5

输出样例

YES
2

数据规模与约定

对于 100 % 100\\% 100% 的数据输入数据中所有元素绝对值不超过 1 0 9 10^9 109

Solution

首先我们分析题意,由于每次只能跳过一个棋子,并且必须找到中轴对称跳过去,我们对输入的三个棋子按照从左到右的顺序排序后,三个棋子依次为 x , y , z x,y,z x,y,z,跳棋操作实际上只有三种情况:

  • y y y 向左跳过 x x x
  • y y y 向右跳过 z z z
  • x , z x,z x,z 中距 y y y 更近的棋子向中间跳过 y y y

画图发现每个状态实际上是可以相互转化的,也就是说你跳过去再跳回来,所以的位置都是可以相互通过跳棋操作相互到达的,也就是说我们就可以把每个跳棋位置的状态作为一个节点,全部是可以连通的,形成了一个无向连通图!

我们就可以考虑将数轴上抽象的问题转化为图论的问题,要知道解决图论问题我们是有很多可行的算法的。

将原模型转化为图模型之后,我们发现,如果从初始状态开始,两边的棋子疯狂往中间跳,最后会挤到一个位置不能再往中间跳,并且由于每次往中间跳的时候只有距离更小的棋子才能跳,所以实际上跳的过程中所有的状态都是固定的,而我们从这个挤不动的位置出发往外跳,又可以跳到全部的状态,也即该点和所有状态都是连通的,也就相当于是一个树根!从树根出发,我们发现每次我们都可以往两边跳,是同级的,每次往两边跳实际上就可以看做是树根的子结点。每次两个方向,向左跳的就可以看做是根节点的左儿子,向右跳的就可以看做是根节点的右儿子,并且实际上所有跳的操作能到达的位置状态是固定的,也就意味着我们最终发现这个无向连通图实际上是一个二叉树。

将平面数轴模型转化为二叉树模型之后,我们发现题目中所求的是,从一个位置状态,到另一个位置状态,最少需要多少次操作,由于所有状态均在一个二叉树上,每一个位置状态实际上就是一个结点,那么原问题就变成了求两个结点之间的最短距离

那么显然问题就变得非常简单了!

我们看起来只需要倍增 LCA 计算树上两点间最短距离即可 ~

但是有一些小细节,如果初始位置距离终点非常非常远,我们如果一步一步跳的话,可能会跳很多很多次,比如

1 2 3
1 INF - 1 INF 

此时我们每次只能跳 1 1 1,而终点离我们有 ∞ \\infin 那么远,显然要T,考虑优化。

我们发现每次只能跳 1 1 1,但是我们会一直重复这个动作知道达到终点,我们显然可以直接用总距离除以每次跳跃的距离 O ( 1 ) O(1) O(1) 算出需要重复多少次,然后 O ( 1 ) O(1) O(1) 跳达。

这样我们跳的过程类似辗转相除,复杂度实际上只有 log ⁡ K \\log K logK

最后求 LCA 即可。类似倍增求 LCA 的过程,我们可以先将初始状态与终点状态中低的那个往上跳到同样的高度,此时我们一起将二者往上继续跳,显然当两点越过 LCA 之后,他们都会到达同一个结点,满足单调性,所以我们就可以二分一下二者在同一高度跳到 LCA 时需要的次数即可。

所以大致的流程就是,我们先 log ⁡ N \\log N logN 将初始状态以及终点状态一起往中间跳,找树根,与此同时记录结点深度,然后将二者调成到同一深度,再二分到 LCA 的距离即可。

Code

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int maxn = 1e5 + 6, INF = 0x3f3f3f3f;

int n, m, s, t;
ll ans;
int x, y, z, X, Y, Z;
struct node
{
	int cnt, x, y, z;
	bool operator == (const node &t) const {
		if(x == t.x && y == t.y && z == t.z)
			return true;
		return false;
	}
}A, B;
int a[10];

void run(node &A, int x, int y, int z)
{
	int cnt = 0;
	while(true) {
		int d1 = y - x, d2 = z - y;
		if(d1 == d2) break;
		if(d1 > d2) {
			int num = (d1 - 1) / d2;
			cnt += num;
			// 直接交换 y,z,此时跳跃操作就相当于 y, z 同时向左平移 d2 * num
			z -= d2 * num;
			y -= d2 * num;
		}
		else {
			int num = (d2 - 1) / d1;
			cnt += num;
			x += d1 * num;
			y += d1 * num;
		}
	}
	A = {cnt, x, y, z};
}

void swap(node &a, node &b)
{
	swap(x, X), swap(y, Y), swap(z, Z);
	swap(a.x, b.x), swap(a.y, b.y), swap(a.z, b.z), swap(a.cnt, b.cnt);
}

inline bool check(int cnt, int x, int y, int z, int X, int Y, int Z)
{
	int tot = cnt;
	while(tot) {
		int d1 = y - x, d2 = z - y;
		if(d1 == d2) break;
		if(d1 > d2) {
			int num = min(tot, (d1 - 1) / d2);
			tot -= num;
			z -= d2 * num;
			y -= d2 * num;
		}
		else {
			int num = min(tot, (d2 - 1) / d1);
			tot -= num;
			x += d1 * num;
			y += d1 * num;
		}
	}

	tot = cnt;
	while(tot) {
		int d1 = Y - X, d2 = Z - Y;
		if(d1 == d2) break;
		if(d1 > d2) {
			int num = min(tot, (d1 - 1) / d2);
			tot -= num;
			Z -= d2 * num;
			Y -= d2 * num;
		}
		else {
			int num = min(tot, (d2 - 1) / d1);
			tot -= num;
			X += d1 * num;
			Y += d1 * num;
		}
	}
	
	if(x == X && y == Y && z == Z) return true;
	return false;
}

int main()
{
	scanf("%d%d%d", &a[1], &a[2], &a[3]);
	sort(a + 1, a + 1 + 3);
	x = a[1], y = a[2], z = a[3];
	scanf("%d%d%d", &a[1], &a[2], &a[3]);
	sort(a + 1, a + 1 + 3);
	X = a[1], Y = a[2], Z = a[3];
	
	//计算树根,顺便统计结点深度
	run(A, x, y, z);
	run(B, X, Y, Z);
	
	//判断是否存在树根
	//if(!(A == B)) return 0 * puts("NO");
	if(A.x != B.x || A.y != B.y || A.z != B.z) return 0 * puts("NO");

	puts("YES");
	if(A.cnt < B.cnt) swap(A, B);
	
	int tot;
	tot = A.cnt - B.cnt;
	ans += tot;
	
	//低的结点往上跳,使得二者处于同一深度
	while(tot) {
		int d1 = y - x, d2 = z - y;
		if(d1 == d2) break;
		if(d1 > d2) {
			int num = min(tot, (d1 - 1) / d2);
			tot -= num;
			z -= d2 * num;
			y -= d2 * num;
		}
		else {
			int num = min(tot, (d2 - 1) / d1);
			tot -= num;
			x += d1 * num;
			y += d1 * num;
		}
	}
	
	ll l = 0, r = B.cnt;
	int ans2 = 0;
	//二分 LCA 距离
	while(l <= r) {
		int mid = (l + r + 1) >> 1;
		if(check(mid, x, y, z, X, Y, Z)) 
			ans2 = mid, r = mid - 1;
		else l = mid + 1;
	}
	
	cout << ans + 2 * ans2 << endl;
	return 0;
}

以上是关于BZOJ 2144 跳跳棋(神仙建模题,倍增 LCA,二分)BZOJ修复工程的主要内容,如果未能解决你的问题,请参考以下文章

bzoj 2144: 跳跳棋——倍增/二分

bzoj2144: 跳跳棋(二分/倍增)

[BZOJ2144]跳跳棋

bzoj2144 跳跳棋 二分

BZOJ 2144 跳跳棋

跳跳棋bzoj2144国家集训队