【例 2】最敏捷的机器人(信息学奥赛一本通- P1542)
【题目描述】
Wind 设计了很多机器人。但是它们都认为自己是最强的,于是,一场比赛开始了……
机器人们都想知道谁是最敏捷的,于是它们进行了如下一个比赛。首先,他们面前会有一排共 n 个数,它们比赛看谁能最先把每连续 k 个数中最大和最小值写下来,当然,这些机器人运算速度都很快,它们比赛的是谁写得快。
但是 Wind 也想知道答案,你能帮助他吗?
【输入】
第一行为 n,k,意义如题目描述。
第二行共 n 个数,为数字序列,所有数字均在 Pascal 的 longint 范围内,即所有数均为整数,且在 [−2^31,2^31−1]范围内。
【输出】
共 n−k+1 行,第 i 行为第 i 至第 i+k−1 这 k 个数中的最大和最小值。
【输入样例】
5 3
1 2 3 4 5
【输出样例】
3 1
4 2
5 3
【提示】
数据范围与提示:
对于全部数据,1≤k≤n≤10^5 。
题目分析
本题是极其经典的静态区间最值查询(RMQ)模型。 给定一个长度为 N 的序列,要求一个长度为 K 的窗口从左向右滑动,每次输出窗口内包含的 K 个元素的最大值和最小值。
核心特征:
-
纯静态查询:序列在初始化后没有任何修改操作,只有查询。
-
高频次查询:一共需要进行 N−K+1 次区间最值查询。
-
数据范围:1≤K≤N≤10^5,且序列元素横跨整个 32 位有符号整型 [−2^31,2^31−1]。
解题思路与思考过程
面对纯静态的 RMQ 问题,最正统的两种数据结构打法分别是ST表和线段树。
-
路径一(ST表):既然没有修改操作,优先考虑利用倍增思想预处理。预处理虽然花费 O(NlogN),但查询能达到极致的 O(1)。面对高频查询,ST 表是绝对的性能王者。为了方便后续扩展或查具体位置,ST 表内部存最值的“下标”比直接存“数值”更具普适性。
-
路径二(线段树):用分治思想将序列转化为树形结构。建树极快,但每次查询需要从根节点向下探索,时间复杂度为 O(logN)。虽然常数略大,但它是所有区间问题的万能基座,逻辑极其严密。
算法设计
1. ST 表(倍增法)
-
状态定义:设
ma[i][j]表示从下标 i 开始,长度为 2^j 的区间内的最大值下标。 -
状态转移:将长度为 2^j 的区间,从中间劈开,变成两个长度为 2^(j−1) 的区间。 即:比较
[i, i+2^(j-1)-1]和[i+2^(j-1), i+2^j-1]这两段的最值。 -
O(1) 查询:查询区间 [L,R] 时,找到一个最大的k,使得 2^k≤R−L+1。然后分别从 L 往右取长度 2^k,从 R 往左取长度 2^k。这两段必然能完全覆盖目标区间,取两者的最值即可。
2. 线段树(分治法)
-
节点结构:每个节点管辖一段物理区间
[l,r],维护该区间内的ma(最大值)和mi(最小值)。 -
向上合并 (
pushup):父节点的最值严格等于左儿子和右儿子最值中的最值。 -
区间查询 (
query):-
完全包含:直接返回当前节点维护的值。
-
完全不相交:返回极其安全的越界兜底值(最大值兜底负无穷,最小值兜底正无穷)。
-
部分相交:递归向左右子树索要答案并合并。
-
时空复杂度分析
| 算法 | 预处理/建树时间 | 单次查询时间 | 整体时间复杂度 | 空间复杂度 |
|---|---|---|---|---|
| ST表 | O(NlogN) | O(1) | O(NlogN+M) | O(NlogN) |
| 线段树 | O(N) | O(logN) | O(N+MlogN) | O(N) |
| (注:本题中查询次数 M=N−K+1) |
坑点总结:
-
log2()的浮点运算陷阱(ST表致命伤): 在 ST 表的 O(1) 查询中,如果直接使用<cmath>库的log2()函数,会引入极其耗时的浮点数运算指令,导致查询性能断崖式下跌。 优化:可以选择替换为 C++ 内置的硬件级位运算函数__lg(),我代码中还是以log2()为例。 -
绝对极值的越界与溢出(线段树致命伤): 数据下限达到了 −2^31。如果不使用
long long类型的无穷大30000000000LL,或者错误地使用0作为越界兜底值,会在遇到全负数数据时发生“最大值被篡改为 0”的惨剧。 正解:明确定义带有LL后缀的超大常量INF,且求最大值越界必须返回-INF,求最小值越界必须返回INF。
核心代码与完整注释
版本一:ST表
//st表
#include <iostream>
#include <cmath>
using namespace std;
int n,kk;
long long a[100010];//原数字串
//ma[i][j]表示以i为起点的长度为2^j的区间的最大值的下标
int ma[100010][20];
//mi[i][j]表示以i为起点的长度为2^j的区间的最小值的下标
int mi[100010][20];
//预处理st表
void pre(){
//每一段区间长度为1的最大值的下标为自己
for(int i=1;i<=n;i++) ma[i][0]=i;
//每一段区间长度为1的最小值的下标为自己
for(int i=1;i<=n;i++) mi[i][0]=i;
//外层循环枚举区间长度的指数j
for(int j=1;(1<<j)<=n;j++){
//内层循环枚举起点i 确保右端点i+2^j-1不越界
for(int i=1;i+(1<<j)-1<=n;i++){
//区间长度的一半
int tmp=1<<(j-1);
//状态转移 比较左半边和右半边的最大值
if(a[ma[i][j-1]]>a[ma[i+tmp][j-1]])
ma[i][j]=ma[i][j-1];
else
ma[i][j]=ma[i+tmp][j-1];
//状态转移 比较左半边和右半边的最小值
if(a[mi[i][j-1]]<a[mi[i+tmp][j-1]])
mi[i][j]=mi[i][j-1];
else
mi[i][j]=mi[i+tmp][j-1];
}
}
}
//区间查询
void query(int l,int r){
int k=log2(r-l+1);
int tmp=1<<k;//找出一个最大的2^k长度
//从左端点向右取2^k 从右端点向左取2^k 两段必然完全覆盖[l,r]
//先输出最大值
cout<<max(a[ma[l][k]],a[ma[r-tmp+1][k]])<<" ";
//再输出最小值
cout<<min(a[mi[l][k]],a[mi[r-tmp+1][k]]);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>kk;
for(int i=1;i<=n;i++) cin>>a[i];
//先进行预处理 得到ma mi数组
pre();
//查询
for(int i=1;i+kk-1<=n;i++){
query(i,i+kk-1);
cout<<"\n";
}
}
版本二:线段树
//线段树做法
#include <iostream>
using namespace std;
int n,k;
const int maxn=100010;
const long long INF=30000000000ll;
int a[maxn];//原数列
//线段树节点
struct node{
int ma;//区间最大值
int mi;//区间最小值
}tree[maxn<<2];
//向上汇总信息 父节点=max/min(左儿子,右儿子)
void pushup(int rt){
tree[rt].ma=max(tree[rt<<1].ma,tree[rt<<1|1].ma);
tree[rt].mi=min(tree[rt<<1].mi,tree[rt<<1|1].mi);
}
//当前节点为rt 当前节点管辖区间为[l,r]
void build(int l,int r,int rt){
if(l==r){//到达物理叶子节点
tree[rt].ma=a[l];
tree[rt].mi=a[l];
return;
}
int mid=(l+r)>>1;
build(l,mid,rt<<1);
build(mid+1,r,rt<<1|1);
pushup(rt);//儿子建完后 更新父亲
}
//最大值查询
//线段树查询 当前查询区间是[L,R] 当前节点是rt 当前节点
//管辖区间是[l,r]
long long query1(int L,int R,int l,int r,int rt){
//目标完全包围当前区间 直接提取当前区间数据
if(L<=l&&R>=r) return tree[rt].ma;
//这里不能return 0 因为序列有正数有负数 最大值完全可能小于0
//所以要返回一个极小值
if(r<L||l>R) return -INF;
int mid=(l+r)>>1;
//找最大值 因为每个数可能为负 所以初始化一个极大的负数
//所以要返回一个极小值
long long ans=-INF;
//如果目标和左边区间有覆盖 去左边
if(L<=mid) ans=max(ans,query1(L,R,l,mid,rt<<1));
//如果目标和右边区间有覆盖 去右边
if(R>mid) ans=max(ans,query1(L,R,mid+1,r,rt<<1|1));
return ans;
}
//最小值查询
//线段树查询 当前查询区间是[L,R] 当前节点是rt 当前节点
//管辖区间是[l,r]
long long query2(int L,int R,int l,int r,int rt){
if(L<=l&&R>=r) return tree[rt].mi;
//这里不能return 0 因为序列有正数有负数 最小值完全可能大于0
//所以要返回一个极大值
if(r<L||l>R) return INF;
int mid=(l+r)>>1;
//找最小值 所以初始化一个极大的数
long long ans=INF;
if(L<=mid) ans=min(ans,query2(L,R,l,mid,rt<<1));
if(R>mid) ans=min(ans,query2(L,R,mid+1,r,rt<<1|1));
return ans;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++) cin>>a[i];
//建树
build(1,n,1);
//查询
for(int i=1;i+k-1<=n;i++){
//查最大值
cout<<query1(i,i+k-1,1,n,1)<<" ";
//查最小值
cout<<query2(i,i+k-1,1,n,1)<<"\n";
}
return 0;
}
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)