启发式合并

启发式合并

问题:将\(n\)个大小为1的集合合并成1个集合

方法:将小的集合合并到大的中

由于每次合并集合大小至少翻倍,故每个元素合并至多合并\(lg\)次,所以时间复杂度是\(O(nlgn)\)

  • 通常需要数组记录第\(i\)的元素所属的集合(因为要交换集合)
  • 树时往往要先考虑一条链

例题1

Luogu P3201 [HNOI2009] 梦幻布丁

颜色只会越合并越少,所以启发式合并,可以用链表维护

#include<bits/stdc++.h>
using namespace std;

//c表示i的颜色被标记为啥了 
//sz表示啥颜色的数量
//V表示啥颜色的位置 
const int N=1e6+5;
int ans,n,m,a[N],sz[N],c[N];
vector<int>V[N];

int main() {
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) {
		scanf("%d",&a[i]);
		ans+=(a[i]!=a[i-1]);
		V[a[i]].push_back(i),sz[a[i]]++;
	}
	for(int i=1;i<=1e6;i++) c[i]=i; 
	for(int i=1;i<=m;i++) {
		int op; scanf("%d",&op);
		if(op==1) {
			int x,y; scanf("%d%d",&x,&y);
			if(c[x]==c[y]) continue; 
			if(sz[c[x]]>sz[c[y]]) swap(c[x],c[y]);
			for(int j:V[c[x]]) {
				if(a[j-1]==c[y]) ans--;
				if(a[j+1]==c[y]) ans--;
			}
			for(int j:V[c[x]]) {
				a[j]=c[y];
				V[c[y]].push_back(j);
			}
			V[c[x]].clear();
		} else {
			printf("%d\n",ans);
		}
	}
	return 0;
}

例题2

Luogu P5290 [十二省联考2019]春节十二响

两条链时很明显暴力合并:最大值和最大值,次大值和次大值...

然后可以发现合并的结果也是一条链,且是最优的,具有最优子结构的性质

所以每次暴力合并子树的结果,启发式合并堆即可,时间复杂度\(O(nlg^2n)\)

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

const int N=2e5+4;
int n,a[N],id[N],sz[N],cnt; 
priority_queue<int>q[N],tmp;
vector<int>V[N];
void dfs(int u) {
	id[u]=++cnt;
	for(int v:V[u]) {
			dfs(v);
			if(sz[id[u]]<sz[id[v]]) swap(id[u],id[v]);
			while(!q[id[v]].empty()) {
				int t1=q[id[u]].top(),t2=q[id[v]].top();
				tmp.push(max(t1,t2));
				q[id[u]].pop(),q[id[v]].pop(),sz[id[u]]--;
			}
			for(;!tmp.empty();tmp.pop()) {
				q[id[u]].push(tmp.top()),sz[id[u]]++;
			}
	}
	q[id[u]].push(a[u]),sz[id[u]]++;
}
int main() {
	scanf("%d",&n);
	for(int i=1;i<=n;i++) {
		scanf("%d",&a[i]);
	}
	for(int i=2;i<=n;i++) {
		int u; scanf("%d",&u);
		V[u].push_back(i);
	}
	dfs(1);
	ll ans=0;
	for(;!q[id[1]].empty();q[id[1]].pop()) ans+=q[id[1]].top();
	printf("%lld\n",ans);
	return 0;
} 

例题3

ybtoj G. 大根堆

很妙的题目

当一条链时,求LIS,有两种方法

  1. 数据结构(线段树,树状数组)维护,每次单点插入,求区间最大值(在树上则需要启发式合并+区间修改),时间复杂度是\(O(nlg^2n)\)
  2. \(b[i]\)数组表示当答案为\(i\)时的最小最后的数,二分答案即可

重点想想第2种方法,可以发现,\(b\)有如下性质:

  1. \(b\)下标时连续的,且\(b\)下标最大为答案

  2. \(b\)数值是递增的(反证法)

  3. 当不同子树合并时,\(b\)也会一起合并成一个有序的数列,成为新的\(b\)

  4. 每次插入一个数相当于代替第一个大于等于它的数

3,4性质告诉我们可以用multiset维护,时间复杂度也是\(O(nlg^2n)\)

WA了一次:用了set,没用multiset

#include<bits/stdc++.h>
const int INF=1e9;
using namespace std;

const int N=2e5+5;
int n,f[N],id[N],a[N],sz[N],cnt;
vector<int>V[N];
multiset<int>s[N];
void dfs(int u) {
	id[u]=++cnt;
	for(int v:V[u]) {
		dfs(v);
		if(sz[id[u]]<sz[id[v]]) swap(id[u],id[v]);
		for(int t:s[id[v]]) {
			s[id[u]].insert(t);
			sz[id[u]]++;
		}
	}
	auto it=s[id[u]].lower_bound(a[u]);
	if(it!=s[id[u]].end()) {
		s[id[u]].erase(it); sz[id[u]]--; 
	}
	s[id[u]].insert(a[u]),sz[id[u]]++;
}
int main() {
	scanf("%d",&n);
	for(int i=1;i<=n;i++) {
		int u; scanf("%d%d",&a[i],&u);
		V[u].push_back(i);
	}
	dfs(1);
	printf("%d\n",sz[id[1]]);
	return 0;
}

例题4

Luogu P3302 [SDOI2013]森林

好一道SB题 还记得初三参加省选听学长的课听到懵逼后蜷缩在位置上写这道题的样子

求第K大显然只能主席树,每次合并启发式合并,时间空间复杂度均为\(O(nlg^2n)\)

WA了一次:以为连单向边就够了,应该连双向(重构)

#include<bits/stdc++.h>
using namespace std;

const int N=8e4+5,M=3e7+4;
int n,m,cnt,ls[M],rs[M],c[M],dep[N],lg[N],f[N][20],rt[N],sz[N],a[N];
vector<int>V[N];
char op[10];
void ins(int &p,int l,int r,int x) {
	c[++cnt]=c[p]+1,ls[cnt]=ls[p],rs[cnt]=rs[p];
	p=cnt;
	if(l==r) return;
	int mid=l+r>>1;
	if(x<=mid) ins(ls[p],l,mid,x);
		else ins(rs[p],mid+1,r,x);
}
void dfs(int fa,int u) {
	dep[u]=!fa?0:dep[fa]+1;
	f[u][0]=fa;
	for(int i=1;i<=lg[dep[u]];i++) {
		f[u][i]=f[f[u][i-1]][i-1];
	}
	rt[u]=rt[fa];  sz[u]=1;
	ins(rt[u],1,1e9,a[u]);
	for(int v:V[u]) {
		if(v!=fa) {
			dfs(u,v);
			sz[u]+=sz[v];
		}
	}
}
inline int lca(int x,int y) {
	if(dep[x]>dep[y]) swap(x,y);
	while(dep[x]<dep[y]) y=f[y][lg[dep[y]-dep[x]]];
	if(x==y) return x;
	for(int i=lg[dep[x]];i>=0;i--) {
		if(f[x][i]!=f[y][i]) {
			x=f[x][i],y=f[y][i];
		}
	}
	return f[x][0];
}
int kth(int p,int p2,int p3,int p4,int l,int r,int k) {
	if(l==r) return l;
	int mid=l+r>>1,d=c[ls[p]]+c[ls[p2]]-c[ls[p3]]-c[ls[p4]];
	if(k<=d) return kth(ls[p],ls[p2],ls[p3],ls[p4],l,mid,k);
	return kth(rs[p],rs[p2],rs[p3],rs[p4],mid+1,r,k-d);
}
inline int find(int x) {
	while(dep[x]) x=f[x][lg[dep[x]]];
	return x; 
}
void dgs(int fa,int u) {
	for(int i=1;i<=lg[dep[u]];i++) f[u][i]=0;
	f[u][0]=fa,dep[u]=dep[fa]+1;
	for(int i=1;i<=lg[dep[u]];i++) {
		f[u][i]=f[f[u][i-1]][i-1];
	}
	rt[u]=rt[fa]; ins(rt[u],1,1e9,a[u]);
	for(int v:V[u]) {
		if(v!=fa) {
			dgs(u,v);
		}
	}
	
}
int main() {
	int T; scanf("%d%d%d",&T,&n,&m); scanf("%d",&T);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=m;i++) {
		int u,v; scanf("%d%d",&u,&v);
		V[u].push_back(v),V[v].push_back(u);
	}
	lg[0]=-1;
	for(int i=1;i<=n;i++) {
		lg[i]=lg[i>>1]+1;
	}
	for(int i=1;i<=n;i++) {
		if(!rt[i]) {
			dfs(0,i);
		}
	}
	int ans=0;
	while(T--) {
		int x,y; scanf("%s%d%d",op,&x,&y);
		if(op[0]=='Q') {
			int k; scanf("%d",&k);
			x^=ans,y^=ans,k^=ans;
			int t=lca(x,y);
			printf("%d\n",ans=kth(rt[x],rt[y],rt[t],rt[f[t][0]],1,1e9,k));
		} else {
			x^=ans,y^=ans;
			int u=find(x),v=find(y);
			if(sz[u]>sz[v]) swap(u,v),swap(x,y); 
			V[y].push_back(x),V[x].push_back(y);
			sz[v]+=sz[u],dgs(y,x);
		}
	}
	return 0;
}

树上启发式合并

应用轻重链剖分,每次先处理轻儿子,清空轻儿子的影响,再处理重儿子,不用清空,之后再暴力算轻儿子的贡献

由于对于每个点轻链只有\(lg\)级别,所以每次暴力处理轻儿子时间复杂度是\(O(nlgn)\)

而重儿子只有一个父亲,继承重儿子(?)是\(O(n)\)

  • 只能处理无修改,全是子树操作的题目

模板

ybtoj B. 【例题2】树上众数

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

const int N=1e5+5;
int n,sz[N],a[N],c[N],son[N],mx;
ll sum,ans[N];
vector<int>V[N];

void dfs(int fa,int u) {
	sz[u]=1;
	for(int v:V[u]) {
		if(v!=fa) {
			dfs(u,v);
			sz[u]+=sz[v];
			if(sz[son[u]]<sz[v]) son[u]=v;
		}
	}
}

void dhs(int fa,int u) {
	c[a[u]]++;
	if(c[a[u]]>mx) mx=c[a[u]],sum=a[u];
		else if(c[a[u]]==mx) sum+=a[u];
	for(int v:V[u]) {
		if(v!=fa) {
			dhs(u,v);
		}
	}
}
void clear(int fa,int u) {
	c[a[u]]--;
	for(int v:V[u]) {
		if(v!=fa) clear(u,v);
	}
}
void dgs(int fa,int u,bool t) {
	for(int v:V[u]) {
		if(v!=fa&&v!=son[u]) {
			dgs(u,v,0);
		}
	}
	if(son[u]) dgs(u,son[u],1);
	c[a[u]]++;
	if(c[a[u]]>mx) mx=c[a[u]],sum=a[u]; 
		else if(c[a[u]]==mx) sum+=a[u];
	for(int v:V[u]) {
		if(v!=fa&&v!=son[u]) dhs(u,v);
	}
	ans[u]=sum;
	if(!t) {
		mx=0,sum=0;
		clear(fa,u);
	}
}
int main() {
	scanf("%d",&n);
	for(int i=1;i<=n;i++) {
		scanf("%d",&a[i]); 
	}
	for(int i=1;i<n;i++) {
		int u,v; scanf("%d%d",&u,&v);
		V[u].push_back(v),V[v].push_back(u); 
	}
	dfs(0,1),dgs(0,1,0);
	for(int i=1;i<=n;i++) {
		printf("%lld%c",ans[i],i==n?'\n':' ');
	}
	return 0;
}

例题2

luogu CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths

回文串等价于至多一个数出现奇数次,也就是将所有的数的2的幂次(保证不重复)异或起来后的和为2的幂次或者0

显然树上启发式合并

如何快速求异或和呢?

  • 法1:设d[u]表示u到根的路径的异或和,d[u,v]=d[u]^d[v]
  • 法2:每次从重儿子移动到父亲时需要将重儿子的所有点异或上边权,反面想想,打上标记,然后将轻儿子异或上那个值,求解时把数组的下标异或上标记即可

这里为了方便采用了法1

由于异或是一个非常好用的东西,d[u]=d[u,v]^d[v],其中d[u,v]是枚举的答案

对于每个二进制数统计最大深度即可(注意是不同子树)

深度用了法2统计,其实用法1统计转化为dep[son]-dep[u]也是ok的

被样例hack了一次:子树的根也可以作为链的端点,链的顶点也要当作轻儿子处理

WA了一次:忘记把重儿子的答案算到子树中了

#include<bits/stdc++.h>
const int INF=1e9;
using namespace std;

const int N=5e5+5,M=(1<<22)+6;
int n,d[N],son[N],sz[N],c[M],ans[N];
char s[20];
struct A {int v,w; };
vector<A>V[N],V1;

void dfs(int u) {
	sz[u]=1; 
	for(A v:V[u]) {
		d[v.v]=d[u]^(1<<v.w);
		dfs(v.v);
		if(sz[son[u]]<sz[v.v]) son[u]=v.v;
	}
}
void dhs(int u,int dep) {
	V1.push_back((A){u,dep});
	for(A v:V[u]) dhs(v.v,dep+1);
}
int g[N];
void clear(int u) {
	c[d[u]]=-INF;
	for(A v:V[u]) {
		clear(v.v);
	}
}
void dgs(int u,bool t) {
	for(A v:V[u]) {
		if(v.v!=son[u]) {
			dgs(v.v,0);
			ans[u]=max(ans[u],ans[v.v]);
		}
	}
	if(son[u]) dgs(son[u],1),ans[u]=max(ans[u],ans[son[u]]);
	g[u]=g[son[u]]+1;
	ans[u]=max(ans[u],c[d[u]]+g[u]-1);
	for(int j=0;j<22;j++) {
		ans[u]=max(ans[u],c[d[u]^(1<<j)]+g[u]-1);
	}
	c[d[u]]=max(c[d[u]],1-g[u]);
	for(A v:V[u]) {
		if(v.v!=son[u]) {
			V1.clear();
			dhs(v.v,1);
			for(A v:V1) {
				ans[u]=max(ans[u],v.w+c[d[v.v]]+g[u]-1);
				for(int j=0;j<22;j++) {
					ans[u]=max(ans[u],v.w+c[d[v.v]^(1<<j)]+g[u]-1);
				}
			}
			for(A v:V1) {
				c[d[v.v]]=max(c[d[v.v]],v.w+1-g[u]);
			}
		}
	}
	if(!t) {
		clear(u);
	}
}
int main() {
	scanf("%d",&n);
	for(int i=2;i<=n;i++) {
		int u;scanf("%d%s",&u,s);
		V[u].push_back((A){i,s[0]-'a'});
	}
	dfs(1);
	for(int i=0;i<(1<<22);i++) c[i]=-INF;
	dgs(1,1);
	for(int i=1;i<=n;i++) {
		printf("%d%c",ans[i],i==n?'\n':' ');
	}
	return 0;
}

并查集

并查集只有按秩合并,时间复杂度是\(O(nlgn)\)的,也就是连通块的树高为\(lgn\)

  • 不支持删除

例题1

ybtoj C. 连通性询问

在不构成环的前提下加边,求路径上的最大值

法1:裂点+倍增\(nlg^2n\)

法2:LCT\(nlgn\)

法3:并查集按秩合并,连结两个并查集的边的边权为时间(从一个集合到另一个集合都需要经过这条边),暴力即可

WA了一次:一直想着搞七搞八,想用秩去搞,是错的,直接暴力取出链即可

RE了一次:还原的时候忘记了根,下次直接复制算了

#include<bits/stdc++.h>
using namespace std;

const int N=5e5+5;
int n,m,f[N],r[N],w[N],ans[N];
inline int ask(int u,int v) {
	if(r[u]>r[v]) swap(u,v);
	int ret=0,t=u;
	for(;u!=f[u];u=f[u]) {
		ans[f[u]]=max(ans[u],w[u]);
	}
	for(;;v=f[v]) {
		if(t==v) {
			for(;t!=f[t];t=f[t]) {
				ans[f[t]]=0;
			}
			return ret;
		} else if(ans[v]) {
			ret=max(ret,ans[v]);
			for(;t!=f[t];t=f[t]) ans[f[t]]=0;
			return ret;
		}
		ret=max(ret,w[v]);
	}
}
inline int find(int u) {
	while(f[u]!=u) u=f[u];
	return u;
}
int main() {
	scanf("%d%d",&n,&m); int lst=0;
	for(int i=1;i<=n;i++) f[i]=i,r[i]=1;
	int cnt=0;
	for(int i=1;i<=m;i++) {
		int op,u,v; scanf("%d%d%d",&op,&u,&v),u^=lst,v^=lst;
		if(op==1) {
			if(find(u)!=find(v)) {
				puts("0"); lst=0;
			} else  printf("%d\n",lst=ask(u,v));
		} else {
			cnt++;
			u=find(u),v=find(v);
			if(u!=v) {
				if(r[u]>r[v]) {
					f[v]=u,w[v]=cnt;
				} else if(r[u]==r[v]) {
					f[v]=u,r[u]++,w[v]=cnt;
				} else {
					f[u]=v,w[u]=cnt;
				}
			}
		}
	}
	return 0;
}

长链剖分

按深度确定轻重儿子,是\(O(n)\)

posted @ 2021-03-30 20:53  wwwsfff  阅读(235)  评论(0编辑  收藏  举报