小技巧——树
Part1
问题:统计一条根链上的点权值出现次数。
首先不难想到对根链建立主席树,可以做到 \(O(nlogn)-O(logn)\) 的优秀复杂度。
码量有些大,但它是在线算法。
离线算法
我们这样考虑:
若知道 \(x\) 的根链的点权集合,那么可以 \(O(1)\) 转移为 \(fa_x\) 和 \(son_x\) 的根链点权集合。(本质上是莫队的思想)
于是通过离线将询问放在点上,通过一次 \(dfs\) 统计答案,点权集合用 桶 或 哈希表。
时间复杂度 \(O(n)\)。
另一种思考角度:可以理解为搜索时的路径记录。
拓展问题:树上任意两点之间的链上的点权值出现次数。
这可以通过树上前缀和进行转化为根链问题。(\(ans_{x,y}=ans_{x,1}+ans_{y,1}-ans_{lca,1}-ans_{fa_{lca},1}\))
Part2
描述:\(dfs\) 序求 \(LCA\)。
有些时候树剖的小常数 \(O(logn)\),已经不够用了,但欧拉序求 \(LCA\) 常数有些大。
这时便有 \(dfs\) 序求 \(LCA\)。
我们回忆欧拉序,其实是通过从 \(a\) 子树到 \(b\) 子树遍历过程中,\(LCA\) 会被遍历到,因此使用 \(RMQ\) 询问。
现在使用 \(dfs\) 序,从 \(a\) 子树到 \(b\) 子树遍历中,如何存储 \(LCA\)?
我们存储 \(fa_x\),查询 \([dfn_x+1,dfn_y]\) 中 \(dep\) 或 \(dfn\) 最小的点。
代码:
#include<iostream>
#include<vector>
using namespace std;
constexpr int N=5e5+5;
int n,m,s,ST[20][N],dfn[N],cnt,log[N];
vector<int> v[N];
int gt(int x,int y){ return (dfn[x]<dfn[y])?x:y; }
void dfs(int x,int f){
ST[0][dfn[x]=++cnt]=f;
for(int u:v[x])if(u!=f)dfs(u,x);
}void build(){
log[0]=-1;
for(int i=1;i<=n;i++)log[i]=log[i>>1]+1;
for(int i=1;i<=log[n];i++)
for(int j=1;j+(1<<i)<=n+1;j++)
ST[i][j]=gt(ST[i-1][j],ST[i-1][j+(1<<(i-1))]);
}int LCA(int x,int y){
if(x==y)return x; x=dfn[x],y=dfn[y];
if(x>y)swap(x,y); int k=log[y-(x++)];
return gt(ST[k][x],ST[k][y-(1<<k)+1]);
}signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>s;
for(int i=1,x,y;i<n;i++)
cin>>x>>y,v[x].push_back(y),v[y].push_back(x);
dfs(s,0),build();
for(int i=1,x,y;i<=m;i++)
cin>>x>>y,cout<<LCA(x,y)<<'\n';
return 0;
}
Part3
描述:树的三度化
作用:将一棵带权树,在保证任意两点距离不变的情况下,转化为每个节点度数最多为 3 的树。
考虑无权多叉树转二叉树:左儿子、右兄弟。
我们不妨在每条边上设置虚点用于连边,具体的如下图所示:

.png)
可以看到,通过构造边权为 0 的边从而使度数减少,其实是用点换度数。
可以证明,总共有 \(2n-1\) 个点。
注意:三度化会大幅增加树的深度,需要配合 树剖/点分树 食用。
Part4
描述:树上二分
当一些答案具有单调性,如带权树的重心,可以进行二分。
具体地,考虑树的重心,可以将问题规模减半,实现 \(logn\) 的复杂度。
建立点分树,通过点分树上查找进行二分。
由于答案只能通过相邻边转移,可以在建立点分树时记录一个 \(pre\) 数组。
含义为 \(pre_rot=son_x=u\),即 \(rot\) 为 \(x\) 在点分树上的儿子,\(u\) 为 \(x\) 在原树上的的儿子,且 \(u\) 子树的重心为 \(rot\)。
由于需要遍历点分树上节点 \(x\) 的所有儿子,所以复杂度与结点度数有关,建议使用三度化。

浙公网安备 33010602011771号