树上方法总结 LCA 树上倍增 树链剖分 树的直径 重心

树是一种非常有条理的数据结构,从很多方面来看都是这样。每一个节点有其唯一确定的父亲节点,也有唯一确定的边权或点权。因为没有环,树上可以方便地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一边入栈,遇到访问过的节点就弹栈。由此又可以想出很多方法把图转成一颗树。
有的时候,原图的解答树直接去掉所有返祖边就是可用的树。有的时候,把环上的点都连到其上深度最浅的点就可以。
缩成树以后可以照样用倍增和树链剖分,特判一下在一个环上的情况就行。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 55
    评论
评论 55
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值