[POI2010] ZAB-Frog
题目描述
在一条特别长且笔直的 Byteotian 小溪的河床上,有块岩石露出水面。它们到小溪源头的距离分别为。一只坐在其中一块岩石上的小青蛙即将开始它的跳跃训练。每次这只青蛙都会跳到距离它所坐岩石第(k)近的岩石上。具体来说,如果青蛙坐在位置为的岩石上,那么它会跳到满足以下条件的上:
如果不唯一,那么青蛙会在这些岩石中选择距离小溪源头最近的那块岩石。根据青蛙起跳时所坐的岩石,在跳了次之后,它会坐在哪块岩石上呢?
输入格式
标准输入的第一行包含三个整数、和,它们由单个空格分隔,分别表示:岩石的数量、参数以及计划跳跃的次数。第二行包含个整数,这些整数由单个空格分隔,表示小溪河床上连续岩石的位置。
输出格式
你的程序应该在标准输出上打印一行内容,其中包含个处于区间内的整数,这些整数之间用单个空格分隔。数字表示青蛙从编号为的岩石(按照输入顺序)出发,经过)次跳跃后最终所到达的岩石的编号。
样例 #1
样例输入 #1
5 2 4
1 2 4 7 10
样例输出 #1
1 1 3 1 1
题目分析
题意简述
题目要求计算一只青蛙从每块石头出发,经过 次跳跃后的位置。青蛙每次跳跃到第 近的石头上,如果有多块石头满足条件,则选择离小溪源头最近的那块石头。
模型抽象
基于距离的移动问题,青蛙的跳跃可以视为在有序数列中寻找第 近的元素。
初步思考
暴力解法:直接模拟青蛙的每次跳跃,直到完成 次跳跃。每次跳跃,都向两侧逐个遍历,确定第 近的位置,单次跳跃复杂度为 ,整体复杂度为 。但是从数据范围中可知m的范围很大,为 。暴力去模拟跳跃必然是会超时。
优化方向:每次一步步跳跃到第 近的位置的过程太慢,需要一种快速计算移动 次后的位置的方法。我们曾经在求【RMQ】和【最近公共祖先】,使用倍增的技巧处理过类似问题。且 可用 位以内的二进制进行表示。可以考虑使用倍增加速跳跃过程。
优化思考
设计 ,nxt[i][j] 为从i点跳跃次到达的位置。更新过程为 。更新预处理的复杂度为 。
在已知nxt[][] 的基础上,要求解跳跃m次后的位置可对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;
}
逻辑通。解决剩下的问题:如何求出从每个位置跳一次到达的位置?
根据题意,到达的位置为离第近的位置。题目中对于位置 有特殊性质:升序。那么对于离第个元素的前近的元素必然分布在的两侧,且形成一个包含长度为的连续区间,设该区间左右端点为和。第近的元素必然在端点处。那么如果 则 为第 近的元素,否则是 。
对于下一个元素 的前小元素形成的区间 一定不会在上一个区间的左侧,也就是 。
p[]是升序的, ,比较和 与左端点的距离在变大,比较和 与有端点的距离在变小,所以如果要更新连续区间,必然是要舍弃左端点的元素的,所以整体区间是在上一个区间的基础上右移,两个端点是同向移动,不会回头的。可推出该区间具有单调性,所以我们可以使用双指针来维护长度为 的连续区间。这样能以的复杂度维护的内容。
//双指针维护
//第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小的元素
;
}
但是目前还是存在一个问题:空间大小。本题空间限制为 ,若根据题意定义出的nxt二维数组会超空间。本题的步数在给出后就不在发生变化,我们可以尝试压缩二维数组至一维。
观察之前求解的转移方程: 。步只由步推出,可以尝试滚动数组压缩,且固定,可在过程中更新 的二进制位为时的位置。
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];
}
}
解题流程
- 初始化:使用双指针维护长度为 的连续区间,确定每个位置的第 近石头。
- 倍增预处理:通过滚动数组优化,将
nxt压缩为一维数组。 - 查询最终位置:通过对 的二进制拆分,结合倍增数组快速计算最终位置。
代码实现
#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;
}
总结回顾
本题的关键在于:
- 使用双指针快速确定第 近的石头。
- 使用倍增加速多次跳跃的计算。
- 使用滚动数组优化空间。