[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 维护,问题在于如何找答案。

考虑已知左右儿子的信息,如何算出当前点的信息。

有四种情况:

  1. 三元上升子序列的三个元素都在同一个儿子中。
  2. 一个在左儿子,一个在自己,一个在右儿子。
  3. 两个在左儿子,一个在自己或右儿子。
  4. 一个在自己或左儿子,两个在右儿子。

第一种情况直接继承左右儿子的答案。

第二种情况为尽量满足条件,一定选左儿子最小值和右儿子最大值进行统计。

第三种情况为尽量满足条件,一定选择左儿子中结尾最小的二元上升子序列进行统计。

第四种情况为尽量满足条件,一定选择右儿子中开头最大的二元上升子序列进行统计。

综上 ,需要维护的信息如下:

  1. 最小值,最大值
  2. 结尾最小的二元上升子序列
  3. 开头最大的二元上升子序列
  4. 答案

最小值最大值的维护平凡,答案的维护按照上面的讨论判断即可。

重点在于如何维护 2 和 3。

首先继承左右儿子信息,然后呢?

为了查询结尾最小的二元上升子序列,开头一定要最小,

所以要查询右儿子内左儿子最小值的后继,

同理查询开头最大的二元上升子序列,结尾一定要最大,

所以要查询左儿子内右儿子最大值的前驱。

但是这里的 FHQ-Treap 维护的是排名信息,不支持前驱后继的查询,

这里就需要一个事实:

由于只查询全局信息,如果当前点答案已经为 YES,直接不维护即可,

若答案为 NO,有:

  1. 左儿子内小于右儿子最大值的数单调递减,
  2. 右儿子内大于左儿子最小值的数单调递减,

用反证法证明 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)\)

posted @ 2024-11-12 21:46  maniubi  阅读(15)  评论(0编辑  收藏  举报