[POI 2010] ZAB-Frog
用双指针维护最近 k+1 块石头的连续窗口求单步目标,再用映射的快速幂处理 m 次跳跃。
OJ: luogu
题目 ID: P3509
难度:提高
标签:双指针倍增函数复合
日期: 2026-07-16 18:28
题意
石头坐标严格递增。青蛙每次跳到距离排名第
先看一个直接按题意模拟的朴素解:
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-08-06 22:43
* update_at: 2026-08-06 22:43
*/
// brute.cpp:小数据暴力解,直接按题意模拟 m 次跳跃,每次暴力找出第 k 近的石头。
// 只适合 n、m 都很小的情况,用来理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int n, k;
long long m;
long long p[MAXN]; // 石头位置,严格递增
// 从石头 start 出发,走 m 步后停在哪个石头。
int simulate(int start) {
int cur = start;
for (long long step = 1; step <= m; step++) {
// 把所有石头按 (距离, 编号) 排序,找出第 k 小的距离。
vector<pair<long long, int>> dist;
for (int i = 1; i <= n; i++) {
if (i == cur) continue;
dist.push_back(make_pair(llabs(p[i] - p[cur]), i));
}
sort(dist.begin(), dist.end());
// 注意:平局(多个石头距离相同且都满足条件)时,
// 题目要求选离源头最近(编号最小)的石头,不能直接取排序后的第 k 个。
long long d_star = dist[k - 1].first;
cur = INT_MAX;
for (int i = 0; i < (int)dist.size(); i++) {
if (dist[i].first == d_star) cur = min(cur, dist[i].second);
}
}
return cur;
}
int main() {
scanf("%d%d%lld", &n, &k, &m);
for (int i = 1; i <= n; i++) {
scanf("%lld", &p[i]);
}
for (int i = 1; i <= n; i++) {
if (i > 1) printf(" ");
printf("%d", simulate(i));
}
printf("\n");
return 0;
}这个暴力每次跳跃都把全部石头按(距离, 编号)排序,找第
思路
一句话本质:把跳
能不能把每跳一次的代价降下来?
暴力每跳一次都要扫描全部石头重新找第 nxt[i] 完全可以只算一次,后面复用它跳
怎么高效找一个石头的第
距离的分布有一个简单规律:石头坐标严格递增,所以越靠近
离
是的。假如存在一个空洞(一块石头落在"最近
这个窗口里按距离从近到远数是:
不需要,窗口只会整体右移——用反证法:
当
但
(用位置差表示距离)所有石头都在一维直线上,距离就是坐标差的绝对值。又因为
现在把
下面解释这个数学知识。
三角不等式是说:平面上任意三个点 A、B、C 之间,"走直线"不会比"经 B 绕路"更长——即
两边之和大于等于第三边。当三个点在一条直线上且 B 恰好落在 A 和 C 的中间时,不等号变成等号:
因为从 A 到 C 没有更短的路径,A→B→C 恰好依次走过整条线段。
本题所有石头都在一条小溪(直线)上,所以这个取等情形天然成立。具体到这里:
p_{l-1} ------- p_i --- p_{i+1}
|<--- dist(l-1, i) --->|<-d->|
|<------ dist(l-1, i+1) ------>|这一步拆分了
将
也就是说,
因此窗口的左、右边界都不可能左移,只能整体右移。用两个指针
单步目标算完了,跳
每块石头恰好有一条出边,把跳一步看作函数
关键在内存:直接存 60 层跳表要
具体做法:
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-08-06 22:43
* update_at: 2026-08-06 22:58
*/
// P3509 [POI 2010] ZAB-Frog
// 每个石头一步跳到距它第 k 近的石头(平局取离源头最近),
// 求每个起点跳 m 次后的位置。
//
// 思路:
// 1) 双指针 O(n) 求出每个石头的单步目标 nxt[i];
// 2) 把"跳一步"看成函数 f,用二进制倍增(映射的快速幂)求 f^m。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
int n, k;
long long m; // 跳跃次数,最大 1e18
long long p[MAXN]; // 石头位置,严格递增
int nxt[MAXN]; // nxt[i]:从石头 i 跳一步到达的石头编号
int F[MAXN]; // 倍增滚动数组,F[i] 表示当前层 2^j 步能到达的石头
int TMP[MAXN]; // 平方时使用的临时数组
int ANS[MAXN]; // ANS[i]:从 i 出发已走完低位块后的位置
// 用双指针求所有石头的一步目标。
// 关键观察:离石头 i 最近的 k+1 块石头(含 i 自己)在有序数组中构成连续窗口 [l, r];
// 第 k 近的石头就是窗口中离 i 较远的那个端点(两端距离相等时取左端,即离源头最近)。
void calc_next() {
int l = 1;
int r = k + 1; // 初始窗口 [1, k+1],保证第 1 块石头右边有 k 块
for (int i = 1; i <= n; i++) {
// 右边下一个石头比左端点更近时,整个窗口右移一格。
// l、r 只增不减,所以总移动次数为 O(n)。
while (r + 1 <= n && p[r + 1] - p[i] < p[i] - p[l]) {
l++;
r++;
}
if (p[r] - p[i] > p[i] - p[l]) {
nxt[i] = r; // 右端点严格更远,第 k 近在右边
} else {
nxt[i] = l; // 左端点更远或两端等距,取左端(离源头最近)
}
}
}
// 映射的快速幂:对每个起点求 f^m 后的位置,结果写入 ANS。
// 与 rbook 模板 quick-pow 同构:m 的每一位为 1 时把当前层 f^(2^j) 作用到答案上,
// 每层再把函数平方成 f^(2^(j+1))(F[F[i]] 相当于映射合成两次)。
// 调用前要求 F 初始化为 f^1,ANS 初始化为原地(0 步)。
void jump_power(long long m) {
while (m > 0) {
if (m & 1) { // 当前 2^j 这一位是 1,走这一块
for (int i = 1; i <= n; i++) {
ANS[i] = F[ANS[i]];
}
}
for (int i = 1; i <= n; i++) { // 平方:f^(2^(j+1)) = f^(2^j) 两次
TMP[i] = F[F[i]];
}
for (int i = 1; i <= n; i++) {
F[i] = TMP[i];
}
m >>= 1;
}
}
void read_input() {
scanf("%d%d%lld", &n, &k, &m);
for (int i = 1; i <= n; i++) {
scanf("%lld", &p[i]);
}
}
void solve() {
calc_next();
// 每个点只有一条出边,所以"跳 2^j 步"可以当函数整体平方。
for (int i = 1; i <= n; i++) {
F[i] = nxt[i]; // 第 0 层:1 步
ANS[i] = i; // 0 步:还在原地
}
jump_power(m); // 从低位到高位拼出 m 步
for (int i = 1; i <= n; i++) {
if (i > 1) printf(" ");
printf("%d", ANS[i]);
}
printf("\n");
}
int main() {
read_input();
solve();
return 0;
}复杂度
双指针求单步目标
总结
- 第
近的等价说法:最近 块中离 最远的那块。 - 最近
块在有序数组中连续成窗口,且窗口单调右移,是双指针的典型特征。 - 每个点一条出边 ⟹ 跳
步 = 函数复合,二进制倍增压层数;内存紧张时用滚动数组代替二维跳表。
图示解析
这张图展示从题目到最终解法的完整路线:
题面:n 块石头(位置递增),每次跳到第 k 近(平局取最左),跳 m 次
│
├─ 暴力(brute.cpp):每跳一次全量排序找第 k 近,逐次模拟 m 次
│ 瓶颈:O(n·m·nlogn),n=1e6、m=1e18 不可行
│
├─ 关键观察 A:第 k 近 = 最近 k+1 块石头中离 i 最远的一块
├─ 关键观察 B:最近 k+1 块在有序数组中连续成窗口
├─ 关键观察 C:窗口随 i 单调右移
│ └─ 双指针 O(n) 求出所有单步目标 nxt[i]
│
└─ 关键观察 D:每点一条出边,跳 m 次 = f^m
└─ 二进制倍增(滚动平方)O(n log m) 拼出 m 步从上到下看,题目先被拆成"求单步目标"和"走 m 步"两个子问题;前者靠窗口连续性用双指针
