题目描述
难度分:2200
输入T(≤104)表示T组数据。所有数据的n之和≤2×105,q之和≤2×105。
每组数据输入n、k(1≤k≤n≤2×105)、q(1≤q≤2×105)和长为n的数组a(1≤a[i]≤n)。
对于一个数组b,你可以执行若干次操作,每次操作可以把一个b[i]改成任意值。
定义f(b)表示使b中存在长度至少为k的连续递增子数组的最小操作次数。(连续递增:对任意i满足b[i+1]=b[i]+1)
然后输入q个询问,每个询问输入l和r,下标从1开始,保证r−l+1≥k。
定义a[i..j]表示a的下标从i到j的子数组。对于每个询问,输出f(a[l..l+k−1])+f(a[l..l+k])+…+f(a[l..r])。
输入样例
3
7 5 3
1 2 3 2 1 2 3
1 7
2 7
3 7
8 4 2
4 3 1 1 2 4 3 2
3 6
1 5
5 4 2
4 5 1 2 3
1 4
1 5
输出样例
6
5
2
2
5
2
3
算法
单调栈+倍增
这题感觉真的很难,不是很懂怎么在div4出现,而且灵佬还拿来当成周四的茶。
连续递增,等价于对任意的i≤j都有a[j]=a[i]+(j−i)成立,即a[j]−j=a[i]−i。所以把a[i]变成a[i]−i,这样「连续递增」就变成了「元素值都相等」。
首先,跑一个定长滑动窗口,计算每个长为k的子数组的众数出现次数,那么「k-众数出现次数」就是这个子数组最少需要修改的元素个数。用一个counter统计窗口内的元素频数,另一个有序集合tup记录counter中的键值对。这两个数据结构辅助,然后滑动窗口就能得到每个长度为k的窗口a[i…i+k−1]的f值。
每个子数组的「k-众数出现次数」记录在一个长为n−k+1的数组mn中。接下来就发现一个很隐秘的事实:
- 一个长为k+1的子数组的f值,等于其内部2个长为k的子数组的「k-众数出现次数」的最小值。
- 一个长为k+2的子数组的f值,等于其内部3个长为k的子数组的「k-众数出现次数」的最小值。
……
这样对于每个询问[l,r],我们求子数组a[l…l+k−1]、a[l+1…l+k]、……、a[r−k+1…r]最小操作数的累加和即可。但如果直接遍历这些区间的话时间复杂度就太高了,可以用倍增来优化。f[i][j]表示mn[i]后第2j个比mn[i]小的数的索引,s[i][j]表示i到f[i][j]之前对应子数组最小操作数的累加和。f[i][0]可以用单调栈求出来,而s[i][0]=(f[i][0]−i)×mn[i],接下来用倍增的预处理计算出整个f和s数组。
而对于每个询问[l,r],利用f数组,以二进制的方式来遍历[l…l+k−1]、[l+1…l+k]、……、[r−k+1…r]这些子数组,利用s数组求出这些最小操作数之和就可以了。说得简单,但如果对倍增理解不够深刻的话写起来也挺晕的。
复杂度分析
时间复杂度
预处理出mn需要滑动窗口遍历整个数组a,时间复杂度为O(n)。对于每个窗口,需要维护有序集合tup中的二元组,存在删除和插入操作,时间复杂度为O(log2n)。所以这一步的时间复杂度为O(nlog2n)。
接下来单调栈+倍增预处理出s和f两个数组,由于询问[l,r]的极限区间长度就是O(n)级别的,所以这一步预处理的时间复杂度为O(nlog2n)。
最后处理每个询问,每个询问都要用二进制的方式来凑r−l+1级别的长度,最差情况下为O(n)长度的区间。而用二进制来凑这个长度就是O(log2n),所以处理所有询问的总时间复杂度就是O(qlog2n)。
综上,整个算法的时间复杂度为O((n+q)log2n)。
空间复杂度
空间消耗的瓶颈在于两个ST表s和f,空间消耗就是O(nlog2n)。mn、counter、tup这几个数据结构中存储的元素数量最多就是O(n)级别。所以整个算法的额外空间复杂度为O(nlog2n)。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 200010, K = 18;
int T, n, k, q, a[N], mn[N];
LL f[N][K], s[N][K];
int main() {
scanf("%d", &T);
while(T--) {
scanf("%d%d%d", &n, &k, &q);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
a[i] -= i;
}
map<int, int> counter;
for(int i = 1; i <= k; i++) {
counter[a[i]]++;
}
set<PII> tup;
for(auto&[num, cnt]: counter) {
tup.insert({cnt, num});
}
int freq = tup.rbegin()->first, mode = tup.rbegin()->second;
mn[1] = k - freq;
for(int i = k + 1; i <= n; i++) {
int j = i - k;
int val = a[j];
int oldfreq = counter[a[j]];
tup.erase({oldfreq, val});
counter[a[j]]--;
if(counter[a[j]] == 0) {
counter.erase(a[j]);
}else {
tup.insert({counter[a[j]], a[j]});
}
oldfreq = counter[a[i]];
tup.erase({oldfreq, a[i]});
counter[a[i]]++;
tup.insert({oldfreq + 1, a[i]});
freq = tup.rbegin()->first, mode = tup.rbegin()->second;
mn[i - k + 1] = k - freq;
}
// 单调栈求每个mn[i]下一个更小数的位置
stack<int> stk;
for(int i = n; i >= 1; i--){
while(stk.size() && mn[stk.top()] >= mn[i]) {
stk.pop();
}
f[i][0] = stk.size()? stk.top(): n + 1;
s[i][0] = (f[i][0] - i) * mn[i];
stk.push(i);
}
for(int i = 0; i < K; i++) {
f[n + 1][i] = n + 1;
}
for(int j = 1; j < K; j++) {
for(int i = 1; i <= n; i++){
f[i][j] = f[f[i][j - 1]][j - 1];
s[i][j] = s[i][j - 1] + s[f[i][j - 1]][j - 1];
}
}
while(q--) {
int l, r;
scanf("%d%d", &l, &r);
r = r - k + 1;
LL ans = 0;
// 用二进制来凑这个r-l+1的距离
for(int i = K - 1; i >= 0; i--) {
if(f[l][i] <= r) {
ans += s[l][i];
l = f[l][i];
}
}
ans += (r - l + 1LL) * mn[l];
printf("%lld\n", ans);
}
}
return 0;
}