启发式合并学习笔记
写 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;
}
嗯,不错。剩下的以后写了题再补。

浙公网安备 33010602011771号