启发式合并学习笔记

P6623 [省选联考 2020 A 卷] 树 时发现根本不会,强制恶补。

如有不足,还请提出。

1.是什么

请出老朋友并查集,并查集中有个优化为按两个并查集高度大小贪心的合并。

void merge(int x,int y)
{
	int fax=find(x),fay=find(y);
	if(siz[fax]<siz[fay])swap(fax,fay);//就是这里
	fa[fay]=fax,siz[fax]+=siz[fay];
	return ;
}l

恭喜你,学会了启发式合并

当然,启发式合并还有其他用处,我大致理解为对于树上的子树问题,假如每次询问可以用 \(O(n)\) 解决并且离线,那么就能尝试把启发式合并套上去。

怎么写

以下面这道题为例:

给出一棵 \(n\) 个节点以 \(1\) 为根的树,节点 \(u\) 的颜色为 \(c_u\),现在对于每个结点 \(u\) 询问以 \(u\) 为根的子树里一共出现了多少种不同的颜色。

离线下来,对于每次询问,显然是可以 O(n) 遍历一遍,用桶记录颜色的。但是我们发现,如果遍历的过程中有很多重复的过程,能不能像并查集合并那样将当前的答案从儿子那转移过来呢。

如果你不考虑空间,可以每次都保留每棵子树对应的桶,但这显然会炸。我们只保留一个桶,则保留子树大小最大(重儿子)的桶是最优的,则对于轻儿子我们仍然用相同的办法统计答案(递归下去)。

在遍历完之后将轻儿子的桶清空,避免影响重儿子以及其他儿子的答案统计,最后遍历重儿子,统计答案并且清空桶。

此时对于当前节点,我们已经有了重儿子的答案(桶),直接拿过来用就好了。接下来只用再遍历一遍轻儿子,将答案加入桶内即可。

实现

给个大致代码,随意口胡的,喷轻点。

void add(int x){/*Do sth.*/}
void add(int x){/*Do sth.*/}
void del(int x){}
int root;//当前遍历到的节点
void update(int x,int fa,int flag)//统计答案,flag:删除/不删
{
	if(flag)add(x);
	else del(x);
	for(auto y:a[x])
		if(y!=fa&&y!=son[root])//不统计重儿子
			update(y,x,flag);
	return ;
}
void dfs(int x,int fa,int flag)//flag:是否为父亲的重儿子
{
	for(auto y:a[x])
		if(y!=fa&&y!=son[x])
			dfs(y,x,0);
	if(son[x])dfs(son[x],x,1);//重儿子
	root=x;
	update(x,fa,1);//统计当前子树的答案
	if(!flag)update(x,fa,0);//不是重儿子则清空
	return ;
}

时间复杂度不太会证,反正这种可以 \(O(n)\) 求一个子树答案的基本上都是 \(O(n log n)\)。(也许?

例题

就开头那道吧,P6623 [省选联考 2020 A 卷] 树

01 Trie 经典存异或值贪心统计答案,一个 \(val(i)\) 是好求的,直接用 \(dsu on tree\) 优化,最后将答案累加就完了。

Tips: 01 Trie 的一个 Trick,快速整体加一之若存在 1 儿子,则交换左右子树,要求从低到高位存 Trie。具体画图感性理解吧。作者太菜

细节看注释。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=525015;
int n;
int v[N];
vector<int>ed[N];
int son[N],deep[N],siz[N];
int Ans[N],root;
namespace Trie{
	int tr[N*20][2],s[30][2],siz[N*20],tot=1;//关于tot,设为0的时候死活过不去
	void insert(int x)
	{
		int pos=1;
		for(int i=0;i<=20;i++)
		{
			siz[pos]++;//计算子树大小
			bool op=x&(1<<i);
			if(!tr[pos][op])tr[pos][op]=++tot;
			s[i][op]++,pos=tr[pos][op];
		}
		siz[pos]++;
		return ;
	}
	int query()
	{
		int ans=0;
		for(int i=20;i>=0;i--)
			if(s[i][1]&1)ans+=(1<<i);//显然,s是第i位上1的个数
		return ans;
	}
	void update()
	{
		int pos=1;
		for(int i=0;i<=20;i++)
		{
			int l=siz[tr[pos][0]],r=siz[tr[pos][1]];//整体+1
			s[i][0]+=r-l,s[i][1]+=l-r;
			swap(tr[pos][0],tr[pos][1]),pos=tr[pos][0];
			if(!pos)return ;
		}
		return ;
	}
	void clear()//清空
	{
		for(int i=1;i<=tot;i++)siz[i]=tr[i][0]=tr[i][1]=0;
    	memset(s,0,sizeof(s));
    	tot=1;
		return ;
	}
}
void dfs(int x)
{
	siz[x]=1;
	for(auto y:ed[x])
		deep[y]=deep[x]+1,dfs(y),siz[x]+=siz[y],
		son[x]=siz[son[x]]<siz[y]?y:son[x];//预处理重子树等
	return ;
}
void update(int x)
{
	Trie::insert(v[x]+deep[x]-deep[root]);//加入贡献
	for(auto y:ed[x])
		if(y!=son[root])
			update(y);
	return ;
}
void dfs1(int x,int flag)
{
	for(auto y:ed[x])
		if(y!=son[x])
			dfs1(y,0);
	if(son[x])dfs1(son[x],1),Trie::update();//重儿子
	root=x;
	update(x),Ans[x]=Trie::query();
	if(!flag)Trie::clear();//轻儿子删掉
}
signed main()
{
	ios::sync_with_stdio(0);cin.tie(0);
	#ifndef ONLINE_JUDGE
		freopen("a.in","r",stdin);
		freopen("a.out","w",stdout);
	#endif
	cin>>n;
	for(int i=1;i<=n;i++)cin>>v[i];
	for(int i=2,x;i<=n;i++)
		cin>>x,ed[x].push_back(i);
	dfs(1),dfs1(1,1);
	int ans=0;
	for(int i=1;i<=n;i++)ans+=Ans[i];
	cout<<ans;
	return 0;
}

嗯,不错。剩下的以后写了题再补。

posted @ 2025-11-20 10:03  GCSG01  阅读(2)  评论(0)    收藏  举报