又叫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
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>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<
这里再来一道差不多的题目
Tree Requests
大意:给定一棵树,以1为根,每一个节点上面有一个字母,定义节点的深度为其到1号节点的路径上的点数。m次查询,查询以a为根的子树内深度为b的节点上的字母重排之后能否构成回文串。
思路:
先考虑一波构成回文串的条件,显然就是要求个数为奇数的字母不超过一个,那么我们只要设一个cnt数组来记录深度为i,字母为j的节点数量即可,其它的就跟上一题没有多大差别了
另外,这里的深度是固定的,而不是对于子树深度要求重排,所以深度也只用记录一次,那么就很简单了
code
#include
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> 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<
再来一个
Dominant Indices
大意:
给定一棵以 1 为根,n 个节点的树。设 d(u,x) 为 u 子树中到 u 距离为 x 的节点数。
对于每个点,求一个最小的 k,使得d(u,k) 最大。
思路:
跟前面差不多,维护一下深度再记录一下最大值就可以了
考虑到最大值标记ma在add操作中不能重置,我们应该在撤销影响后初始化ma
code
#include
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]>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<
Tree and Queries
还是板子题
u k
:询问在以 u 为根的子树中,出现次数 ≥k 的颜色有多少种唯一可能有点绕的地方应该就是对大于等于k的次数的处理
那么这个其实也很好解决。我们不难发现,大于等于i的数字,一定是从大于等于i-1的地方转移过来的,所以我们只要在每次add操作的时候O(1)去更新一下就可以了
code
#include
#include
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 q[N];
ll num[N],dk[N];
map ans[N];
vector 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>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<
前面都是小菜,这里再来一个压轴难度的