[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)\)。