题目
输入一串数字,给你 M 个询问,每次询问就给你两个数字 X,Y,要求你说出 X 到 Y 这段区间内的最大数。
输入格式
第一行两个整数 N,M 表示数字的个数和要询问的次数;
接下来一行为 N 个数;
接下来 M 行,每行都有两个整数 X,Y。
输出格式
输出共 M 行,每行输出一个数。
数据范围
1
≤
N
≤
1
0
5
1≤N≤10^5
1≤N≤105,
1
≤
M
≤
1
0
6
1≤M≤10^6
1≤M≤106,
1
≤
X
≤
Y
≤
N
1≤X≤Y≤N
1≤X≤Y≤N,
数列中的数字均不超过
2
31
−
1
2^{31}−1
231−1
Solution#1 RMQ
RMQ(Range Minimum/Maximum Query),即区间最值查询,是指这样一个问题:对于长度为n的数列A,回答若干次询问RMQ(i,j),返回数列A中下标在区间[i,j]中的最小/大值。
本文介绍一种比较高效的ST算法解决这个问题。ST(Sparse Table)算法可以在O(nlogn)时间内进行预处理,然后在O(1)时间内回答每个查询。
1)预处理
设A[i]是要求区间最值的数列,F[i, j]表示从第i个数起连续2^j个数中的最大值。(DP的状态)
例如:
A数列为:3 2 4 5 6 8 1 2 9 7
F[1,0]表示第1个数起,长度为2^0=1的最大值,其实就是3这个数。同理 F[1,1] = max(3,2) = 3, F[1,2]=max(3,2,4,5) = 5,F[1,3] = max(3,2,4,5,6,8,1,2) = 8;
并且我们可以容易的看出F[i,0]就等于A[i]。(DP的初始值)
我们把F[i,j]平均分成两段(因为F[i,j]一定是偶数个数字),从 i 到i + 2 ^ (j - 1) - 1为一段,i + 2 ^ (j - 1)到i + 2 ^ j - 1为一段(长度都为2 ^ (j - 1))。于是我们得到了状态转移方程F[i, j]=max(F[i,j-1], F[i + 2^(j-1),j-1])。
2)查询
假如我们需要查询的区间为(i,j),那么我们需要找到覆盖这个闭区间(左边界取i,右边界取j)的最小幂(可以重复,比如查询1,2,3,4,5,我们可以查询1234和2345)。
因为这个区间的长度为j - i + 1,所以我们可以取k=log2( j - i + 1),则有:RMQ(i, j)=max{F[i , k], F[ j - 2 ^ k + 1, k]}。
举例说明,要求区间[1,5]的最大值,k = log2(5 - 1 + 1)= 2,即求max(F[1, 2],F[5 - 2 ^ 2 + 1, 2])=max(F[1, 2],F[2, 2]);
#include <cstdio>
#include <cmath>
using namespace std;
const int N = 1e5+5;
int n,m;
int a[N],f[N][20];
int rmq(int l,int r)
{
int k=0;
while((1<<(k+1))<=(r-l+1))
k++;
return max(f[l][k],f[r-(1<<k)+1][k]);
}
int main()
{
scanf("%d%d", &n, &m);
int x,y;
for(int i=1; i<=n; i++)
{
scanf("%d",&a[i]);
f[i][0] = a[i];
}
for(int j=1; (1<<j)<=n; j++)
for(int i=1; i+(1<<j)-1<=n; i++)
f[i][j] = max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
for(int i=1; i<=m; i++)
{
scanf("%d%d",&x,&y);
printf("%d\n",rmq(x,y));
}
return 0;
}
Solution#2 线段树
对常规线段树的修改:
- 原本父节点是左右子树之和,这里修改为左右子树的最大值
- 查询过程中,考虑负数情况,把原本返回0的地方修改返回最小值
- 时间要求严格,要卡常数(用scanf和printf)
#include <iostream>
#include <cstdio>
#include <cmath>
#include <climits>
using namespace std;
const int N = 1e5+5;
int a[N],t[4*N];
void bulid(int node,int start,int end)
{
if(start==end)
t[node]=a[start];
else
{
int mid=start+end>>1;
bulid(node<<1,start,mid);
bulid(node<<1|1,mid+1,end);
t[node]=max(t[node<<1],t[node<<1|1]);
}
}
int query(int node,int start,int end,int l,int r)
{
int min_inf=INT_MIN;
if(end<l || start>r)
return min_inf;
else if(start==end)
return t[node];
else if(l<=start && end<=r)
return t[node];
else
{
int mid = start+end>>1;
int left_max = max(min_inf,query(node<<1,start,mid,l,r));
int right_max = max(min_inf,query(node<<1|1,mid+1,end,l,r));
return max(left_max,right_max);
}
}
int main()
{
int n,m,x,y;
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++) scanf("%d",&a[i]);
bulid(1,1,n);
while (m -- )
{
scanf("%d%d",&x,&y);
printf("%d\n",query(1,1,n,x,y));
}
return 0;
}
Solution#3 树状数组
暂时留个坑,因为对树状数组的原理了解不深,待深入学习后回来补充,把代码一步步跑完,发现树状数组是个很神奇的结构,把二进制玩活了。贴上代码,供感兴趣的各位学习。
#include <bits/stdc++.h>
using namespace std;
const int N = 15;
int nums[N], bit[N], n, m;
inline int lowbit(int x) {
return x & -x;
}
void build() { // 初始化树状数组
for (int i = 1; i <= n; ++ i) {
bit[i] = nums[i];
for (int j = 1; j < lowbit(i); j <<= 1)
bit[i] = max(bit[i], bit[i - j]);
}
}
int query(int l, int r) { // 区间查询
int maxv = INT_MIN;
while (l <= r) {
maxv = max(maxv, nums[r]);
-- r;
for (; l <= r - lowbit(r); r -= lowbit(r))
maxv = max(maxv, bit[r]);
}
return maxv;
}
int main()
{
int l, r;
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; ++ i) scanf("%d", &nums[i]);
build();
while (m --) {
scanf("%d %d", &l, &r);
printf("%d\n", query(l, r));
}
return 0;
}
/*from:https://www.acwing.com/solution/content/83790/*/