洛谷 - P1852 跳跳棋

题目链接

考察知识点

  • 图论 - 树上问题 - 最近公共祖先
  • 算法 - 二分

思路分析

根据题意我们可以发现,跳跳棋移动的情况只有四种,分别是:

  1. 中间的点往两边跳
    1. 中间的点往左边跳
      01
    2. 中间的点往右边跳
      02
  2. 两边的点往中间跳
    1. 左边的点往中间跳
      03
    2. 右边的点往中间跳
      04

可以发现,状态 \((x,y,z)\) 经“中间的点往两边跳”后的状态 \((u,v,w)\) 等价于状态 \((u,v,w)\) 经“两边的点往中间跳”后的状态 \((x,y,z)\)

可以把状态 \((u,v,w)\) 和状态 \((x,y,z)\) 之间连一条无向边,这样本题第一问就转化为当前棋子的位置状态和目标位置状态是否存在最近公共祖先,第二问就转化为当前棋子的位置状态和目标位置状态的简单路径的长度。

再看一眼数据规模:对于100%的数据,绝对值不超过\(10^9\)

如果按照上面的过程进行求解,那么不是TLE就是MLE。因此考虑优化。

回顾一下倍增求LCA的过程:

  1. 预处理阶段:通过一次DFS遍历,记录每个节点的深度,并预处理一个重要的数组 fa[node][i]。这个数组的含义是:节点 node 向上跳 \(2^i\) 步所到达的祖先节点

  2. 查询阶段:对于任意两个节点 uv,通过 fa 数组进行“跳跃”操作,先将它们调整到同一深度,然后一起向上跳,直到找到它们的最近公共祖先。

可以发现,倍增求LCA的核心就是处理向上跳k步后的状态,因此,我们决定优化这一步骤。

\(u=y-x,v=z-y\),当左右距离u和v不相等时,可连续跳跃多次。

\(u \lt v\),左边棋子可向右跳 \(k=min(\lfloor {\frac{v-1}{u}} \rfloor,s)\) 次(s 为剩余步数上限),直接更新状态为 \((x+k \times u,y+k \times u,z)\)

\(u \gt v\),右边棋子可向左跳 \(k=min(\lfloor {\frac{u-1}{v}} \rfloor,s)\) 次(s 为剩余步数上限),直接更新状态为 \((x,y-k \times v,z-k \times v)\)

为什么这样可行?

我们先证明连续跳跃的可行性

当三个棋子的位置为 \((x, y, z)\)(满足 \(x \le y \le z\))时,左右两边的距离分别为:

  • 左边距离 \(u=y-x\)(左到中)
  • 右边距离 \(v=z-y\)(中到右)

情况 1:\(u \lt v\)(左边距离小于右边距离)

此时,左边的棋子 x 可以向右跳(属于 “两边向中间跳” 的规则),跳跃后新位置为:

  • 原中间棋子 y 成为新的左端点;
  • 原左棋子 x 跳至 \(2y - x\)(新的中间点);
  • 右端点 z 不变。

新状态排序后为 \((y,2y-x,z)\),此时新的距离为:

  • 新左边距离:\((2y-x)-y=y-x=u\)(与原左边距离相等);
  • 新右边距离:\(z-(2y-x)=(z-y)-(y-x)=v-u\)(右边距离减少了u)。

每次左边棋子向右跳时,左边距离 u 保持不变,而右边距离 v 会减少 u(即\(v_{新}=v_{旧}- u\))。 只要 \(v_{新} \gt u\),就可以继续重复这个跳跃过程(因为此时仍然满足 \(u \lt v_{新}\))。

情况 2:\(v \lt u\)(右边距离小于左边距离)

类似地,右边的棋子 z 可以向左跳,跳跃后新位置排序为 \((x,2y-z,y)\),此时:

  • 新右边距离:\(y-(2y-z)=z-y=v\)(与原右边距离相等);
  • 新左边距离:\((2y-z)-x=(y-x)-(z-y)=u-v\)(左边距离减少了 v)。

同样,只要 \(u_{新} \gt v\),就可以连续跳跃。

证毕

接着再证明跳跃次数 k 的计算的合理性

连续跳跃的次数 k 受两个限制:

  1. 不能超过剩余允许的最大步数 s;
  2. 不能跳至 “无法继续同方向跳跃” 的状态(即跳 k 次后,需保证下一次跳跃不再满足同方向条件)。

\(u \lt v\) 时:

每次跳跃后右边距离减少 u,经过 k 次跳跃后,右边距离变为 \(v - k \times u\)。 为了保证 “跳 k 次后无法再继续同方向跳跃”,需满足:\(v - k \times u \lt u\) (即跳 k 次后右边距离小于左边距离,下一次跳跃方向改变)。

整理得:\(k \times u \gt v - u \implies k \gt \frac{v-u}{u} \implies k \ge \lfloor \frac{v - 1}{u} \rfloor\)

这里用 \(v - 1\) 是为了取最大整数 k(例如 \(v=5, u=2\) 时,\((5-1)/2=2\),即最多跳 2 次,跳后右边距离为 \(5 - 2 \times 2 = 1 \lt 2\),符合条件)。

\(v \lt u\) 时:

类似地,每次跳跃后左边距离减少 v,经过 k 次跳跃后,左边距离变为 \(u - k \times v\)。 需满足:\(u - k \times v \lt v \implies k > \frac{u - v}{v} \implies k \ge \lfloor \frac{u - 1}{v} \rfloor\)

证毕

之后二分从a到a和b的最近公共祖先的距离ans,即可求出答案,为\(2 \times ans+(dep1-dep2)\)

时间复杂度

\(O(log_{2}MaxValue+log_{2}log_{2}MaxValue)\)

C++代码

// Problem: P1852 跳跳棋
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1852
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

// 包含标准输入输出库,用于scanf和printf等函数
#include <cstdio>
// 包含算法库,用于swap等标准算法函数
#include <algorithm>
// 使用标准命名空间,避免在调用标准库函数时加std::前缀
using namespace std;

// 定义结构体Node,用于表示跳跳棋三个棋子的位置状态
struct Node{
    int x, y, z;  // 三个棋子的坐标,后续会保证x ≤ y ≤ z的顺序
    
    // 成员函数init:初始化Node对象,读取三个坐标并排序
    void init(){
        // 从标准输入读取三个整数,分别赋值给x、y、z
        scanf("%d%d%d", &x, &y, &z);
        // 以下三次swap确保x是最小值,z是最大值,即x ≤ y ≤ z
        if(x > y) swap(x, y);  // 若x>y,交换x和y,保证x ≤ y
        if(x > z) swap(x, z);  // 若x>z,交换x和z,保证x ≤ z(此时x已是三者中最小)
        if(y > z) swap(y, z);  // 若y>z,交换y和z,保证y ≤ z
    };
};

// 函数equal:判断两个Node对象是否表示相同的状态(三个坐标都相等)
bool equal(Node a, Node b){
    // 当且仅当a和b的x、y、z都分别相等时,返回true
    return a.x == b.x && a.y == b.y && a.z == b.z;
}

// 函数transform:对给定状态t进行最多s步转换,返回转换后的状态,并通过引用l返回实际转换的步数
// 转换规则基于跳跳棋的跳跃逻辑:中间的棋子可以向两边跳,或两边的棋子可以向中间跳(此处模拟多步跳跃以提高效率)
Node transform(Node t, int s, int &l){
    l = 0;  // 初始化实际转换步数为0
    // 当剩余可转换步数s>0时,继续转换
    while(s > 0){
        // 计算相邻两个棋子的距离:u是左到中,v是中到右
        int u = t.y - t.x;
        int v = t.z - t.y;
        // 若两边距离相等(u==v),则无法继续转换(对称状态,跳不动),退出循环
        if(u == v) break;
        int k;  // 记录当前可连续跳跃的步数
        // 若左边距离u小于右边距离v,说明左边的棋子可以向右跳多次
        if(u < v){
            // 计算最多可跳的步数k:(v-1)/u是因为跳k步后需保证新的中间距离不超过右边(避免超过对称点),同时不超过剩余步数s
            k = min((v - 1) / u, s);
            // 跳跃k步后,左棋子和中棋子的位置右移k*u(模拟左棋向右跳k次)
            t.x += k * u;
            t.y += k * u;
        } else {
            // 若右边距离v小于左边距离u,说明右边的棋子可以向左跳多次
            // 计算最多可跳的步数k:(u-1)/v同理,不超过剩余步数s
            k = min((u - 1) / v, s);
            // 跳跃k步后,中棋子和右棋子的位置左移k*v(模拟右棋向左跳k次)
            t.y -= k * v;
            t.z -= k * v;
        }
        l += k;  // 累加本次跳跃的步数到实际总步数
        s -= k;  // 剩余可跳步数减少k
    }
    return t;  // 返回转换后的状态
}

// 主函数:程序入口
int main(){
    Node a, b;  // 定义两个Node对象,分别表示初始状态和目标状态
    a.init(), b.init();  // 初始化a和b,读取输入并排序
    
    int dep1, dep2;  // 记录a和b转换到"最深"状态时的步数(类似深度)
    // 将a转换到尽可能深的状态(s=1e9表示足够大的步数),结果存在p,步数存在dep1
    Node p = transform(a, 1e9, dep1);
    // 同理处理b,结果存在q,步数存在dep2
    Node q = transform(b, 1e9, dep2);
    
    // 若最深状态p和q不相等,说明a和b无法到达同一状态,输出NO
    if(!equal(p, q)) puts("NO");
    else{
        // 若dep1 < dep2,交换a和b、dep1和dep2,保证dep1 ≥ dep2(方便后续对齐深度)
        if(dep1 < dep2) swap(dep1, dep2), swap(a, b);
        int tmp;  // 临时变量,用于接收transform的实际步数(此处无用)
        // 将a转换dep1-dep2步,使a和b处于同一深度(此时两者到最深状态的步数相同)
        a = transform(a, dep1 - dep2, tmp);
        
        // 二分查找最大的mid,使得a和b各转换mid步后状态相同(即共同祖先)
        int l = 0, r = dep2, ans = 0;  // l和r是二分边界,ans记录找到的最大mid
        while(l <= r){
            int mid = (l + r) / 2;  // 计算中间值
            // 检查a转换mid步和b转换mid步后的状态是否相同
            if(equal(transform(a, mid, tmp), transform(b, mid, tmp))){
                // 若相同,尝试找更小的mid(因为要最大的符合条件的mid)
                r = mid - 1;
                ans = mid;  // 更新ans为当前mid
            } else {
                // 若不同,需要增大mid
                l = mid + 1;
            }
        }
        // 总步数为:2*ans(a和b到共同祖先的步数之和) + (dep1-dep2)(之前a多走的步数)
        printf("YES\n%d\n", 2 * ans + (dep1 - dep2));
    }
    return 0;  // 程序正常结束
}
posted @ 2025-08-25 13:28  九三青梧  阅读(18)  评论(0)    收藏  举报