POI2010 ZAB-Frog:倍增优化与双指针的巧妙结合

85 阅读1分钟

[POI2010] ZAB-Frog

题目描述

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

{pa:papi<pjpi}k 且 {pa:papipjpi}>k|\{ 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

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

输入格式

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

输出格式

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

样例 #1

样例输入 #1

5 2 4
1 2 4 7 10

样例输出 #1

1 1 3 1 1

题目分析

题意简述

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

模型抽象

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

初步思考

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

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

优化思考

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

在已知nxt[][] 的基础上,要求解跳跃m次后的位置可对m进行二进制拆分。单次查询复杂度为 O(logm)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;
}

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

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

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

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

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

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

  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+1k+1 的连续区间,确定每个位置的第 kk 近石头。
  2. 倍增预处理:通过滚动数组优化,将 nxt 压缩为一维数组。
  3. 查询最终位置:通过对 mm 的二进制拆分,结合倍增数组快速计算最终位置。

代码实现

#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. 使用双指针快速确定第 kk 近的石头。
  2. 使用倍增加速多次跳跃的计算。
  3. 使用滚动数组优化空间。