速通版:
问题如果可以转化成关于子树的查询,(比如查询某棵子树内的信息、比如查询兄弟子树的信息),那么可以先尝试暴力做法:
对于rt,我们一棵一棵地遍历它的子树,每棵子树都是先统计答案,然后更新相关信息。 如果我们枚举每棵子树,统计答案,然后清空信息,这样是标准的O(N²), 但是我们发现,最多可以保留1棵子树的信息不清空,因为回到rt时这颗子树和前面0棵子树的信息不会产生答案贡献,从第二颗子树开始才会产生贡献, 所以我们希望让保留信息的子树尽量大,于是结合重儿子的概念,保留重子树,这样可以证明是O(Nlogn)
也就是说,dsu on tree仅仅是改了个dfs的顺序,让重子树最后计算,并保留它的信息给根节点。
参考大佬博客传送
我们先从第一个例题开始看起:
E. Lomsat gelral
题意是给定了一个无根树,带点权,q次询问,每次询问某棵子树内,点权众数之和。(如果2 3 4都是众数,那么答案是2+3+4=9)
首先最直接的暴力方法就是,枚举每棵子树,通过遍历其节点O(n)求出答案,并删除记录的信息,时间复杂度是O(n²)
优化方法,我们注意到对于根节点rt,遍历完其所有孩子节点的子树后,最后递归回来的这颗子树不需要删除,可以直接由rt继承,我们自然想到最后这个子树的size越大
越好,这便是所谓启发式合并。 子树size最大不正是轻重链剖分里面的重儿子的概念吗? 所以算法流程就出来了:
首先轻重链剖分,求出重儿子
全局维护出现众数的出现次数,以及所求的答案,进行dfs。优先dfs轻儿子,最后dfs重儿子,重儿子的影响保留,rt重新统计轻儿子的贡献并记录答案,如果rt是其父节点的轻儿子,删除rt的影响,否则保留。(建议看代码以及注释)
删除操作就是暴力递归给轻儿子,每到一个节点就消除影响。
时间复杂度分析 ,我们考虑某个节点u,u被访问只有两种情形:
①通过重链被访问,这种情况只会访问1次 ,因为重链的影响是永久性的,不会被删
②通过轻边被访问,由于树剖后根节点到u只有logn条轻边,所以u只会被访问logn次。
综上时间复杂度为nlogn
例题代码:
#include
using namespace std;
//#pragma GCC optimize(2)
#define ull unsigned long long
#define ll long long
#define pii pair<int, int>
#define pdd pair<double, double>
#define re register
#define lc rt<<1
#define rc rt<<1|1
const int maxn = 1e5 + 10;
const ll mod = 998244353;
const ll inf = (ll)4e17+5;
const int INF = 1e9 + 7;
const double pi = acos(-1.0);
//给定树 带点权 询问子树中,出现次数最多的若干种点权的点权和 (众数之和)
//暴力解法 对于每个子树都花O(n)求出其答案,并用O(N)清空影响 时间O(N平方)
//发现对于根节点rt,最后递归的子树可以不删,保留信息给根节点,我们最后递归重儿子,保留重儿子的信息 即:启发式合并
//我们保留重儿子,删除轻儿子的影响,并最后重新统计一遍轻儿子
//nlogn 某个点u被访问:要么通过重链(不会被删,只会被访问1次),要么通过轻边,但根到u只有logn条轻边,所以最多logn次
vector<int> g[maxn];
int v[maxn];
int cnt[maxn];
int mmx=-1;//众数出现次数
ll ret, ans[maxn];
int n;
//轻重链剖分
int son[maxn],siz[maxn];
void dfs(int rt,int fa)
{
siz[rt]=1;
for(int &i:g[rt])
{
if(i==fa) continue;
dfs(i,rt);
if(siz[i] > siz[son[rt]]) son[rt]=i;
siz[rt]+=siz[i];
}
}
int Son;//当前子树的重儿子
void add(int rt,int fa,int a)//rt 统计贡献 贡献v
{
cnt[v[rt]]+=a;
if(cnt[v[rt]]==mmx)
{
ret+=v[rt];
}
else if(cnt[v[rt]] > mmx)
{
mmx=cnt[v[rt]];
ret=v[rt];
}
for(int &i:g[rt])
{
if(i==fa || i==Son) continue;//重边不会被访问 也就是每次通过轻边最多访问size/2个节点
add(i,rt,a);
}
}
void dfs2(int rt,int fa,bool ok) //先递归轻链 最后再求重儿子并且父节点的答案继承重儿子的答案 再暴力统计轻儿子
{
for(int &i:g[rt])
{
if(i==fa || i==son[rt]) continue;
dfs2(i,rt,0);
}
if(son[rt]) dfs2(son[rt],rt,1),Son=son[rt];//重链的信息是永久性的 不会被删
add(rt,fa,1);
ans[rt]=ret;
if(!ok) //是轻儿子子树的信息 需要删去整棵轻儿子子树
{
Son=0;//保证轻儿子的重儿子也要删去
add(rt,fa,-1);
mmx=-1;
ret=0;
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",v+i);
for(int i=1;i<n;i++)
{
int v,u;
scanf("%d %d",&u,&v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1,1);
dfs2(1,1,0);
for(int i=1;i<=n;i++) cout<<ans[i]<<' ';
return 0;
}
总结一下就是:
树上启发式合并一般适用于
②预处理:树剖求出重儿子(忽略长链剖分,因为太罕见 )
③solve:递归所有轻儿子,统计该子树答案后删除影响;
递归重儿子,记录答案,不删除影响
暴力遍历rt的轻儿子,统计rt的答案
如果rt是其父亲的轻儿子,就暴力删除rt的影响。(根据所维护的数据结构的不同有时候不需要递归遍历,可以直接删掉)
常数优化小技巧:add函数的递归用dfs序来实现
子树查询,不带修改,先思考暴力解法。
对于每棵子树,我们统计cnt[d][i]
表示该子树中,深度为d,颜色为i的节点个数,
离线查询,统计完rt的信息后,枚举rt的每个查询{h,id}
,检查cnt[h][i]
中奇数是否超过了1个,如果是,ans[id]=0,否则ans[id]=1;
暴力解法是O(n²),但是发现最后一棵子树是的信息可以合并给根节点的,所以可以用dsu on tree优化成nlogn。
#include
using namespace std;
//#pragma GCC optimize(2)
#define ull unsigned long long
#define ll long long
#define pii pair<int, int>
const int maxn = 5e5 + 10;
const ll mod = 998244353;
const ll inf = (ll)4e17+5;
const int INF = 1e9 + 7;
const double pi = acos(-1.0);
ll inv(ll b){if(b==1)return 1;return(mod-mod/b)*inv(mod%b)%mod;}
//1为根 有根树 带点权 点权a到z 询问rt子树 深度为d的这层节点 点权重排能否构成回文串
//暴力 -> 树上启发式合并
vector<int> g[maxn];
int cnt[maxn][30];//深度为i 颜色为j的个数
int son[maxn],siz[maxn],dep[maxn];
char col[maxn];
vector<pii> q[maxn];
bool ans[maxn];
//轻重链剖分
void dfs(int rt)
{
siz[rt]=1;
for(int &i:g[rt])
{
dep[i]=dep[rt]+1;
dfs(i);
if(siz[i] > siz[son[rt]]) son[rt]=i;
siz[rt]+=siz[i];
}
}
bool check(int d) //check深度为d 颜色分布情况
{
int odd=0;//奇数个数
for(int i=1;i<=26;i++)
{
odd+=(cnt[d][i]&1);
}
return odd<=1;
}
int SON;
void add(int rt,int v) //这个函数其实就是暴力解法的步骤 只不过当前rt的重儿子不需要递归下去
{
int id=col[rt]-'a'+1;
cnt[dep[rt]][id]+=v;
for(int &i:g[rt])
{
if(i==SON) continue;//重儿子不需要统计
add(i,v);
}
}
void dfs2(int rt,bool ok)
{
for(int &i:g[rt])
{
if(i==son[rt]) continue;
dfs2(i,0);
}
if(son[rt])
{
dfs2(son[rt],1);
SON=son[rt];
}
add(rt,1),SON=0;
for(auto i:q[rt])
{
ans[i.second]=check(i.first);
}
if(!ok) //是轻儿子 删了
{
add(rt,-1);
}
}
int n,m;
int main()
{
dep[1]=1;
scanf("%d %d",&n,&m);
for(int i=2;i<=n;i++)
{
int fa;scanf("%d",&fa);
g[fa].push_back(i);
}
scanf("%s",col+1);
dfs(1);
for(int i=1;i<=m;i++)
{
int u,d;
scanf("%d %d",&u,&d);
q[u].push_back({d,i});
}
dfs2(1,0);
for(int i=1;i<=m;i++)
if(ans[i]) puts("Yes");
else puts("No");
return 0;
}