平衡树

平衡树

  • 定义

    全称为二叉平衡搜索树。

  • 二叉搜索树

    形式化的,这个概念是指一棵节点带权的二叉树,令节点 \(i\) 的权值为 \(w_i\),左子树点集为 \(L\),右子树点集为 \(R\),其满足 \(\forall u \in L,w_u \le w_i\)\(\forall v \in R,w_v > w_i\)

  • 性质 / 作用

    • 具备单调性,可实现 \(\log\) 级别复杂度的查找。

    • 中序遍历单调不减,于是可以与一个序列形成映射(将下标作为权值即可)。

  • 平衡的意义

    众所周知,一个序列对应的二叉搜索树不一定惟一。我们希望取得高度更小的那一棵,以保证复杂度。

    若一棵二叉树的任意节点 \(i\),其左右子树高度差 \(\Delta h \le 1\),则我们称其为一棵二叉平衡树

  • 常用的平衡树

    treap、fhq-treap、splay。

FHQ-Treap

  • 定义

    又称无旋 Treap。即仅靠分裂与合并完成维护。

  • \(\operatorname{split}(k,a,b,val)\)

    顾名思义,即将根节点为 \(k\) 的 tree 分裂成根节点为 \(a,b\) 的两棵 tree,点集为 \(A,B\),其中 \(\forall u \in A, w_u \le val\)\(\forall v \in B, w_v > val\)

    大体的思路就是 \(k\) 为当前节点,若 \(w_k \le val\),说明找到了 \(a\),并且其左子树也都在 \(A\) 中,然后去右子树中寻找 \(b\)\(A\) 所缺的右子树。找到了 \(b\) 则反之处理即可。

  • \(\operatorname{merge}(k,a,b)\)

    顾名思义,即将根节点为 \(a,b\) 的 tree 合并为根节点为 \(k\) 的 tree,点集为 \(A,B\),其中 \(\forall u \in A,v \in B,w_u \le w_v\)

    根据定义,肯定是 \(A\)\(B\) 右了,但是高度无法确定。

    于是,我们给每个节点随机一个 \(rank\),谁 \(rank\) 小谁就在高处,这样退化成一条链的几率很小,可以保证均摊复杂度 \(\log\) 级别。

    确定了高度,譬如 \(A\) 在上,那么 \(B\) 就需要和 \(A\) 的右子树继续竞争,递归下去即可。

这两个函数是 FHQ-Treap 的精髓之所在。

其他的因题而异,请自行阅读代码进行理解。本代码只给出函数部分。

实现
void upd(int k){
	tree[k].siz=tree[tree[k].lt].siz+tree[tree[k].rt].siz+1;
}
int add_node(int val){
	tree[++tot].siz=1;
	tree[tot].val=val;
	tree[tot].rnk=rand();
	tree[tot].lt=tree[tot].rt=0;
	return tot;
}
void split(int k,int &a,int &b,int val){
	if(!k){
		a=b=0;
		return;
	}
	if(tree[k].val<=val){
		a=k;
		split(tree[k].rt,tree[k].rt,b,val);
	}
	else{
		b=k;
		split(tree[k].lt,a,tree[k].lt,val);
	}
	upd(k);
}
void merge(int &k,int a,int b){
	if(!a||!b){
		k=a+b;
		return;
	}
	if(tree[a].rnk<tree[b].rnk){
		k=a;
		merge(tree[k].rt,tree[k].rt,b);
	}
	else{
		k=b;
		merge(tree[k].lt,a,tree[k].lt);
	}
	upd(k);
}
void ins(int &k,int val){
	int a=0,b=0,cur=add_node(val);
	split(k,a,b,val);
	merge(a,a,cur);
	merge(k,a,b);
}
void del(int &k,int val){
	int a=0,b=0,c=0;
	split(k,a,b,val);
	split(a,a,c,val-1);
	merge(c,tree[c].lt,tree[c].rt);
	merge(a,a,c);
	merge(k,a,b);
}
int fnd_rnk(int &k,int val){
	int a=0,b=0;
	split(k,a,b,val-1);
	int res=tree[a].siz+1;
	merge(k,a,b);
	return res;
}
int fnd_num(int k,int x){
	while(tree[tree[k].lt].siz+1!=x){
		if(tree[tree[k].lt].siz>=x)
			k=tree[k].lt;
		else{
			x-=tree[tree[k].lt].siz+1;
			k=tree[k].rt;
		}
	}
	return tree[k].val;
}
int pre(int &k,int val){
	int a=0,b=0;
	split(k,a,b,val-1);
	int res=fnd_num(a,tree[a].siz);
	merge(k,a,b);
	return res;
}
int suf(int &k,int val){
	int a=0,b=0;
	split(k,a,b,val);
	int res=fnd_num(b,1);
	merge(k,a,b);
	return res;
}

应用

P1486(整体修改、第 \(K\) 大)

看到如上的关键词,我们考虑用一棵平衡树维护工资档案。

具体的:

  • I k:新建并插入一个节点。

  • A k:维护一个 \(tag\) 表示整体修改的值,令 \(tag+k \to tag\)

  • S k:令 \(tag-k \to tag\),并删除权值 \(< min-tag\)注意不是 \(<min\))的所有节点。

  • F k:查询第 \(k\) 大即可。

实现
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=1e5+5;
int n,ans,root,tot,mini,tag,all;
struct fhq{
	int val,rnk,siz,lt,rt;
}tree[N];

int add_node(int val){
	tree[++tot].val=val,all++;
	tree[tot].siz=1;
	tree[tot].rnk=rand();
	tree[tot].lt=tree[tot].rt=0;
	return tot;
}
void upd(int k){
	tree[k].siz=tree[tree[k].lt].siz+tree[tree[k].rt].siz+1;
}
void split(int k,int &a,int &b,int val){
	if(!k){
		a=b=0;
		return;
	}
	if(tree[k].val<=val){
		a=k;
		split(tree[k].rt,tree[k].rt,b,val);
	}
	else {
		b=k;
		split(tree[k].lt,a,tree[k].lt,val);
	}
	upd(k);
}
void merge(int &k,int a,int b){
	if(!a||!b){
		k=a+b;
		return;
	}
	if(tree[a].rnk<tree[b].rnk){
		k=a;
		merge(tree[k].rt,tree[k].rt,b);
	}
	else {
		k=b;
		merge(tree[k].lt,a,tree[k].lt);
	}
	upd(k);
}
void ins(int &k,int val){
	int a=0,b=0,cur=add_node(val);
	split(k,a,b,val);
	merge(a,a,cur);
	merge(k,a,b);
}
void del(int &k){
	int a=0,b=0;
	split(k,a,b,mini-tag-1);
	ans+=tree[a].siz;
	all-=tree[a].siz;
	k=b;
}
int fnd_num(int k,int x){
	while(tree[tree[k].rt].siz+1!=x){
		if(tree[tree[k].rt].siz>=x)
			k=tree[k].rt;
		else{
			x-=tree[tree[k].rt].siz+1;
			k=tree[k].lt;
		}
	}
	return tree[k].val;
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	srand(time(0));
	cin>>n>>mini;
	add_node(-1e18),add_node(1e18);
	all=0;
	while(n--){
		char x; int k;
		cin>>x>>k;
		if(x=='I'&&k>=mini)
			ins(root,k-tag);
		else if(x=='A')
			tag+=k;
		else if(x=='S')
			tag-=k,del(root);
		else if(x=='F'){
			if(all<k)
				cout<<"-1\n";
			else
				cout<<fnd_num(root,k)+tag<<'\n';
		}
	}
	cout<<ans;
	return 0;
} 

总结:

  • \(K\) 大考虑平衡树。

  • 整体修改考虑给整棵树打标记。

P2234(前驱、后继)

我们仍然考虑使用一棵平衡树维护营业额。

对于每天的营业额,我们顺次插入平衡树,然后在其前驱和后继这两个之间找差最小的即可。

需要注意的是:

  • 如果前面出现过相同的营业额,绝对差直接为 \(0\)

  • 应当设置哨兵节点以防找不到前驱 / 后继。

实现

总结:

  • 差最小考虑平衡树维护前驱 / 后继。

P3224(与 DSU 的结合)

还是查询第 \(K\) 小,考虑平衡树维护。

很容易想到对于每个联通块建一棵平衡树,然后用并查集维护联通性(合并)的时候顺便把树也合并了。

但是,我们的 merge 函数只能接受满足 \(\forall u \in A,v \in B,u<v\) 的点集为 \(A,B\) 的两棵树进行合并,可联通块并不具有此种性质。

于是启发式合并,将较小的树的点一个一个地扔进较大的树中即可,时间复杂度是 \(\log\) 级别的(一个点至多会被合并 \(\log n\) 次,因为每次合并都会使大小翻倍)。

实现
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
#define int long long
using namespace std;

const int N=2e5+5;
int n,m,q;
int root[N],fa[N],tot;
struct fhq{
	int val,rnk,siz,lt,rt,id;
}tree[N<<2];

int fnd(int x){
	return (fa[x]==x?x:fa[x]=fnd(fa[x]));
}
int add_node(int val,int id){
	tree[++tot].val=val;
	tree[tot].siz=1;
	tree[tot].rnk=rand();
	tree[tot].lt=tree[tot].rt=0;
	tree[tot].id=id;
	return tot;
}
void upd(int k){
	tree[k].siz=tree[tree[k].lt].siz+tree[tree[k].rt].siz+1;
}
void split(int k,int &a,int &b,int val){
	if(!k){
		a=b=0;
		return;
	}
	if(tree[k].val<=val){
		a=k;
		split(tree[k].rt,tree[k].rt,b,val);
	}
	else {
		b=k;
		split(tree[k].lt,a,tree[k].lt,val);
	}
	upd(k);
}
void merge(int &k,int a,int b){
	if(!a||!b){
		k=a+b;
		return;
	}
	if(tree[a].rnk<tree[b].rnk){
		k=a;
		merge(tree[k].rt,tree[k].rt,b);
	}
	else {
		k=b;
		merge(tree[k].lt,a,tree[k].lt);
	}
	upd(k);
}
void ins(int &k,int val,int id){
	int a=0,b=0,cur=add_node(val,id);
	split(k,a,b,val);
	merge(a,a,cur);
	merge(k,a,b);
}
void cls(int x){
	tree[x].val=tree[x].rnk=tree[x].siz=tree[x].lt=tree[x].rt=tree[x].id=0;
}
void mrg(int x,int y){
	x=fnd(x),y=fnd(y);
	if(x==y)
		return;
	if(tree[root[y]].siz>tree[root[x]].siz)
		swap(x,y);
	fa[y]=x;
	while(1){
		ins(root[x],tree[root[y]].val,tree[root[y]].id);
		cls(root[y]);
		merge(root[y],tree[root[y]].lt,tree[root[y]].rt);
		if(!root[y]){
			root[y]=root[x];
			break;
		}
	}
}
int fnd_num(int k,int x){
	while(tree[tree[k].lt].siz+1!=x){
		if(tree[tree[k].lt].siz>=x)
			k=tree[k].lt;
		else{
			x-=tree[tree[k].lt].siz+1;
			k=tree[k].rt;
		}
	}
	return tree[k].id;
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	srand(time(0));
	cin>>n>>m;
	for(int i=1,x;i<=n;i++)
		cin>>x,root[i]=i,fa[i]=i,add_node(x,i);
	for(int i=1,u,v;i<=m;i++)
		cin>>u>>v,mrg(u,v);
	cin>>q;
	while(q--){
		char opt; int x,y;
		cin>>opt>>x>>y;
		if(opt=='Q')
			cout<<(tree[root[fnd(x)]].siz<y?-1:fnd_num(root[fnd(x)],y))<<'\n';
		else
			mrg(x,y);
	}
	return 0;
} 

总结:

  • merge 不行,启发式合并来凑。

P3391(区间翻转问题、按 size 分裂)

首先,我们考虑将序列映射到这棵平衡树的中序遍历上。

这样,按照 \([1,r] \to [l,r]\) 的方式分裂两次即可得到 \([l,r]\) 区间。

然后翻转这东西,它就像异或一样,做两次相当于没做。

于是我们考虑对每个节点打标记,于是每次反转操作只需要分出区间再更新标记(别忘了把当前节点的左右子树反过来)即可。

哦对了,最后输出的时候可以一边中序遍历、一边继续下传标记(中间不一定下传完了)。

关于实现,分裂的时候需要写一个按 size 分裂的 fhq-treap,具体看代码8。

实现
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
#define int long long
using namespace std;

const int N=1e5+5;
int n,m,root,tot;
struct fhq{
	int val,rnk,siz,lt,rt,tag;
}tree[N];

int add_node(int val){
	tree[++tot].val=val;
	tree[tot].siz=1;
	tree[tot].rnk=rand();
	tree[tot].lt=tree[tot].rt=0;
	return tot;
}
void pushup(int k){
	tree[k].siz=tree[tree[k].lt].siz+tree[tree[k].rt].siz+1;
}
void pushdown(int k){
	if(!tree[k].tag)
		return;
	swap(tree[k].lt,tree[k].rt);
	tree[tree[k].lt].tag^=1;
	tree[tree[k].rt].tag^=1;
	tree[k].tag=0;
}
void split(int k,int &a,int &b,int x){
	if(!k){
		a=b=0;
		return;
	}
	pushdown(k);
	if(tree[tree[k].lt].siz+1>x){
		b=k;
		split(tree[k].lt,a,tree[k].lt,x);
	}
	else {
		a=k;
		split(tree[k].rt,tree[k].rt,b,x-tree[tree[k].lt].siz-1);
	}
	pushup(k);
}
void merge(int &k,int a,int b){
	if(!a||!b){
		k=a+b;
		return;
	}
	pushdown(a);
	pushdown(b);
	if(tree[a].rnk<tree[b].rnk){
		k=a;
		merge(tree[k].rt,tree[k].rt,b);
	}
	else {
		k=b;
		merge(tree[k].lt,a,tree[k].lt);
	}
	pushup(k);
}
void ins(int &k,int val){
	int a=0,b=0,cur=add_node(val);
	split(k,a,b,val);
	merge(a,a,cur);
	merge(k,a,b);
}
void qry(int &k,int l,int r){
	int a=0,b=0,c=0;
	split(k,a,b,r);
	split(a,a,c,l-1);
	tree[c].tag^=1;
	merge(a,a,c);
	merge(k,a,b);
}
void dfs(int k){
	if(!k)
		return;
	pushdown(k);
	dfs(tree[k].lt);
	cout<<k<<' ';
	dfs(tree[k].rt);
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	srand(time(0));
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		ins(root,i);
	for(int i=1,l,r;i<=m;i++){
		cin>>l>>r;
		qry(root,l,r);
	}
	dfs(root);
	return 0;
} 

总结:

  • 序列问题考虑映射到平衡树的中序遍历上,以及使用按 size 分裂的。

  • 数据结构注重打标记(对每个节点打标记最常见),然后注意下传。

P5217(按 size 分裂查排名、状压思想)

全家桶了属于是。

插入、删除、翻转、查排名对应的字母都不讲。

P 操作就是查字母对应的排名,具体而言就是从 \(x\) 开始,若它是右儿子,那么不停地往上跳,然后累加左子树的节点数,很容易理解吧。特别注意翻转标记必须先下传完再处理这个操作,不然会导致「当前文本」不是真正的当前文本。

Q 操作这种种类数的询问显然考虑状压维护,每次 pushup 时或一下左右子树的二进制值再传给父亲即可。

实现
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
//#define int long long
using namespace std;

const int N=2e5+5;
int n,m,tot,root;
string str;
struct fhq{
	int tag,val,rnk,lt,rt,siz,word,fa;
}tree[N];
void pushup(int k){
	if(tree[k].lt)
		tree[tree[k].lt].fa=k;
	if(tree[k].rt)
		tree[tree[k].rt].fa=k;
	tree[k].word=tree[tree[k].lt].word|tree[tree[k].rt].word|1<<tree[k].val;
	tree[k].siz=tree[tree[k].lt].siz+tree[tree[k].rt].siz+1;
}
void pushdown(int k){
	if(!tree[k].tag)
		return;
	swap(tree[k].lt,tree[k].rt);
	tree[tree[k].lt].tag^=1;
	tree[tree[k].rt].tag^=1;
	tree[k].tag=0;
}
int add_node(int val){
	tree[++tot].siz=1;
	tree[tot].lt=tree[tot].rt=0;
	tree[tot].word=0;
	tree[tot].val=val;
	tree[tot].rnk=rand();
	tree[tot].tag=0;
	pushup(tot);
	return tot;
}
void split(int k,int &a,int &b,int x){
	if(!k){
		a=b=0;
		return;
	}
	pushdown(k);
	if(tree[tree[k].lt].siz+1>x){
		b=k;
		split(tree[k].lt,a,tree[k].lt,x);
		tree[a].fa=0;
	}
	else{
		a=k;
		split(tree[k].rt,tree[k].rt,b,x-tree[tree[k].lt].siz-1);
		tree[b].fa=0;
	}
	pushup(k);
}
void merge(int &k,int a,int b){
	if(!a||!b){
		k=a+b;
		return;
	}
	pushdown(a),pushdown(b);
	if(tree[a].rnk<tree[b].rnk){
		k=a;
		merge(tree[k].rt,tree[k].rt,b);
	}
	else{
		k=b;
		merge(tree[k].lt,a,tree[k].lt);
	}
	pushup(k);
}
void dddd(int k){
	if(tree[k].fa)
		dddd(tree[k].fa);
	pushdown(k);
}
void dfs(int k){
	if(!k)
		return;
	pushdown(k);
	dfs(tree[k].lt);
	cout<<(char)(tree[k].val+'a');
	dfs(tree[k].rt); 
}


signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	srand(time(0));
	cin>>n>>m>>str;
	str='#'+str;
	for(int i=1;i<=n;i++)
		merge(root,root,add_node(str[i]-'a'));
	while(m--){
		char op,ch;
		int x,y,a=0,b=0,c=0;
		cin>>op>>x;
		if(op=='I'){
			cin>>ch;
			split(root,a,b,x);
			merge(a,a,add_node(ch-'a'));
			merge(root,a,b);
		}
		else if(op=='D'){
			split(root,a,b,x);
			split(a,a,c,x-1);
			tree[c].val=-1;
			merge(root,a,b);
		}
		else if(op=='R'){
			cin>>y;
			split(root,a,b,y);
			split(a,a,c,x-1);
			tree[c].tag^=1;
			merge(a,a,c);
			merge(root,a,b);
		}
		else if(op=='P'){
			if(tree[x].val==-1){
				cout<<"0\n";
				continue;
			}
			dddd(x);
			int res=tree[tree[x].lt].siz+1;
			for(int i=x;tree[i].fa;i=tree[i].fa)
				if(tree[tree[i].fa].rt==i)
					res+=tree[tree[tree[i].fa].lt].siz+1;
			cout<<res<<'\n';
		}
		else if(op=='T'){
			split(root,a,b,x);
			split(a,a,c,x-1);
			cout<<(char)(tree[c].val+'a')<<'\n';
			merge(a,a,c);
			merge(root,a,b);
		}
		else if(op=='Q'){
			cin>>y;
			split(root,a,b,y);
			split(a,a,c,x-1);
			cout<<__builtin_popcount(tree[c].word)<<'\n';
			merge(a,a,c);
			merge(root,a,b);
		}
		//dfs(root);
		//cout<<'\n';
	}
	return 0;
}

总结:

  • 积累了按 size 分裂的 fhq_treap 如何查排名。

  • 种类数的维护考虑状压。

P3765(摩尔投票、随机化)

积累一个摩尔投票(求区间内绝对众数)的 trick:

在一个区间内,首先令第一个元素为答案;维护一个 \(cnt\),之后每遇到一个与答案相同的,\(cnt+1 \to cnt\),否则 \(cnt-1 \to cnt\)。若 \(cnt=0\),则令当前这个为答案,继续重复上述步骤,最后的答案就有可能为绝对众数。正确性显然。

很容易发现摩尔投票具有区间可加性,我们就可以使用线段树维护绝对众数。具体而言,若两个区间众数相同,则合起来众数不变、\(cnt\) 相加;否则众数为较大的那个、\(cnt\) 为绝对差。这样在保证存在绝对众数的情况下,可以做到 \(O(1)\) 回答询问。

问题是现在无法保证存在绝对众数,于是我们需要进行验证,也就是验证求出的众数是否真的有 \(r-l+1\) 个人投他。考虑使用平衡树维护之,将每个人插入他投的那个人的平衡树中,于是问题等价于询问排名 \(l \sim r\) 中是否有 \(r-l+1\) 个人,所以我们需要 \(n\) 棵按 val 分裂的平衡树(方便查找排名)。

然后这个题就做完了,但是巨大难写,怎么办?

考虑乱搞。具体的,我们完全可以不需要线段树维护区间绝对众数,我们直接随机一个,再用平衡树检查。只随机一次的错误概率为 \(\frac{1}{2}\),这样重复 \(15 \sim 20\) 次,错误概率最低仅为 \(\frac{1}{2^{20}}\),几乎可以忽略不计。

实现
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
//#define int long long
using namespace std;

inline int read()
{
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9')
    {
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(ch>='0' && ch<='9')
        x=x*10+ch-'0',ch=getchar();
    return x*f;
}

void write(int x)
{
    if(x<0)
        putchar('-'),x=-x;
    if(x>9)
        write(x/10);
    putchar(x%10+'0');
    return;
}

const int N=5e6+5;
int n,m,root,tot;
int a[N];
bool vis[N];
struct FHQ{
	int val,rnk,siz,lt,rt;
}tree[N];
struct fhq{
	int root=0;
	int add_node(int val){
		tree[++tot].val=val;
		tree[tot].siz=1;
		tree[tot].rnk=rand();
		tree[tot].lt=tree[tot].rt=0;
		return tot;
	}
	void upd(int k){
		tree[k].siz=tree[tree[k].lt].siz+tree[tree[k].rt].siz+1;
	}
	void split(int k,int &a,int &b,int val){
		if(!k){
			a=b=0;
			return;
		}
		if(tree[k].val<=val){
			a=k;
			split(tree[k].rt,tree[k].rt,b,val);
		}
		else {
			b=k;
			split(tree[k].lt,a,tree[k].lt,val);
		}
		upd(k);
	}
	void merge(int &k,int a,int b){
		if(!a||!b){
			k=a+b;
			return;
		}
		if(tree[a].rnk<tree[b].rnk){
			k=a;
			merge(tree[k].rt,tree[k].rt,b);
		}
		else {
			k=b;
			merge(tree[k].lt,a,tree[k].lt);
		}
		upd(k);
	}
	void ins(int val){
		int a=0,b=0,cur=add_node(val);
		split(root,a,b,val);
		merge(a,a,cur);
		merge(root,a,b);
	}
	void del(int val){
		int a=0,b=0,c=0;
		split(root,a,b,val);
		split(a,a,c,val-1);
		merge(c,tree[c].lt,tree[c].rt);
		merge(a,a,c);
		merge(root,a,b);
	}
	int fnd_rnk(int val){
		int a=0,b=0;
		split(root,a,b,val);
		int res=tree[a].siz;
		merge(root,a,b);
		return res;
	}
}tr[N];

int qry(int l,int r){
	int res=-1,stk[30]={0},top=0;
	for(int i=1;i<=15;i++){
		int seed=a[rand()%(r-l+1)+l];
		if(vis[seed])
			continue;
		vis[seed]=1,stk[++top]=seed;
		if(tr[seed].fnd_rnk(r)-tr[seed].fnd_rnk(l-1)>(r-l+1)/2){
			res=seed;
			break;
		}
	}
	while(top)
		vis[stk[top]]=0,top--;
	return res;
}

signed main(){
	srand(time(0));
	n=read(),m=read();
	for(int i=1;i<=n;i++)
		a[i]=read(),tr[a[i]].ins(i);
	for(int i=1,l,r,s,k;i<=m;i++){
		l=read(),r=read(),s=read(),k=read();
		int winner=qry(l,r);
		if(winner==-1)
			winner=s;
		for(int j=1,x;j<=k;j++)
			x=read(),tr[a[x]].del(x),a[x]=winner,tr[winner].ins(x);
		write(winner),putchar('\n');
	}
	write(qry(1,n));
	return 0;
} 

总结:

  • 区间绝对众数:摩尔投票 / 随机化。

  • 注意平衡树的使用是按 size(一般用于序列问题) 还是 val(一般用于查排名、前驱、后继等) 分裂。

posted @ 2025-03-09 09:44  _KidA  阅读(59)  评论(0)    收藏  举报