启发式合并
定义
在合并两个集合时,每次合并时都将 集合内元素数量较少者合并至较多者,称这种合并方式为启发式合并。
如果一次合并的方法是将某个集合内的每个元素依次加入另一个集合,则将总大小为 \(n\) 的集合按照上述方式合并为一个集合时,在某个集合内加入某个元素的操作次数为 \(O(n\log n)\) 的。对于某个元素,其在改变所属的集合时,改变前后集合的大小至少上升了一倍,故每个元素对时间复杂度的贡献不超过 \(O(\log n)\)。
普通用法例题:P3224 永无乡
解法
考虑使用平衡树维护每个连通块内点权以方便查询第 \(k\) 大点权,在查询时直接把对应的平衡树启发式合并即可。时间复杂度为 \(O(n\log^2 n)\)。
当然可以权值线段树合并。
代码
点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
int n,m,i,a,b;
char ch;
struct node{
int val,ls,rs,siz,fa;
unsigned key;
}tr[maxn];
#define val(p) tr[p].val
#define ls(p) tr[p].ls
#define rs(p) tr[p].rs
#define siz(p) tr[p].siz
#define fa(p) tr[p].fa
#define key(p) tr[p].key
int Find(int p){
while(fa(p)) p=fa(p);
return p;
}
random_device rd;
mt19937 Rand(rd());
void Pushup(int p){
if(ls(p)) fa(ls(p))=p;
if(rs(p)) fa(rs(p))=p;
siz(p)=siz(ls(p))+siz(rs(p))+1;
}
void vSplit(int p,int v,int &x,int &y){
if(!p){
x=y=0;
return;
}
if(val(p)<=v){
x=p;
vSplit(rs(p),v,rs(p),y);
}
else{
y=p;
vSplit(ls(p),v,x,ls(p));
}
Pushup(p);
}
int Merge(int x,int y){
if(!(x&&y)) return x|y;
if(key(x)<key(y)){
rs(x)=Merge(rs(x),y);
Pushup(x);return x;
}
else{
ls(y)=Merge(x,ls(y));
Pushup(y);return y;
}
}
void Insert(int p,int rt){
int x,y;
vSplit(rt,val(p),x,y);
Merge(Merge(x,p),y);
}
void Auto(int x,int y){
if(ls(y)) Auto(x,ls(y));
const int rt=rs(y);
fa(y)=ls(y)=rs(y)=0;
siz(y)=1;Insert(y,x);
if(rt) Auto(x,rt);
}
void MergeT(int x,int y){
x=Find(x);y=Find(y);
if(x==y) return;
if(siz(x)<siz(y)) Auto(y,x);
else Auto(x,y);
}
int main(){
scanf("%d%d",&n,&m);
for(i=1;i<=n;++i){
scanf("%d",&val(i));
key(i)=Rand();siz(i)=1;
}
while(m--){
scanf("%d%d",&a,&b);
MergeT(a,b);
}
scanf("%d",&m);
while(m--){
scanf(" %c%d%d",&ch,&a,&b);
if(ch=='B') MergeT(a,b);
else{
a=Find(a);
if(b>siz(a)) a=-1;
else for(;;){
if(siz(ls(a))>=b) a=ls(a);
else{
b-=siz(ls(a));
if(b==1) break;
else{a=rs(a);--b;}
}
}
printf("%d\n",a);
}
}
return 0;
}
用途
在并查集上使用启发式合并可以在 保证时间复杂度 的同时保证每次合并只需要一次修改。这使得这种并查集可持久化、可撤销。
在树上统计子树总信息时,也可以通过 树上启发式合并,把轻儿子的信息合并到重儿子上,优化时间复杂度。
练习题:P4755 Beautiful Pair(启发式分裂/分治)
给定一个长为 \(n\) 的序列 \(a\),求多少个子区间满足两端乘积不大于最大值。\(n\le 10^5,a_i\le 10^9\)。
解法
考虑某个数 \(a_i\) 成为某个区间 \([l,r]\) 最大值时 \(l,r\) 均需要在某个区间内,设这个区间为 \(l\) 需要在 \([L_i,i]\) 内,\(r\) 需要在 \([i,R_i]\) 内。
问题变成了对于每个对于 \([L_i,i]\) 内的每个数 \(u\) 求 \([i,R_i]\) 中有多少个数 \(v\) 满足 \(uv>a_i\)。在主席树上二分统计 \(u,v\) 加特判 \(v=a_i\) 的情况。然后在处理 \(a_{L_i-1}\) 或 \(a_{R_i+1}\) 时,\([L_i,R_i]\) 会成为新的最大值 \(a_j\) 对应的 \([L_j,j]\) 或 \([j,R_j]\) 的子区间。实现时可以递归向下统计,使用 ST 表维护当前区间最大值的位置。
计算 \(a_i\) 作为最大值时对答案的贡献时可以使用启发式合并的思想,枚举更小的区间的数在更大的区间内对答案造成的贡献,则整体的时间复杂度变为 \(O(n\log n\log 值域)\)。
代码
点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxl=17;
const int maxd=31;
const int maxn=100010;
int n,i,j,b,lt,rt,mt,lc,rc,lp,rp,p1,p2;
int a[maxn],lb[maxn],dt[maxn],st[maxl][maxn];
struct seg{int ls,rs,cnt;}tr[maxn*maxd];
long long ans;
#define ls(p) tr[p].ls
#define rs(p) tr[p].rs
#define cnt(p) tr[p].cnt
void calc(int l,int r){
if(l>r) return;
if(l==r){
ans+=(a[l]==1);
return;
}
int m;
b=lb[r-l+1]; lt=st[b][l];
m=rt=st[b][r-(1<<b)+1];
if(a[lt]>a[rt]) m=lt;
calc(l,m-1); calc(m+1,r);
lc=l; rc=lp=m; rp=r;
if(m-l>r-m){
swap(lc,lp);
swap(rc,rp);
}
--lp;
for(i=lc;i<=rc;++i){
lt=1; rt=1e9;
b=a[m]/a[i];
p1=dt[lp]; p2=dt[rp];
while(lt<rt){
mt=(lt+rt)>>1;
if(mt<=b){
ans+=cnt(ls(p2))-
cnt(ls(p1));
lt=mt+1;
p1=rs(p1);
p2=rs(p2);
}
else{
rt=mt;
p1=ls(p1);
p2=ls(p2);
}
}
}
}
int main(){
for(i=2;i<maxn;++i) lb[i]=lb[i>>1]+1;
scanf("%d",&n);
for(i=1;i<=n;++i){
scanf("%d",a+i);
st[0][i]=i;
lt=1; rt=1e9;
p1=dt[i-1];
dt[i]=++p2;
tr[p2]=tr[p1];
++cnt(p2);
while(lt<rt){
mt=(lt+rt)>>1;
if(a[i]<=mt){
p1=ls(p1);
ls(p2)=p2+1;
rt=mt;
}
else{
p1=rs(p1);
rs(p2)=p2+1;
lt=mt+1;
}
tr[++p2]=tr[p1];
++cnt(p2);
}
}
for(j=1;j<maxl;++j){
b=n-(1<<j)+1;
for(i=1;i<=b;++i){
lt=st[j-1][i];
rt=st[j-1][i+(1<<(j-1))];
if(a[lt]<a[rt]) st[j][i]=rt;
else st[j][i]=lt;
}
}
calc(1,n);
printf("%lld",ans);
return 0;
}
本文来自博客园,作者:Fran-Cen,转载请注明原文链接:https://www.cnblogs.com/Fran-CENSORED-Cwoi/p/16777634.html