树上方法总结 LCA 树上倍增 树链剖分 树的直径 重心
2017-11-06 21:31
555 查看
树是一种非常有条理的数据结构,从很多方面来看都是这样。每一个节点有其唯一确定的父亲节点,也有唯一确定的边权或点权。因为没有环,树上可以方便地dfs。并且很多链上的做法都可以推广到树上。
树上常用或不常用的有这些方法:
- 倍增
- 树链剖分
- dfs
- 树的直径和重心
- 树形dp
- 树上背包
- 树上期望dp
- 各种图转树
应用主要就是求lca。没有修改权值的时候可以代替树链剖分。树上的节点是可以动态增加的,每增加一个只用更新新节点的信息就行。
考虑用线段树维护树的某种序列,达到查询、修改都是logn的效果。树上怎么快速查询?考虑把查询的路径划成一段段的,每一段上的编号又是连续的,就可以方便地使用线段树了。
怎么划分?一种好的方法是重链剖分,可以保证复杂度较低(如长链剖分就不行)。重儿子是每个节点的儿子节点中子树节点数最大的那一个。从根开始dfs,每到一个节点,先dfs它的重儿子,再依次dfs其他儿子。这样,树就自然被按重儿子划开了。
树链剖分还可以处理子树。因为子树里的编号一定是连续的,因此节点u的子树的区间就是[id[x],id[x]+size[x]−1](size是子树节点数)。
少数细节见代码。
直径有一些应用。比如要在树上选一个点使得它到所有叶子结点(含根)的距离和最小,那么这个点就是直径的中点。
重心的最大子树大小最小。求重心相对更容易,做一遍dfs,记录每个子树节点的个数,对于一个节点,除了它的子树以外的节点的个数就是节点总个数n减去子树大小size。每次取两边的最小值,然后找所有节点中该值的最小值就可以了。
注意只有一个儿子的时候直接01背包,不需要合并。
有的时候,原图的解答树直接去掉所有返祖边就是可用的树。有的时候,把环上的点都连到其上深度最浅的点就可以。
缩成树以后可以照样用倍增和树链剖分,特判一下在一个环上的情况就行。
树上常用或不常用的有这些方法:
- 倍增
- 树链剖分
- 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; d833 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一边入栈,遇到访问过的节点就弹栈。由此又可以想出很多方法把图转成一颗树。有的时候,原图的解答树直接去掉所有返祖边就是可用的树。有的时候,把环上的点都连到其上深度最浅的点就可以。
缩成树以后可以照样用倍增和树链剖分,特判一下在一个环上的情况就行。
相关文章推荐
- 【大二最后两题】Hrbust 2064 萌萌哒十五酱的宠物~【思维+树链剖分 / 树上倍增LCA】
- 模板 树上求LCA 倍增和树链剖分
- [置顶] 对LCA、树上倍增、树链剖分(重链剖分&长链剖分)和LCT(Link-Cut Tree)的学习(填坑ing)
- 树上倍增方法求LCA(最近公共祖先)(转)
- BZOJ-1977 次小生成树 Tree 树上倍增LCA+Kruskal+位运算
- 【Vijos-P1935】不可思议的清晨-树上倍增+LCA+分类讨论
- Codevs 2370 小机房的树 LCA 树上倍增
- ural1752找树上距某个点某距离的点(树的直径+倍增)
- [bzoj3123][洛谷P3302] [SDOI2013]森林(树上主席树+倍增lca+启发式合并)
- 【模板】【LCA】【树上倍增】
- [NOIP2013]货车运输 D1 T3 kruscal最大生成树+树上倍增lca+rmq
- 树形结构转换线性结构的方法(lca倍增)
- noip运输计划(倍增lca,树上差分)
- Codeforces Round #294 (Div. 2) E 树上倍增lca
- [算法]树上倍增求LCA
- [HNOI2014][BZOJ3572] 世界树|虚树|树上倍增LCA|树型dp|dfs序
- BZOJ 2815 浅谈有向图必经点问题总结+拓扑序+倍增LCA灭绝树求法
- 【NOIP2016提高组T2】天天爱跑步-倍增LCA+树上差分
- HDU 4822 Tri-war(LCA树上倍增)(2013 Asia Regional Changchun)
- 树上倍增求LCA及例题