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修复工程的主要内容,如果未能解决你的问题,请参考以下文章