树上启发式合并 学习笔记

又叫dsu on tree,一般用来解决下面这类问题

1.只有对子树的查询

2.没有修改操作

其实就有点像并查集里面的启发式合并,只不过是在树上做信息合并罢了。

先来看一道祖传例题

cf600 E

大意:

给出一个树,求出每个节点的子树中出现次数最多的颜色的编号和

思路:

想一想,我们如果暴力的话,就是对每一个节点做dfs,时间复杂度是n^2级别的。

考虑优化,发现只有对子树的询问,所以我们不难想到树链剖分,这样之后子树问题就转化为了序列问题,然后可以再用莫队来解决,时间复杂度来到了n*sqrt(n)级别的。

如果再优化的话,就是今天的dsu on tree了。时间复杂度可以达到nlogn

想一想,启发式合并做的是什么?是把小的一堆信息合并到大的信息上去,那么在树上如何区分信息大小?就是子树大小。那么我们应该如何确定要作为被合并的子树?显然取最大的子树对应的根节点,也就是重儿子。那么,一顿树剖之后,我们就可以确定合并对象了。

再想想,对于普通暴力的做法(虽然时间复杂度已经炸了),显然我们需要一个cnt数组记录每一种颜色的数量,如果每次对一个节点完成查询后清除cnt数组,我们的空间复杂度就不会炸,否则就MLE了。树上启发式也一样,对于儿子的信息查询之后,我们势必要清空数组,但是我们可以合并信息,这也就意味着,如果我们最后对重儿子进行查询的话,查完之后的cnt数组并不用清空,我们可以直接将其它子树的信息合并上去(当然轻儿子的查询是要清空信息的)。

这样一来,重链我们只会访问一次(查询时),加上轻链的边数不会超过logn(因为轻链意味着每一层都要减去至少一半的节点数),我们的时间复杂度就可以达到nlogn。

综上,我们首先一遍dfs找到重儿子。第二遍dfs的时候,首先对轻儿子dfs,然后对重儿子dfs,然后合并信息,最后清除轻儿子的影响即可。这其中合并信息和清除影响可以用一个函数实现,因为清除影响等价于沿路的对应颜色数-1。

code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=1e5+10;
struct ty
{
	ll t,l,next;
}edge[N<<1];
ll cn=0;
ll head[N];
ll flag=0;//标记重儿子 
ll sum=0,ma=0;
void add_edge(ll a,ll b,ll c)
{
	edge[++cn].t=b;
	edge[cn].l=c;
	edge[cn].next=head[a];
	head[a]=cn;
}
ll n;
ll col[N],siz[N],son[N],cnt[N],ans[N]; 
void init_dfs(ll id,ll fa)
{
	siz[id]=1;
	ll sn=0;
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==fa) continue;
		init_dfs(y,id);
		siz[id]+=siz[y];
		if(siz[y]>siz[sn])
		{
			sn=y;
		}
	}
	son[id]=sn;
}
void add(ll id,ll fa,ll val)
{
	cnt[col[id]]+=val;
	if(cnt[col[id]]>ma)
	{
		ma=cnt[col[id]];
		sum=col[id];
	}
	else if(cnt[col[id]]==ma)
	{
		sum+=col[id];
	}
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==fa||y==flag) continue;
		add(y,id,val);
	}
}
void dfs2(ll id,ll fa,ll kp)
{
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==fa) continue;
		if(y==son[id]) continue;
		dfs2(y,id,0); 
	}
	if(son[id]) dfs2(son[id],id,1),flag=son[id];
	add(id,fa,1);//添加 
	flag=0;//清空以免影响后面的删除操作
	ans[id]=sum;
	if(kp==0)
	{
		add(id,fa,-1);
		sum=0;
		ma=0;
	}
}
void solve()
{
	memset(head,-1,sizeof head);
	cin>>n;
	for(int i=1;i<=n;++i) cin>>col[i];
	for(int i=1;i<n;++i)
	{
		ll a,b;
		cin>>a>>b;
		add_edge(a,b,1);
		add_edge(b,a,1);
	}
	init_dfs(1,0);
	dfs2(1,1,0);
	for(int i=1;i<=n;++i) cout<<ans[i]<<" ";
	cout<<endl;
}
int main()
{
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	solve();
	return 0;
}

这里再来一道差不多的题目

Tree Requests

大意:给定一棵树,以1为根,每一个节点上面有一个字母,定义节点的深度为其到1号节点的路径上的点数。m次查询,查询以a为根的子树内深度为b的节点上的字母重排之后能否构成回文串。

思路:

先考虑一波构成回文串的条件,显然就是要求个数为奇数的字母不超过一个,那么我们只要设一个cnt数组来记录深度为i,字母为j的节点数量即可,其它的就跟上一题没有多大差别了

另外,这里的深度是固定的,而不是对于子树深度要求重排,所以深度也只用记录一次,那么就很简单了

code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=5e5+10;
struct ty
{
	ll t,l,next;
}edge[N<<1];
ll cn=0;
ll head[N];
void add_edge(ll a,ll b,ll c)
{
	edge[++cn].t=b;
	edge[cn].l=c;
	edge[cn].next=head[a];
	head[a]=cn;
}
ll n,m;
ll siz[N],son[N],dep[N],col[N];
vector<pair<ll,ll>> vt[N];
ll cnt[N][30],ans[N];
ll son_flag=0;
void init_dfs(ll id,ll fa)
{
	siz[id]=1;
	ll sn=0;
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==fa) continue;
		dep[y]=dep[id]+1;
		init_dfs(y,id);
		siz[id]+=siz[y];
		if(siz[y]>siz[sn])
		{
			sn=y;
		}
	}
	son[id]=sn;
}
bool check(ll dep)//深度信息 
{
	ll num=0;
	for(int i=0;i<26;++i)
	{
		num+=(cnt[dep][i]%2);
	}
	return num<=1;
}
void add(ll id,ll fa,ll val)
{
	cnt[dep[id]][col[id]]+=val;
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==son_flag||y==fa) continue;
		add(y,id,val);
	}
}
void dfs2(ll id,ll fa,ll kp)
{
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==fa||y==son[id]) continue;
		dfs2(y,id,0);
	}
	if(son[id])
	{
		dfs2(son[id],id,1);
		son_flag=son[id];
	}
	add(id,fa,1);
	son_flag=0;
	//统计
	for(auto op:vt[id])
	{
		ans[op.first]=check(op.second);	
	} 
	if(kp==0)
	{
		add(id,fa,-1);
	}
}
void solve()
{
	memset(head,-1,sizeof head);
	cin>>n>>m;
	dep[1]=1;
	for(int i=2;i<=n;++i)
	{
		ll a;
		cin>>a;
		add_edge(a,i,1);
		add_edge(i,a,1);	
	}
	for(int i=1;i<=n;++i){
		char sdd;
		cin>>sdd;
		col[i]=(sdd-'a');
	}
	for(int i=1;i<=m;++i)
	{
		ll a,b;
		cin>>a>>b;
		vt[a].push_back(make_pair(i,b));
	}
	init_dfs(1,0);
//	for(int i=1;i<=n;++i) cout<<dep[i]<<' ';
	dfs2(1,0,0);
	for(int i=1;i<=m;++i) cout<<(ans[i]?"Yes":"No")<<endl;
} 
int main()
{
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	solve();
	return 0;
}

再来一个

Dominant Indices
大意:

给定一棵以 1 为根,n 个节点的树。设 d(u,x) 为 u 子树中到 u 距离为 x 的节点数。

对于每个点,求一个最小的 k,使得d(u,k) 最大。

思路:

跟前面差不多,维护一下深度再记录一下最大值就可以了

考虑到最大值标记ma在add操作中不能重置,我们应该在撤销影响后初始化ma

code

#include<bits/stdc++.h>
using namespace std;
#define ll int
#define endl '\n'
const ll N=1e6+10;
struct ty
{
	ll t,l,next;
}edge[N<<1];
ll cn=0;
ll head[N];
void add_edge(ll a,ll b,ll c)
{
	edge[++cn].t=b;
	edge[cn].l=c;
	edge[cn].next=head[a];
	head[a]=cn;
}
ll n,m;
ll siz[N],son[N],dep[N],col[N];
ll cnt[N],ans[N];
ll son_flag=0;
ll ma=0,p=0;
void init_dfs(ll id,ll fa)
{
	siz[id]=1;
	ll sn=0;
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==fa) continue;
		dep[y]=dep[id]+1;
		init_dfs(y,id);
		siz[id]+=siz[y];
		if(siz[y]>siz[sn])
		{
			sn=y;
		}
	}
	son[id]=sn;
}
void add(ll id,ll fa,ll val)
{
	cnt[dep[id]]+=val;
	if(cnt[dep[id]]>ma||(cnt[dep[id]]==ma&&dep[id]<p))
	{
		ma=cnt[dep[id]];
		p=dep[id];
	}
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==son_flag||y==fa) continue;
		add(y,id,val);
	}
}
void dfs2(ll id,ll fa,ll kp)
{
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==fa||y==son[id]) continue;
		dfs2(y,id,0);
	}
	if(son[id])
	{
		dfs2(son[id],id,1);
		son_flag=son[id];
	}
	
	add(id,fa,1);
	son_flag=0;
	//统计
	//cout<<id<<' '<<ma<<" "<<p<<' '<<dep[id]<<endl;
	ans[id]=p-dep[id];
	
	if(kp==0)
	{
		add(id,fa,-1);
		ma=0,p=0;
	}
	//
}
void solve()
{
	memset(head,-1,sizeof head);
	cin>>n;
	for(int i=2;i<=n;++i)
	{
		ll a,b;
		cin>>a>>b;
		add_edge(a,b,1);
		add_edge(b,a,1);	
	}
	init_dfs(1,0);
	dfs2(1,0,1);
	for(int i=1;i<=n;++i) cout<<ans[i]<<endl;
} 
int main()
{
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	solve();
	return 0;
}

Tree and Queries

还是板子题

  • 给定一棵 n 个节点的树,根节点为 1。每个节点上有一个颜色ci​。�m 次操作。操作有一种:
    1. u k:询问在以 u 为根的子树中,出现次数 ≥k 的颜色有多少种

唯一可能有点绕的地方应该就是对大于等于k的次数的处理

那么这个其实也很好解决。我们不难发现,大于等于i的数字,一定是从大于等于i-1的地方转移过来的,所以我们只要在每次add操作的时候O(1)去更新一下就可以了

code

#include<bits/stdc++.h>
#include <cstring>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=1e5+10;
struct ty
{
    ll t,next;
}edge[N<<1];
ll head[N];
ll cn=0;
void add_edge(ll a,ll b)
{
    edge[++cn].t=b;
    edge[cn].next=head[a];
    head[a]=cn;
}
ll n,m;
ll Flag;
ll siz[N],son[N],col[N];
pair<ll,ll> q[N];
ll num[N],dk[N];
map<ll,ll> ans[N];
vector<ll> vt[N];
void init_dfs(ll id,ll fa)
{
    siz[id]=1;
    ll sn=0;
    for(int i=head[id];i!=-1;i=edge[i].next)
    {
        ll y=edge[i].t;
        if(y==fa) continue;
        init_dfs(y,id);
        siz[id]+=siz[y];
        if(siz[y]>sn)
        {
            sn=siz[y];
            son[id]=y;
        }
    }
}
void add(ll id,ll fa,ll val)
{
    if(val==-1) dk[num[col[id]]]--;
    num[col[id]]+=val;
    if(val==1) dk[num[col[id]]]++;
    for(int i=head[id];i!=-1;i=edge[i].next)
    {
        ll y=edge[i].t;
        if(y==fa||y==Flag) continue;
        add(y,id,val);
    }
}
void dfs2(ll id,ll fa,ll op)
{
    for(int i=head[id];i!=-1;i=edge[i].next)
    {
        ll y=edge[i].t;
        if(y==fa||y==son[id]) continue;
        dfs2(y,id,0);
    }
    if(son[id]) dfs2(son[id],id,1),Flag=son[id];
    add(id,fa,1);
    Flag=0;
    for(auto s:vt[id])
    {
        ans[id][s]=dk[s];
    }
    if(op==0)
    {
        add(id,fa,-1);
    }
}
void solve()
{
    memset(head,-1,sizeof(head));
    cin>>n>>m;
    for(int i=1;i<=n;++i) cin>>col[i];
    for(int i=1;i<n;++i)
    {
        ll a,b;
        cin>>a>>b;
        add_edge(a,b);
        add_edge(b,a);
    }
    for(int i=1;i<=m;++i)
    {
        ll a,b;
        cin>>a>>b;
        vt[a].push_back(b);
        q[i]=make_pair(a,b);
    }
    init_dfs(1,0);
    dfs2(1,0,0);
    for(int i=1;i<=m;++i)
    {
        ll a=q[i].first;
        ll b=q[i].second;
        cout<<ans[a][b]<<endl;
    }
}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

前面都是小菜,这里再来一个压轴难度的

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值