https://ac.nowcoder.com/acm/contest/11256/K
题意
给定长度为
n
n
n 的数列
a
[
]
a[]
a[],
m
m
m 次询问。
每次询问给定一个值
k
k
k,问一共有多少个不同的区间满足,其元素 最大值 - 最小值 > k
。
1
≤
n
≤
1
0
5
,
1
≤
m
≤
200
1≤n≤10^5,1≤m≤200
1≤n≤105,1≤m≤200
1
≤
a
i
≤
1
0
9
1≤a_i≤10^9
1≤ai≤109
1
≤
k
≤
1
0
9
1≤k≤10^9
1≤k≤109
思路
如果只有一个询问的话,可以遍历每个位置 i
,把该位置作为区间左端点,向右二分找第一个满足的右端点 j
,该位置和后面的所有位置都是满足的,区间个数贡献 n - j + 1
。
但时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),需要想办法优化。
假设有一个合法区间的左端点为 l
,右端点为 r
,当 l
右移的时候,区间中就少了几个数,可能这个区间就不合法了。为了让区间重新合法,右端点就需要右移。
所以,当左端点从左往右走时,为了构成合法区间,右端点是单调往后走的。
那么就可以用双指针来优化,时间复杂度 O ( n ) O(n) O(n)。
从前往后遍历每个位置 i
,把当前位置作为区间左端点,往后找第一个合法的右端点 j
,该位置和后面的所有位置都可以作为右端点,能够贡献 n-j+1
个区间。
判断区间是否合法可以用 两个单调队列分别维护一段移动区间的最大值和最小值,也可以用 ST表直接求区间最大值最小值。
Code:
法1:单调队列维护移动区间最值,O(n)
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define endl '\n'
const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N];
int q1[N], q2[N];
signed main(){
Ios;
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> a[i];
while(m --)
{
int x; cin >> x;
int st1 = 0, ed1 = -1; //最小值,递增
int st2 = 0, ed2 = -1; //最大值,递减
int ans = 0;
int j = 0;
for(int i=1;i<=n;i++)
{
while(st2 > ed2 || st1 > ed1 || a[q2[st2]] - a[q1[st1]] <= x) //往后找到第一个满足位置(当队列中没有值或者还没找到时继续找)
{
if(j == n) break; //如果到最后一个位置了还没找到,退出
j ++;
while(ed1 >= st1 && a[q1[ed1]] >= a[j]) ed1 --;
while(ed2 >= st2 && a[q2[ed2]] <= a[j]) ed2 --;
q1[++ed1] = j;
q2[++ed2] = j;
}
if(st2 <= ed2 && st1 <= ed1 && a[q2[st2]] - a[q1[st1]] > x) //如果找到了
ans += n - j + 1;
if(q1[st1] == i) st1 ++; //把当前位置从队列中清除
if(q2[st2] == i) st2 ++;
}
cout << ans << endl;
}
return 0;
}
法2:ST表维护任意区间最值,O(nlogn)
需要注意的是,这个题目中需要调用 query 的地方很多,而每次调用该函数都会调用一次 log()
函数,这个函数是比较耗时的,在这个题目中就超时了。
解决办法:
先把所有区间长度的 log 值预处理出现,存到数组中,后面调用直接查数组即可。总的区间长度为 1e5,那么子区间长度最多也 1e5 个。
另外可以用内置函数 __builtin_log()
函数替换 log()
,这个快一点。
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define endl '\n'
const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N];
int q1[N], q2[N];
int Fmin[N][30], Fmax[N][30];
double Log[N], Log2 = log(2);
void ST()
{
for(int i=1;i<=n;i++) Fmin[i][0] = Fmax[i][0] = a[i];
int k = log(n) / log(2);
for(int j=1;j<=k;j++)
for(int i=1;i<=n;i++){
Fmin[i][j] = min(Fmin[i][j-1], Fmin[i + (1 << j - 1)][j - 1]);
Fmax[i][j] = max(Fmax[i][j-1], Fmax[i + (1 << j - 1)][j - 1]);
}
}
int query_max(int l, int r){
int k = Log[r - l + 1] / Log2;
return max(Fmax[l][k], Fmax[r - (1 << k) + 1][k]);
}
int query_min(int l, int r){
int k = Log[r - l + 1] / Log2;
return min(Fmin[l][k], Fmin[r - (1 << k) + 1][k]);
}
signed main(){
Ios;
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> a[i];
for(int i=1;i<=n;i++) Log[i] = log(i);
ST();
while(m --)
{
int x; cin >> x;
int ans = 0;
int j = 1;
for(int i=1;i<=n;i++)
{
while(j < n && query_max(i, j) - query_min(i, j) <= x) j ++; //找到后面第一个满足的位置
if(query_max(i, j) - query_min(i, j) > x) //如果找到了
ans += n - j + 1;
}
cout << ans << endl;
}
return 0;
}
之前遇这种区间最值问题都是直接 ST表,单调队列练的少,这道题竟然被卡了。
只要需要求区间最值的区间是一个位置一个位置移动的,那么就可以用单调队列来维护最值,处理这种移动的区间最值问题。时间复杂度 O(n),比 ST表 的 O(nlogn) 效率更高。