[NOIP 2024 模拟12]序列
[NOIP 2024 模拟12]序列
题意
给出长度为 \(n\) 的序列 \(a\),每次操作给出 \(l,r,k\),把 \([l,r]\) 进行 \(k\) 次循环位移。
每次操作结束后查询整个序列是否存在三元上升子序列,即是否存在 \(i<j<k,a_i<a_j<a_k\)。
思路
区间循环位移使用 FHQ-Treap 维护,问题在于如何找答案。
考虑已知左右儿子的信息,如何算出当前点的信息。
有四种情况:
- 三元上升子序列的三个元素都在同一个儿子中。
- 一个在左儿子,一个在自己,一个在右儿子。
- 两个在左儿子,一个在自己或右儿子。
- 一个在自己或左儿子,两个在右儿子。
第一种情况直接继承左右儿子的答案。
第二种情况为尽量满足条件,一定选左儿子最小值和右儿子最大值进行统计。
第三种情况为尽量满足条件,一定选择左儿子中结尾最小的二元上升子序列进行统计。
第四种情况为尽量满足条件,一定选择右儿子中开头最大的二元上升子序列进行统计。
综上 ,需要维护的信息如下:
- 最小值,最大值
- 结尾最小的二元上升子序列
- 开头最大的二元上升子序列
- 答案
最小值最大值的维护平凡,答案的维护按照上面的讨论判断即可。
重点在于如何维护 2 和 3。
首先继承左右儿子信息,然后呢?
为了查询结尾最小的二元上升子序列,开头一定要最小,
所以要查询右儿子内左儿子最小值的后继,
同理查询开头最大的二元上升子序列,结尾一定要最大,
所以要查询左儿子内右儿子最大值的前驱。
但是这里的 FHQ-Treap 维护的是排名信息,不支持前驱后继的查询,
这里就需要一个事实:
由于只查询全局信息,如果当前点答案已经为 YES,直接不维护即可,
若答案为 NO,有:
- 左儿子内小于右儿子最大值的数单调递减,
- 右儿子内大于左儿子最小值的数单调递减,
用反证法证明 1:
若不单调递减,必存在 \(i<j,a_i<a_j\),再根据 \(a_i<a_j<mx\),
得出存在三元上升子序列,与条件矛盾,所以结论成立。
2 的证明同理。
有了这两个结论,前驱后继转化为了:
左儿子中小于右儿子最大值且在最左边的数,
右儿子中大于左儿子最小值且在最右边的数。
可以用类似平衡树上二分的思路做。
不要忘了平衡树上自己可以贡献信息,
不能只统计左右儿子的贡献。
时间复杂度:\(O(n\log^2 n)\)。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
struct treap {
struct node {
int ls, rs;
int val, siz, key;
int mx, mn; // 最大最小值
int Mx, Mn; // 开头最大/结尾最小的二元上升子序列
bool ans;
node() {
mx = 0;
mn = INT_MAX;
}
} t[N];
int cnt, root;
int new_node(int val) {
++ cnt;
t[cnt].ls = 0, t[cnt].rs = 0;
t[cnt].val = val, t[cnt].siz = 1;
t[cnt].key = rand();
t[cnt].mx = t[cnt].mn = val;
t[cnt].Mx = t[cnt].Mn = -1;
t[cnt].ans = 0;
return cnt;
}
int findL(int p, int k) { // 平衡树上二分 查询最左侧小于 k 的数
if (t[p].mn >= k) return -1;
if (!t[p].ls && !t[p].rs) return t[p].val;
if (t[p].ls && t[t[p].ls].mn < k) return findL(t[p].ls, k);
if (t[p].val < k) return t[p].val;
if (t[p].rs && t[t[p].rs].mn < k) return findL(t[p].rs, k);
return -1;
}
int findR(int p, int k) { // 平衡树上二分 查询最右侧大于 k 的数
if (t[p].mx <= k) return -1;
if (!t[p].ls && !t[p].rs) return t[p].val;
if (t[p].rs && t[t[p].rs].mx > k) return findR(t[p].rs, k);
if (t[p].val > k) return t[p].val;
if (t[p].ls && t[t[p].ls].mx > k) return findR(t[p].ls, k);
return -1;
}
void push_up(int p) {
t[p].siz = t[t[p].ls].siz + t[t[p].rs].siz + 1;
t[p].mx = max({t[p].val, t[t[p].ls].mx, t[t[p].rs].mx}); // 最大最小值
t[p].mn = min({t[p].val, t[t[p].ls].mn, t[t[p].rs].mn});
t[p].ans = t[t[p].ls].ans || t[t[p].rs].ans; // 继承
t[p].Mx = t[p].Mn = -1;
if (t[p].ls && t[p].rs) { // 维护答案
if (~t[t[p].rs].Mx) t[p].ans |= (t[t[p].ls].mn < t[t[p].rs].Mx);
if (~t[t[p].ls].Mn) t[p].ans |= (t[t[p].ls].Mn < t[t[p].rs].mx);
}
if (t[p].ls && (~t[t[p].ls].Mn)) t[p].ans |= (t[t[p].ls].Mn < t[p].val); // 维护答案
if (t[p].rs && (~t[t[p].rs].Mx)) t[p].ans |= (t[p].val < t[t[p].rs].Mx); // 维护答案
if (t[p].ls && t[p].rs) t[p].ans |= (t[t[p].ls].mn < t[p].val && t[p].val < t[t[p].rs].mx); // 维护答案
if (t[p].ans) return ; // 答案为 YES 直接不维护
if (t[p].ls) t[p].Mx = findL(t[p].ls, t[p].val); // 自己的贡献
if (t[p].ls && t[p].rs) t[p].Mx = max(t[p].Mx, findL(t[p].ls, t[t[p].rs].mx)); // 右儿子的贡献
if (t[p].rs && t[p].val < t[t[p].rs].mx) t[p].Mx = max(t[p].Mx, t[p].val); // 自己的贡献
if (t[p].rs) t[p].Mn = findR(t[p].rs, t[p].val); // 自己的贡献
if (t[p].ls && t[p].rs) t[p].Mn = min((t[p].Mn != -1 ? t[p].Mn : (int)1e9), findR(t[p].rs, t[t[p].ls].mn)); // 左儿子的贡献
if (t[p].ls && t[t[p].ls].mn < t[p].val) t[p].Mn = min((t[p].Mn != -1 ? t[p].Mn : (int)1e9), t[p].val); // 自己的贡献
if (t[p].ls) t[p].Mx = max(t[p].Mx, t[t[p].ls].Mx); // 继承
if (t[p].rs) t[p].Mx = max(t[p].Mx, t[t[p].rs].Mx); // 继承
if (t[p].ls && t[t[p].ls].Mn != -1) t[p].Mn = min((t[p].Mn != -1 ? t[p].Mn : (int)1e9), t[t[p].ls].Mn); // 继承
if (t[p].rs && t[t[p].rs].Mn != -1) t[p].Mn = min((t[p].Mn != -1 ? t[p].Mn : (int)1e9), t[t[p].rs].Mn); // 继承
}
void split(int p, int k, int &x, int &y) {
if (!p) {
x = y = 0;
return ;
}
if (t[t[p].ls].siz + 1 <= k) {
x = p;
split(t[p].rs, k - t[t[p].ls].siz - 1, t[p].rs, y);
} else {
y = p;
split(t[p].ls, k, x, t[p].ls);
}
push_up(p);
}
int merge(int x, int y) {
if (!x || !y) return x + y;
if (t[x].key > t[y].key) {
t[x].rs = merge(t[x].rs, y);
push_up(x);
return x;
} else {
t[y].ls = merge(x, t[y].ls);
push_up(y);
return y;
}
}
void forMove(int l, int r, int k) { // 循环位移
int x, y, z;
split(root, r, y, z);
split(y, l - 1, x, y);
int a, b, len = r - l + 1;
split(y, len - k, a, b);
y = merge(b, a);
int temp = merge(x, y);
root = merge(temp, z);
}
void push_back(int val) {
root = merge(root, new_node(val));
}
bool query() {return t[root].ans;}
void display(int p) {
if (!p) return ;
display(t[p].ls);
cout << t[p].val << " ";
display(t[p].rs);
if (p == root) cout << "\n";
}
} T;
int n, q, a[N];
int main() {
freopen("xu.in", "r", stdin);
freopen("xu.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i ++) cin >> a[i];
for (int i = 1; i <= n; i ++)
T.push_back(a[i]);
cin >> q;
while (q --) {
int l, r, k;
cin >> l >> r >> k;
T.forMove(l, r, k);
if (T.query()) cout << "YES\n";
else cout << "NO\n";
}
return 0;
}
这道题带来的启发
有时得到想要的东西了就可以摆烂了。
维护信息不一定完全从左右儿子的信息来,
可以先通过左右儿子的信息得到一些信息,
根据这些信息推出性质,方便转移。
有时 push_up
不一定是 \(O(1)\)。
本文来自博客园,作者:maniubi,转载请注明原文链接:https://www.cnblogs.com/maniubi/p/18542707,orz