这道题目的内容相对来讲比较复杂, 我会在这里尽可能详细的解释代码及思路, 以便我自己梳理清楚思路
原题链接: G2. Yunli‘s Subarray Queries (hard version)
原题目
题目解析:
这道题给我们一个长度为 n 的数组, q 次询问, 让我们在 l 到 r 的范围内构建一个长度为 k 的连续子数组, 问分别以 l + k - 1 到 r 为连续子数组的结尾, 求需要的操作次数之和
与 easy 版不同的是, 这道题里每个询问都是上道题里一段答案之和, 因此本能的想用一个简单的求和, 然后中间不断取最小值
但是会超时
那么如何去优化这一问题?
我们发现, 这道题要求我们求一段值, 但是这段的值会随着左右区间的不同改变, 将其按左区间从大到小排序, 每次修改一段, 那么在这种情况下, 就能解决左面更小的值让后面的值变小的问题, 而同时修改一段并求一段之和, 这可以用线段树在很短的时间内解决
代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 200010;
typedef long long LL;
int a[N], l[N], r[N], f[N], ans[N], p[N], stk[N];
struct Node
{
int l, r, tag;
int sum;
}tr[N * 4];
void build(int u, int l, int r)
{
tr[u] = {l, r, -1};
if(l != r)
{
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
}
}
void pushup(int u, int c)
{
tr[u].sum = (tr[u].r - tr[u].l + 1) * c;
tr[u].tag = c;
}
void pushdown(int u)
{
if(tr[u].tag == -1)
{
return;
}
pushup(u << 1, tr[u].tag);
pushup(u << 1 | 1, tr[u].tag);
tr[u].tag = -1;
}
void modify(int u, int l, int r, int c)
{
if(tr[u].l >= l && tr[u].r <= r)
{
pushup(u, c);
}
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if(l <= mid) modify(u << 1, l, r, c);
if(r >= mid + 1) modify(u << 1 | 1, l, r, c);
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
}
int query(int u, int l, int r)
{
if(l <= tr[u].l && r >= tr[u].r)
{
return tr[u].sum;
}
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if(r <= mid) return query(u << 1, l, r);
if(l >= mid + 1) return query(u << 1 | 1, l, r);
return query(u << 1, l, r) + query(u << 1 | 1, l, r);
}
void solve()
{
int n, k, q;
map<int, int> mp;
multiset<int> s;
cin >> n >> k >> q;
for(int i = 1; i <= n; i ++ )
{
cin >> a[i];
a[i] -= i;
s.insert(0);
}
for(int i = 1; i <= n; i ++ )
{
s.erase(s.find(mp[a[i]]));
mp[a[i]] ++;
s.insert(mp[a[i]]);
if(i - k > 0)
{
s.erase(s.find(mp[a[i - k]]));
s.insert(-- mp[a[i - k]]);
}
if(i >= k)
{
f[i - k + 1] = k - *s.rbegin();
}
}
for(int i = 1; i <= q; i ++ )
{
cin >> l[i] >> r[i];
p[i] = i;
}
sort(p + 1, p + q + 1, [&] (int a, int b) {
return l[a] > l[b];
});
build(1, 1, n - k + 1);
int tp = 0;
stk[ ++ tp] = n - k + 2;
f[n - k + 2] = -1;
for(int i = 1, j = n - k + 1; i <= q; i ++ )
{
while(j >= l[p[i]])
{
while(f[stk[tp]] > f[j])
{
tp --;
}
modify(1, j, stk[tp] - 1, f[j]);
stk[++ tp] = j --;
}
ans[p[i]] = query(1, l[p[i]], r[p[i]] - k + 1);
}
for(int i = 1; i <= q; i ++ )
{
cout << ans[i] << "\n";
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T = 1;
cin >> T;
while(T -- )
{
solve();
}
}
注释
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 5;
int a[N], f[N], l[N], r[N], p[N];
struct Node
{
int l, r, tag;
LL s;
} tr[N * 4];
int stk[N];
LL ans[N];
// 创建线段树, 模板
void build(int u, int l, int r)
{
// 当前这一节点
tr[u] = {l, r, -1};
// 如果还可以再细分
if (l != r)
{
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
}
}
void pushup(int u, int c)
{
// 区间长度 * 值
tr[u].s = (tr[u].r - tr[u].l + 1ll) * c;
tr[u].tag = c;
}
void pushdown(int u)
{
// tag == -1, 代表没改过, 不用管
if (tr[u].tag == -1) return;
// 否则把 tag 传下去
pushup(u << 1, tr[u].tag);
pushup(u << 1 | 1, tr[u].tag);
// 当前就没改过了
tr[u].tag = -1;
}
//将一个区间里的每个值修改为 c
void modify(int u, int l, int r, int c)
{
// 如果在范围内, 就直接算一下当前这段的和为 c * 值个数
if (tr[u].l >= l && tr[u].r <= r)
{
pushup(u, c);
}
else {
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) modify(u << 1, l, r, c);
if (r >= mid + 1) modify(u << 1 | 1, l, r, c);
tr[u].s = tr[u << 1].s + tr[u << 1 | 1].s;
}
}
LL query(int u, int l, int r)
{
// 如果查询的区间就包裹了当前的node, 那么直接返回即可
if (tr[u].l >= l && tr[u].r <= r)
return tr[u].s;
// 先 pushdown 下一层
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
// 如果查询的只有左半部分
if (r <= mid) return query(u << 1, l, r);
// 如果查询的只有右半部分
if (l >= mid + 1) return query(u << 1 | 1, l, r);
// 查询的不单独在一侧
return query(u << 1, l, r) + query(u << 1 | 1, l, r);
}
void solve() {
int n, m, k;
cin >> n >> k >> m;
// 读入a[i] - i
for (int i = 1; i <= n; i++) {
cin >> a[i];
a[i] -= i;
}
map<int, int> mp;
multiset<int> st;
// 插入 n 个 0 以便后续 erase
for (int i = 0; i < n; i++) {
st.insert(0);
}
for (int i = 1; i <= n; i++) {
// 删掉原先当前这个数有几个
st.erase(st.find(mp[a[i]]));
// 插入现在这个数有几个
st.insert(++mp[a[i]]);
// 如果够 k 个, 就要删掉窗口左面的, 维持窗口的数量为 k 个
if (i - k > 0) {
st.erase(st.find(mp[a[i - k]]));
st.insert(--mp[a[i - k]]);
}
// 如果 i >= k, 这时候就知道 k 个数里最少改几个数能行了
if (i >= k) f[i - k + 1] = k - *st.rbegin();
}
// 读入要查询的数
for (int i = 1; i <= m; i++) {
cin >> l[i] >> r[i];
// 离散化
p[i] = i;
}
// 将 p 数组按照查询的左区间从大到小去排序
sort(p + 1, p + m + 1, [&](int i, int j) {
return l[i] > l[j];
});
// 创建线段树
build(1, 1, n - k + 1);
// tp = 0, 作为记录栈顶, 这里手动模拟了一个栈
int tp = 0;
// 栈一开始存放 n - k + 2
stk[ ++ tp] = n - k + 2;
// 栈对应的位置存一个 -1
f[n - k + 2] = -1;
// 一共 m 次询问
for (int i = 1, j = n - k + 1; i <= m; i++)
{
// 当 j 大于等于 查询的左区间
while (j >= l[p[i]])
{
// 拿最小的答案
while (f[stk[tp]] > f[j])
{
tp--;
}
// 用f[j]修改一片区域的值, 因此需要懒标记
modify(1, j, stk[tp] - 1, f[j]);
// 栈中存入 j, j --
stk[ ++ tp] = j --;
}
// 存答案, 由于离散化, 将答案存到了对应的位置上去, query 是线段树的查询, 这里查询的正如题意, 求取总和
ans[p[i]] = query(1, l[p[i]], r[p[i]] - k + 1);
}
for (int i = 1; i <= m; i++) {
cout << ans[i] << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}