树是一种非常有条理的数据结构,从很多方面来看都是这样。每一个节点有其唯一确定的父亲节点,也有唯一确定的边权或点权。因为没有环,树上可以方便地dfs。并且很多链上的做法都可以推广到树上。
树上常用或不常用的有这些方法:
- 倍增
- 树链剖分
- dfs
- 树的直径和重心
- 树形dp
- 树上背包
- 树上期望dp
- 各种图转树
dfs
dfs用于统计很多信息。这里贴一份常用代码。
void dfs(int u, int pa) {
siz[u] = 1;
for(int i = 0; i < G[u].size(); i++) {
int v = G[u][i].to;
if(v == pa) continue;
fa[v] = u;
dpt[v] = dpt[u] + 1;
w[v] = G[u][i].dist;
//g[u].push_back(v); 如果想把无根树转成有根树可以加这一句
dfs(v, u);
siz[u] += siz[v];
}
}
dfs找唯一路径
这是很暴力的做法。就是以起点为根遍历整棵树。它的时间复杂度是 O(n) 的。
倍增
其实非常好写。 预处理每个节点的 2j 级祖先,可以同时处理节点到祖先上的权值(最大或求和)。查询时两边不断向上跳 2k 个节点并统计信息。
void getanc() {
for(int i = 0; i < n; i++) anc[i][0] = fa[i];
for(int j = 1; (1<<j) < n; j++) for(int i = 0; i < n; i++) anc[i][j] = anc[anc[i][j-1]][j-1];
}
int lca(int x, int y) {
if(dpt[x] < dpt[y]) swap(x, y);
for(int j = 30; j >= 0; j--) if(dpt[x] - (1<<j) >= dpt[y]) x = anc[x][j];
if(x == y) return x;
for(int j = 30; j >= 0; j--) if(anc[x][j] != anc[y][j]) {
x = anc[x][j];
y = anc[y][j];
}
return anc[x][0];
}
应用主要就是求lca。没有修改权值的时候可以代替树链剖分。树上的节点是可以动态增加的,每增加一个只用更新新节点的信息就行。
树链剖分
正如从ST表到线段树,倍增算法不能处理修改,因而用树链剖分。(其实它们两个是完全不同的算法,且链剖更早)
考虑用线段树维护树的某种序列,达到查询、修改都是
logn
的效果。树上怎么快速查询?考虑把查询的路径划成一段段的,每一段上的编号又是连续的,就可以方便地使用线段树了。
怎么划分?一种好的方法是重链剖分,可以保证复杂度较低(如长链剖分就不行)。重儿子是每个节点的儿子节点中子树节点数最大的那一个。从根开始dfs,每到一个节点,先dfs它的重儿子,再依次dfs其他儿子。这样,树就自然被按重儿子划开了。
树链剖分还可以处理子树。因为子树里的编号一定是连续的,因此节点u的子树的区间就是
[id[x],id[x]+size[x]−1]
(size是子树节点数)。
少数细节见代码。
#include<cstdio>
#include<vector>
#include<algorithm>
#include<cstring>
using namespace std;
const long long maxn = 100050;
vector<long long> G[maxn];
long long n, p;
long long fa[maxn], hson[maxn], tp[maxn], dpt[maxn], w[maxn], siz[maxn]; //hson是重儿子 tp是每条链的顶端 dpt是深度 w是点权 siz是子树大小
long long id[maxn], idn, real[maxn];
struct despac {
long long l, r, su, laz;
}tr[maxn<<2]; //线段树节点
long long nn;
long long read() {
long long a = 0, c = getchar(), w = 1;
for(; c < '0' || c > '9'; c = getchar()) if(c == '-') w = -1;
for(; c >= '0' && c <= '9'; c = getchar()) a = a * 10 + c - '0';
return a * w;
}
void build(long long u, long long pa) {
long long maxw = 0;
siz[u] = 1;
for(long long i = 0; i < G[u].size(); i++) {
long long v = G[u][i];
if(v == pa) continue;
fa[v] = u;
dpt[v] = dpt[u] + 1;
build(v, u);
if(siz[v] > maxw) {
maxw = siz[v];
hson[u] = v;
}
siz[u] += siz[v];
}
}
void build2(long long u, long long pa, long long top) {
real[idn] = u;
id[u] = idn++;
tp[u] = top;
if(hson[u] != -1) build2(hson[u], u, tp[u]);
for(long long i = 0; i < G[u].size(); i++) {
long long v = G[u][i];
if(v == pa || v == hson[u]) continue;
build2(v, u, v);
}
}
void build_st(long long x, long long l, long long r) {
if(l == r) {
tr[x].su = w[real[l]];
tr[x].su %= p;
return;
}
long long mid = l + r >> 1;
build_st(tr[x].l=nn++, l, mid);
build_st(tr[x].r=nn++, mid+1, r);
tr[x].su = tr[tr[x].l].su + tr[tr[x].r].su;
tr[x].su %= p;
}
void pushdown(long long x, long long ln, long long rn) {
if(tr[x].laz) {
tr[tr[x].l].laz += tr[x].laz;
tr[tr[x].l].laz %= p;
tr[tr[x].r].laz += tr[x].laz;
tr[tr[x].r].laz %= p;
tr[tr[x].l].su += ln * tr[x].laz;
tr[tr[x].l].su %= p;
tr[tr[x].r].su += rn * tr[x].laz;
tr[tr[x].r].su %= p;
tr[x].laz = 0;
}
}
long long query(long long x, long long l, long long r, long long L, long long R) {
if(L <= l && R >= r) return tr[x].su;
long long mid = l+r>>1;
pushdown(x, mid-l+1, r-mid);
long long ans = 0;
if(L <= mid) {
ans += query(tr[x].l, l, mid, L, R);
ans %= p;
}
if(R > mid) {
ans += query(tr[x].r, mid+1, r, L, R);
ans %= p;
}
return ans;
}
void modify(long long x, long long l, long long r, long long L, long long R, long long val) {
if(L <= l && R >= r) {
tr[x].su += val * (r-l+1);
tr[x].su %= p;
tr[x].laz += val;
tr[x].laz %= p;
return;
}
long long mid = l+r>>1;
pushdown(x, mid-l+1, r-mid);
if(L <= mid) modify(tr[x].l, l, mid, L, R, val);
if(R > mid) modify(tr[x].r, mid+1, r, L, R, val);
tr[x].su = tr[tr[x].l].su + tr[tr[x].r].su;
tr[x].su %= p;
}
long long cquery(long long x, long long y) { // c = chain
long long tx = tp[x], ty = tp[y];
long long ans = 0;
while(tx != ty) { // not in one chain
if(dpt[tx] < dpt[ty]) {
swap(tx, ty);
swap(x, y);
}
ans += query(0, 0, n-1, id[tx], id[x]);
ans %= p;
x = fa[tx];
tx = tp[x];
}
if(dpt[x] < dpt[y]) swap(x, y);
ans += query(0, 0, n-1, id[y], id[x]);
ans %= p;
return ans;
}
long long cmodify(long long x, long long y, long long val) {
long long tx = tp[x], ty = tp[y];
while(tx != ty) {
if(dpt[tx] < dpt[ty]) {
swap(tx, ty);
swap(x, y);
}
modify(0, 0, n-1, id[tx], id[x], val);
x = fa[tx];
tx = tp[x];
}
if(dpt[x] < dpt[y]) swap(x, y);
modify(0, 0, n-1, id[y], id[x], val);
}
int main() {
long long q, rt;
n = read(); q = read(); rt = read()-1; p = read(); //以rt为根,所有答案对p取模
for(long long i = 0; i < n; i++) w[i] = read();
for(long long i = 0; i < n-1; i++) {
long long from = read()-1, to = read()-1;
G[from].push_back(to);
G[to].push_back(from);
}
memset(hson, -1, sizeof(hson));
build(rt, -1);
build2(rt, -1, rt);
build_st(nn++, 0, n-1);
while(q--) {
long long op = read();
if(op == 1) { //链修改
long long x = read()-1, y = read()-1, z = read();
cmodify(x, y, z);
}
if(op == 2) { //链询问
long long x = read()-1, y = read()-1;
printf("%lld\n", cquery(x, y));
}
if(op == 3) { //子树修改(整个子树增加z )
long long x = read()-1, z = read();
modify(0, 0, n-1, id[x], id[x]+siz[x]-1, z);
}
if(op == 4) { //子树询问
long long x = read()-1;
printf("%lld\n", query(0, 0, n-1, id[x], id[x]+siz[x]-1));
}
}
return 0;
}
树的直径和重心
直径是树上最长的一条链。找直径的做法是
O(n)
的,先随便找一个点为根dfs,找到离这个点最远的点P,再以P为根dfs,找到离P最远的点Q。则PQ就是树的直径。
直径有一些应用。比如要在树上选一个点使得它到所有叶子结点(含根)的距离和最小,那么这个点就是直径的中点。
重心的最大子树大小最小。求重心相对更容易,做一遍dfs,记录每个子树节点的个数,对于一个节点,除了它的子树以外的节点的个数就是节点总个数n减去子树大小size。每次取两边的最小值,然后找所有节点中该值的最小值就可以了。
点分治
每次找到当前树的重心,然后递归去掉重心后的每一颗子树。
树形DP
树形DP可以考虑两种DP顺序,一种是从根到叶子,一种是从叶子到根。很多时候都需要灵活运用。只有父子关系最简单,如果兄弟之间互相关联就较困难,如果兄弟之间大多都要考虑,应考虑不用DP。有一种方法就像求重心统计时用的,即统计除了子树以外的节点。
树上期望DP
考虑一个节点的dp值,一定是来源于父亲已计算的值和另一个值。即 f[u]=f[fa]+g[fa] 。
树上背包
先合并所有子树的背包,再把父亲节点的背包合并上去。
注意只有一个儿子的时候直接01背包,不需要合并。
tarjan思路
有一个类似于树的结构叫作仙人掌。这上面用tarjan缩点是最方便的。当然一般的图也可以直接做。tarjan非常好理解,一边dfs一边入栈,遇到访问过的节点就弹栈。由此又可以想出很多方法把图转成一颗树。
有的时候,原图的解答树直接去掉所有返祖边就是可用的树。有的时候,把环上的点都连到其上深度最浅的点就可以。
缩成树以后可以照样用倍增和树链剖分,特判一下在一个环上的情况就行。