[POI2010] ZAB-Frog

题目描述

在一条特别长且笔直的 Byteotian 小溪的河床上,有$n$块岩石露出水面。它们到小溪源头的距离分别为$p_1 < p_2 < \cdots < p_n$。一只坐在其中一块岩石上的小青蛙即将开始它的跳跃训练。每次这只青蛙都会跳到距离它所坐岩石第(k)近的岩石上。具体来说,如果青蛙坐在位置为$p_i$的岩石上,那么它会跳到满足以下条件的$p_j$上:

$$ |\{ p_a : |p _ a - p _ i| < |p_j - p_i| \}| \leq k \text{ 且 } |\{ p_a : |p _ a - p _ i| \leq |p_j - p_i| \}| > k $$

如果$p_j$不唯一,那么青蛙会在这些岩石中选择距离小溪源头最近的那块岩石。根据青蛙起跳时所坐的岩石,在跳了$m$次之后,它会坐在哪块岩石上呢?

输入格式

标准输入的第一行包含三个整数$n$、$k$和$m$$(1 \leq k < n \leq 1000000,1 \leq m \leq 10^{18})$,它们由单个空格分隔,分别表示:岩石的数量、参数$k$以及计划跳跃的次数。第二行包含$n$个整数$p_j(1 \leq p_1 < p_2 < \cdots < p_n \leq 10^{18})$,这些整数由单个空格分隔,表示小溪河床上连续岩石的位置。

输出格式

你的程序应该在标准输出上打印一行内容,其中包含$n$个处于区间$[1, n]$内的整数$r_1, r_2, \cdots, r_n$,这些整数之间用单个空格分隔。数字$r_i$表示青蛙从编号为$i$的岩石(按照输入顺序)出发,经过$m$)次跳跃后最终所到达的岩石的编号。

样例 #1

样例输入 #1

5 2 4
1 2 4 7 10

样例输出 #1

1 1 3 1 1

题目分析

题意简述

题目要求计算一只青蛙从每块石头出发,经过 $m$ 次跳跃后的位置。青蛙每次跳跃到第 $k$ 近的石头上,如果有多块石头满足条件,则选择离小溪源头最近的那块石头。

模型抽象

基于距离的移动问题,青蛙的跳跃可以视为在有序数列中寻找第$k$ 近的元素。

初步思考

暴力解法:直接模拟青蛙的每次跳跃,直到完成 $m$ 次跳跃。每次跳跃,都向两侧逐个遍历,确定第 $k$ 近的位置,单次跳跃复杂度为 $O(k)$ ,整体复杂度为 $O(mk)$ 。但是从数据范围中可知m的范围很大,为 $10^{18}$。暴力去模拟跳跃必然是会超时。

优化方向:每次一步步跳跃到第 $k$ 近的位置的过程太慢,需要一种快速计算移动 $m$ 次后的位置的方法。我们曾经在求【最近公共祖先】问题中,使用倍增的技巧处理过。且 $m$ 可用 $64$ 位以内的二进制进行表示。可以考虑使用倍增加速跳跃过程。

优化思考

设计$nxt[位置][跳跃次数]$ ,nxt[i][j] 为从i点跳跃$2^j$次到达的位置。更新过程为 $nxt[i][j]=nxt[nxt[i][j-1]][j-1]$ 。更新预处理的复杂度为 $O(n\log m)$ 。

在已知nxt[][] 的基础上,要求解跳跃m次后的位置可对m进行二进制拆分。单次查询复杂度为 $O(\log m)$

int query(int pos,int m){
    int lg=log2(m);
    for(int i=0;i<=lg;i++){//对m进行二进制拆分
        if((m>>i)&1){//第i位为1
            pos=nxt[pos][i];//从pos跳跃2^i次的位置
        }
    }
    return pos;
}

逻辑通。解决剩下的问题:如何求出从每个位置跳一次到达的位置?

根据题意,到达的位置为离$i$第$k$近的位置。题目中对于位置$p_i$ 有特殊性质:升序。那么对于离第$i$个元素的前$k$近的元素必然分布在$i$的两侧,且形成一个包含$i$长度为$k+1$的连续区间,设该区间左右端点为$L$和$R$。第$k$近的元素必然在端点处。那么如果 $p_R-p_i>p_i-p_L$ 则$p[R]$ 为第$k$ 近的元素,否则是 $p[L]$ 。

对于下一个元素 $p_{i+1}$ 的前$k$小元素形成的区间$[L_{i+1},R_{i+1}]$ 一定不会在上一个区间$[L_i,R_i]$的左侧,也就是 $L_i\le L_{i+1},R_i\le R_{i+1}$ 。

p[]是升序的,$p[i]<p[i+1]$ ,比较$p[i]-p[L]$和$p[i+1]-p[L]$ 与左端点的距离在变大,比较$p[R]-p[i]$和$p[R]-p[i+1]$ 与有端点的距离在变小,所以如果要更新连续区间,必然是要舍弃左端点的元素的,所以整体区间是在上一个区间的基础上右移,两个端点是同向移动,不会回头的。可推出该区间具有单调性,所以我们可以使用双指针来维护长度为 $k+1$的连续区间。这样能以$O(n)$的复杂度维护$nxt[i][0]$的内容。

//双指针维护
//第1个点的第k近的元素位置
int L=1,R=k+1;
//[L,R] 区间变化具有单调性,第i+1个点的区间L{i+1}>=L{i},R{i+1}>=R{i}
for(int i=2;i<=n;i++){
//求出第i个点第k近的元素位置 
while(R+1<=n && p[R+1]-p[i]<p[i]-p[L]){
  L++,R++;
} 

if(p[R]-p[i]>p[i]-p[L]) 
    //p[R]为第k小的元素;
    ;
else 
    //p[L]是第k小的元素
    ;
}

但是目前还是存在一个问题:空间大小。本题空间限制为 $125Mb$ ,若根据题意定义出的nxt二维数组会超空间。本题的步数$m$在给出后就不在发生变化,我们可以尝试压缩二维数组至一维。

观察之前求解的转移方程: $nxt[i][j]=nxt[nxt[i][j-1]][j-1]$ 。$2^j$步只由$2^{j-1}$步推出,可以尝试滚动数组压缩,且$m$固定,可在过程中更新 $m$ 的二进制位为$1$时的位置。

  int lg=log2(m);
  for(int i=0;i<=lg;i++){
    if((m>>i)&1){
      for(int j=1;j<=n;j++){
        ans[j]=nxt[ans[j]];
      }
    }
    //倍增:nxt[i][j]=nxt[nxt[i][j-1]][j-1]
    //节省空间压缩nxt空间为一维
    for(int j=1;j<=n;j++){
      tmp[j]=nxt[nxt[j]];
    }
    for(int j=1;j<=n;j++){
      nxt[j]=tmp[j];
    }
  }

解题流程

  1. 初始化:使用双指针维护长度为 $k+1$ 的连续区间,确定每个位置的第 $k$ 近石头。
  2. 倍增预处理:通过滚动数组优化,将 nxt 压缩为一维数组。
  3. 查询最终位置:通过对 $m$ 的二进制拆分,结合倍增数组快速计算最终位置。

代码实现

#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N = 1e6 + 5;
int n,k;//n个点,第k近
i64 m;//跳m次
i64 p[N];//石头位置
int nxt[N];//nxt[i]从位置i跳跃到第k近的目标位置
int ans[N],tmp[N];//ans[i] 表示从位置i跳跃m次后的最终位置,tmp用于临时存储
int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr);
  //输入数据
  cin>>n>>k>>m;
  for(int i=1;i<=n;i++){
    cin>>p[i];
  }
  //求每个点第k近的元素,前k近的元素分布i的两侧,是一个连续区间,长度为k+1
  //双指针维护
  nxt[1]=k+1;//第1个点的第k近的元素位置
  int L=1,R=k+1;
  //[L,R] 区间变化具有单调性,第i+1个点的区间L{i+1}>=L{i},R{i+1}>=R{i}
  for(int i=2;i<=n;i++){
    //求出第i个点第k近的元素位置 
    while(R+1<=n && p[R+1]-p[i]<p[i]-p[L]){
      L++,R++;
    } 
    //确定第k近的石头
    if(p[R]-p[i]>p[i]-p[L]) nxt[i]=R;//p[R]为第k小的元素
    else nxt[i]=L;//p[L]是第k小的元素
  }
  //初始化
  for(int i=1;i<=n;i++){
    ans[i]=i;
  }
  //倍增优化:对m进行二进制拆分
  int lg=log2(m);
  for(int i=0;i<=lg;i++){
    if((m>>i)&1){
      for(int j=1;j<=n;j++){
        ans[j]=nxt[ans[j]];
      }
    }
    //倍增:nxt[i][j]=nxt[nxt[i][j-1]][j-1]
    //节省空间压缩nxt空间为一维
    for(int j=1;j<=n;j++){
      tmp[j]=nxt[nxt[j]];//更新从j跳跃2^i的位置
    }
    for(int j=1;j<=n;j++){
      nxt[j]=tmp[j];
    }
  }
  //输出结果
  for(int i=1;i<=n;i++){
    cout<<ans[i]<<" ";
  }
  return 0;
}

总结回顾

本题的关键在于:

  1. 使用双指针快速确定第 $k$ 近的石头。
  2. 使用倍增加速多次跳跃的计算。
  3. 使用滚动数组优化空间。
最后修改:2025 年 03 月 04 日
如果觉得我的文章对你有用,请随意赞赏