树链剖分学习&总结

前言

  1. 系统的,针对性的,学习一些铜牌算法,争取达到单挑铜牌的水平。
  2. 参考博客:oi-wiki
  3. 最后的训练时间了,那就做最重要的事情:把基础打好,我觉得针对性训练&一两年的积累,就算佛系了一些,大四也还是可以达到单挑铜首最好是银中上的水平(前提是还能一周训练3*5小时+)
    1. 习惯先想清楚再行动:做一道题,最好是尽量多的把各个细节想清楚之后再行动,除非是那种想都不用想的细节,可以直接拿起键盘就写。
    2. 要把那些铜牌银牌算法都了解一下:理解算法思想&模板总结。
      1. 比如树链剖分,抓住关键:两个dfs&一棵树的dfn序相同&线段树维护。另外,求两点间的xx,跳点不同于倍增求LCA,但是很相似

树链剖分的思想及能解决的问题

  1. 把整棵树剖分成若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。
  2. 树链剖分有多种形式,如重链剖分,长链剖分等,大多数形况下,“树链剖分”都指“重链剖分”。
  3. 重链剖分可以将树上的任意一条路径划分成不超过O(logn)条连续的链
  4. 另外,重链剖分还可以保证划分出的每条链上的节点DFS序连续,甚至每棵子树上的节点DFS序连续因此可以方便的用一些维护序列的数据结构(比如线段树)来维护树上路径的信息。如:
    1. 修改树上两点之间的路径上所有点的值。
    2. 查询树上两点之间的路径上节点权值的和/极值/其它(在序列上可以用数据结构维护,便于合并的信息)
  5. 除了配合数据结构来维护树上路径信息,树剖还可以用来O(logn)(且常数较小)地来求LCA。在某些题目中,还可以利用它其性质来灵活的运用树剖
    1. 因为上面这句话重链剖分可以将树上的任意一条路径划分成不超过O(logn)条连续的链
  6. 总结能解决的问题
    1. O(logn)求LCA
    2. 维护树上路径的信息,比如修改某个点,查询和/极值、以及其他能用线段树维护的信息(比如删点&维护子树大小)。

重链剖分的操作

知道重链剖分的操作,其他xx链剖分的操作也就很容易写出来了

一些定义

  1. 重子节点:其子节点中子树最大的子节点。如果有多个子树最大的子节点,取其一。如果没有子节点,就无重子节点。
  2. 轻子节点:剩余的所有子节点。
  3. 重边/轻边:从一个节点到其重子节点的边为重边,否则叫轻边。
  4. 重链:若干条首尾衔接的重边构成重链。把落单的节点也当作重链,那么整棵树就被剖分成若干条重链
    在这里插入图片描述

代码实现

  1. 树剖的实现分两个DFS的过程(其他就是线段树等维护信息的东西了)。
    1. 第一个DFS记录每个节点的父节点(father)、深度(deep)、子树大小(size)、重子节点(hson)。
    2. 第二个DFS记录所在链的链顶(top,应初始化为节点本身)、重边优先遍历时的DFS序(dfn)、DFS序对应的节点编号(rank)。
  2. 给出一些定义:
    在这里插入图片描述
  3. 我们进行两遍DFS预处理出这些值,其中第一次DFS求出 f a ( x ) , d e p ( x ) , s i z ( x ) , s o n ( x ) fa(x),dep(x),siz(x),son(x) fa(x),dep(x),siz(x),son(x),第二次DFS求出 t o p ( x ) , d f n ( x ) , r n k ( x ) top(x),dfn(x),rnk(x) top(x),dfn(x),rnk(x)
//这里根节点的父亲fa是0,深度dep为1
void dfs1(int x, int fu) {
    
    
    //处理叶子节点可以放在这里
    son[x] = -1;  //没有重儿子
    siz[x] = 1;   //大小为1
    for (auto i : g[x]) {
    
    
        if (i == fu) continue;
        dep[i] = dep[x] + 1;  //深度
        fa[i] = x;            //父亲
        dfs1(i, x);
        siz[x] += siz[i];
        if (son[x] == -1 || siz[i] > siz[son[x]]) son[x] = i;  //更新重儿子
    }
}
void dfs2(int x, int t) {
    
    
    //遍历到这个点的时候处理这个点
    top[x] = t;                // t为重链顶端,x属于重链t
    dfn[x] = ++cnt;            //重链优先的dfs序
    rnk[cnt] = x;              // dfs序为cnt的节点为x
    if (son[x] == -1) return;  //没有重儿子
    dfs2(son[x], t);
    for (auto i : g[x]) {
    
    
        if (i == fa[x]) continue;
        if (i == son[x]) continue;  //重儿子已经提前遍历过了
        dfs2(i, i);                 //自己是自己的重链顶端
    }
}

重链剖分的性质

  1. 树上每个节点都属于且仅属于一条重链。
  2. 所有的重链将整棵树完全剖分
  3. 在剖分时重边优先遍历,最后树的DFN序上,重链内的DFN序是连续的。按DFN排序后的序列即为剖分后的链
  4. 一棵子树内的DFN序是连续的。
  5. 可以发现,当我们向下经过一条轻边时,所在子树的大小至少会除以2。因此,对于树上的任意一条路径,把它拆分成从lca分别向两边往下走,分别最多走O(logn)次,因此,树上的每条路径都可以被拆分成不超过O(logn)条重链。

常见应用

路径上维护

  1. 比如用树链剖分求树上两点路径权值和。
    1. 链上的DFS序是连续的,可以使用线段树,树状数组维护。
    2. 每次选择深度交大的链往上跳,知道两点在同一条链上。
    3. 同样的跳链结构适用于维护、统计路径上的其他信息。
  2. 另外线段树能维护的东西都能求
  3. 求LCA的操作算是路径上维护的最基本的操作

子树维护

  1. 比如将以 x 为根的子树的所有节点的权值增加 v 。
    1. 在DFS搜索的时候,子树中的节点的DFS序是连续的。
    2. 每一个节点记录bottom表示所在子树连续区间末端的结点(ps:其实好像不用特意记录,查询子树大小即可,因为连续)。
    3. 这样就把子树信息转化为连续的一段区间信息。

求最近公共祖先

  1. 不断往上跳重链,当跳到同一条重链上时,深度较小的结点即为LCA。
  2. 向上跳重链时需要先跳所在重链顶端深度较大的那个。
int lca(int u, int v) {
    
    
    while (top[u] != top[v]) {
    
    
        if (dep[top[u]] > dep[top[v]])
            u = fa[top[u]];
        else
            v = fa[top[v]];
    }
    return (dep[u] > dep[v]) ? v : u;
}

了解:怎么有理有据卡树剖

在这里插入图片描述

模板例题

题目1:树的统计 LibreOJ - 10138 (单结点修改权值&查询路径最大权值&查询路径权值和)

树的统计 LibreOJ - 10138

  1. 题意
    在这里插入图片描述
  2. 数据范围
    在这里插入图片描述
  3. 题解:树链剖分之后线段树维护即可,查询路径的时候需要用到和LCA差不多的操作。每次查询时间复杂度 O ( l o g 2 n ) O(log^2n) O(log2n)(实际上重链个数很难达到O(logn)(可以用完全二叉树卡满),所以树剖在一半情况下常熟较小)。
  4. 代码
#include <bits/stdc++.h>
// #define int long long
#define lc p << 1
#define rc p << 1 | 1
#define dbg(x) cout << #x << "===" << x << endl
using namespace std;
const int N = 3e4 + 5;
const int inf = 2e9;
int n, w[N];
vector<int> g[N];
int siz[N], top[N], son[N], dep[N], fa[N], dfn[N], rnk[N], cnt;
struct SegTree {
    
    
    int sum[N << 2], mx[N << 2];
    void build(int p, int l, int r) {
    
    
        if (l == r) {
    
    
            sum[p] = mx[p] = w[rnk[l]];
            return;
        }
        int mid = (l + r) >> 1;
        build(lc, l, mid);
        build(rc, mid + 1, r);
        sum[p] = sum[lc] + sum[rc];
        mx[p] = max(mx[lc], mx[rc]);
    }
    //查询区间最大值
    int query1(int p, int l, int r, int x, int y) {
    
    
        if (l > y || r < x)
            return -inf;  //然后下面就不用判断x or y与mid的关系,直接return即可
        if (x <= l && r <= y) return mx[p];
        int mid = (l + r) >> 1;
        return max(query1(lc, l, mid, x, y), query1(rc, mid + 1, r, x, y));
    }
    //查询区间和
    int query2(int p, int l, int r, int x, int y) {
    
    
        if (l > y || r < x) return 0;
        if (x <= l && r <= y) return sum[p];
        int mid = (l + r) >> 1;
        return query2(lc, l, mid, x, y) + query2(rc, mid + 1, r, x, y);
    }
    //这里是单点修改
    //整颗树修改的话改成区间修改就ok了。裸的线段树还是熟悉的
    void update(int p, int l, int r, int x, int k) {
    
    
        if (l == r) {
    
    
            sum[p] = mx[p] = k;
            return;
        }
        int mid = (l + r) >> 1;
        if (x <= mid)
            update(lc, l, mid, x, k);
        else
            update(rc, mid + 1, r, x, k);
        sum[p] = sum[lc] + sum[rc];
        mx[p] = max(mx[lc], mx[rc]);
    }
} st;
// querymax和querysum的操作基本上完全一样
//往上跳点,和倍增求LCA不一样。
//倍增求LCA是怎样跳点来着?每次跳到相同深度,然后同时往上慢慢跳
//树链剖分的性质很重要!!!
int querymax(int x, int y) {
    
    
    int res = -inf, fx = top[x], fy = top[y];
    while (fx != fy) {
    
    
        //也是首先跳深度大的,没跳一次判断一下是否在一棵树
        if (dep[fx] >= dep[fy])
            res = max(res, st.query1(1, 1, n, dfn[fx], dfn[x])), x = fa[fx];
        else
            res = max(res, st.query1(1, 1, n, dfn[fy], dfn[y])), y = fa[fy];
        fx = top[x];
        fy = top[y];
    }
    if (dfn[x] < dfn[y])
        res = max(res, st.query1(1, 1, n, dfn[x], dfn[y]));
    else
        res = max(res, st.query1(1, 1, n, dfn[y], dfn[x]));
    return res;
}
int querysum(int x, int y) {
    
    
    int res = 0, fx = top[x], fy = top[y];
    //跳到fx==fy即在同一树链上为止
    while (fx != fy) {
    
    
        if (dep[fx] >= dep[fy])
            res += st.query2(1, 1, n, dfn[fx], dfn[x]), x = fa[fx];
        else
            res += st.query2(1, 1, n, dfn[fy], dfn[y]), y = fa[fy];
        //这里y=fa[fy]也是关键,跳到了另一条重链上了
        fx = top[x];
        fy = top[y];
    }
    //同一条重链上,dfs序小的深度小
    if (dfn[x] < dfn[y])
        res += st.query2(1, 1, n, dfn[x], dfn[y]);
    else
        res += st.query2(1, 1, n, dfn[y], dfn[x]);
    return res;
}
//这里根节点的父亲是0,深度是1
void dfs1(int x, int fu) {
    
    
    //处理叶子节点可以放在这里
    son[x] = -1;  //没有重儿子
    siz[x] = 1;   //大小为1
    for (auto i : g[x]) {
    
    
        if (i == fu) continue;
        dep[i] = dep[x] + 1;  //深度
        fa[i] = x;            //父亲
        dfs1(i, x);
        siz[x] += siz[i];
        if (son[x] == -1 || siz[i] > siz[son[x]]) son[x] = i;  //更新重儿子
    }
}
void dfs2(int x, int t) {
    
    
    //遍历到这个点的时候处理这个点
    top[x] = t;      // t为重链顶端,x属于重链t
    dfn[x] = ++cnt;  //重链优先的dfs序
    rnk[cnt] = x;    // dfs序为cnt的节点为x
    if (son[x] == -1) return;  //没有重儿子
    dfs2(son[x], t);
    for (auto i : g[x]) {
    
    
        if (i == fa[x]) continue;
        if (i == son[x]) continue;  //重儿子已经提前遍历过了
        dfs2(i, i);  //自己是自己的重链顶端
    }
}
signed main() {
    
    
    cin >> n;
    for (int i = 1, a, b; i < n; i++) {
    
    
        cin >> a >> b;
        g[a].push_back(b), g[b].push_back(a);
    }
    for (int i = 1; i <= n; i++) cin >> w[i];
    dep[1] = 1;  // 1的深度为1
    dfs1(1, 0);
    dfs2(1, 1);
    st.build(1, 1, n);
    int q, u, v;
    char op[10];
    cin >> q;
    while (q--) {
    
    
        scanf("%s%d%d", op, &u, &v);
        if (op[1] == 'H') st.update(1, 1, n, dfn[u], v);  // change
        // else
        // cout << ">>>>>>>>>>>>>>>>>>>>>>>>";
        if (op[1] == 'M') printf("%d\n", querymax(u, v));
        if (op[1] == 'S') printf("%d\n", querysum(u, v));
    }
    return 0;
}

专题训练

  1. 专题地址树链剖分练习题
  2. 总结经验
    1. 之前时间多,刷专题的时候都是先刷简单的,然后再刷难的。现在一个专题都不一定刷得完,那就首先刷能让自己收获更大的题目吧。

题目1:P4374 [USACO18OPEN]Disruption P(边权转化为点权&树链剖分)

P4374 [USACO18OPEN]Disruption P

  1. 题意:有 n n n 个农场,n-1 条双向道路(构成一棵树),长度都为1。另外有 q 条双向道路{u,v,k}分别表示 u,v结点之间有一条长度为k的双向道路。
    1. 要求输出,第 i 条线段断了之后,最短的可替代道路的长度(某条线断了之后,树分成两半,求能将它们连起来的最短道路的长度),如果不存在则输出 -1。
    2. 1 ≤ n , q ≤ 5 e 4 1\le n,q\le 5e4 1n,q5e4,另外所以道路长度都是一个至多为 1e9 的正整数。
  2. 题解
    1. 替代道路:原树中u,v路径间的线段都可以被{u,v,k}替代——树链剖分的路径操作
    2. 边转化为点:建图的时候注意一下就ok
    3. 具体的见代码,不难。
  3. 代码
#include <bits/stdc++.h>
// #define int long long
#define lc p << 1
#define rc p << 1 | 1
#define dbg(x) cout << #x << "===" << x << endl
using namespace std;
const int N = 1e5 + 5;  // 5e4*2
const int inf = 2e9;
int n, q, u, v;
vector<int> g[N];
int dep[N], son[N], siz[N], fa[N];  // dfs1要维护的
int dfn[N], rnk[N], top[N], cnt;    // dfs2要维护的
void dfs1(int x, int fu) {
    
    
    son[x] = -1;
    siz[x] = 1;
    for (auto i : g[x]) {
    
    
        if (i == fu) continue;
        dep[i] = dep[x] + 1;
        fa[i] = x;
        dfs1(i, x);
        siz[x] += siz[i];
        if (son[x] == -1 || siz[i] > siz[son[x]]) son[x] = i;
    }
}
// t表示top
void dfs2(int x, int t) {
    
    
    top[x] = t;
    dfn[x] = ++cnt;
    rnk[cnt] = x;
    if (son[x] == -1) return;  //叶子结点没有重儿子
    dfs2(son[x], t);
    for (auto i : g[x]) {
    
    
        if (i == fa[x]) continue;
        if (i == son[x]) continue;
        dfs2(i, i);
    }
}
//这一题,需要线段树维护最小值
struct SegTree {
    
    
    int mi[N << 2], tag[N << 2];
    void build(int p, int l, int r) {
    
    
        mi[p] = tag[p] = inf;
        if (l == r) return;
        int mid = (l + r) >> 1;
        build(lc, l, mid);
        build(rc, mid + 1, r);
    }
    void pushdown(int p) {
    
    
        mi[lc] = min(mi[lc], tag[p]);
        tag[lc] = min(tag[lc], tag[p]);
        mi[rc] = min(mi[rc], tag[p]);
        tag[rc] = min(tag[rc], tag[p]);
        tag[p] = inf;
    }
    //区间维护最小值
    //这一题,可以从大到小排序之后再update,就不用min
    void update(int p, int l, int r, int x, int y, int k) {
    
    
        if (x <= l && r <= y) {
    
    
            mi[p] = min(mi[p], k);
            tag[p] = min(tag[p], k);
            return;
        }
        pushdown(p);
        int mid = (l + r) >> 1;
        if (x <= mid) update(lc, l, mid, x, y, k);
        if (y > mid) update(rc, mid + 1, r, x, y, k);
        mi[p] = min(mi[lc], mi[rc]);
    }
    //单点查询最小值
    int query(int p, int l, int r, int x) {
    
    
        if (x < l || x > r) return inf;
        if (l == r) return mi[p];
        pushdown(p);
        int mid = (l + r) >> 1;
        return min(query(lc, l, mid, x), query(rc, mid + 1, r, x));
    }
} st;
void update(int x, int y, int k) {
    
    
    int fx = top[x], fy = top[y];
    while (fx != fy) {
    
    
        if (dep[fx] >= dep[fy])
            st.update(1, 1, 2 * n - 1, dfn[fx], dfn[x], k), x = fa[fx];
        else
            st.update(1, 1, 2 * n - 1, dfn[fy], dfn[y], k), y = fa[fy];
        fx = top[x];
        fy = top[y];
    }
    if (dep[x] <= dep[y])
        st.update(1, 1, 2 * n - 1, dfn[x], dfn[y], k);
    else
        st.update(1, 1, 2 * n - 1, dfn[y], dfn[x], k);
}
signed main() {
    
    
    cin >> n >> q;
    for (int i = 1; i < n; i++) {
    
    
        cin >> u >> v;
        g[u].push_back(n + i), g[n + i].push_back(u);
        g[v].push_back(n + i), g[n + i].push_back(v);
    }
    dep[1] = 1;
    dfs1(1, 0);
    dfs2(1, 1);
    st.build(1, 1, 2 * n - 1);
    // for (int i = 1; i <= n; i++) {
    
    
    // cout << ">>>" << i << " " << dfn[i] << endl;
    // }
    int k;
    while (q--) {
    
    
        scanf("%d%d%d", &u, &v, &k);
        update(u, v, k);
    }
    for (int i = 1, ans; i < n; i++) {
    
    
        ans = st.query(1, 1, 2 * n - 1, dfn[n + i]);
        if (ans == inf) ans = -1;
        printf("%d\n", ans);
    }
    return 0;
}

题目2:P2486 [SDOI2011]染色(维护路径颜色段和–相邻相同的颜色算一段)

  1. P2486 [SDOI2011]染色
  2. 题意
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  3. 题解提示
    1. 只需要在普通的维护区间和的线段树中维护区间端点的颜色即可
    2. 但是操作还是有点麻烦,还是不熟悉线段树emm(对线段树理解不够深入)
  4. 收获总结:对线段树有了更深的理解
    1. 树链剖分容易将结点序和dfs序搞混淆,要注意
    2. 在线段树查询的时候,到最后一层一定有l==xorr=y
      在这里插入图片描述
  5. 代码
// https://www.luogu.com.cn/problem/P2486
#include <bits/stdc++.h>
#define lc p << 1
#define rc p << 1 | 1
#define dbg(x) cout << #x << "===" << x << endl
using namespace std;
const int N = 1e5 + 5;
int n, m, w[N];
vector<int> g[N];
int dep[N], siz[N], fa[N], son[N];
int cnt, top[N], dfn[N], rnk[N];
void dfs1(int x, int fu) {
    
    
    son[x] = -1;
    siz[x] = 1;
    for (auto i : g[x]) {
    
    
        if (i == fu) continue;
        dep[i] = dep[x] + 1;
        fa[i] = x;
        dfs1(i, x);
        siz[x] += siz[i];
        if (son[x] == -1 || siz[i] > siz[son[x]]) son[x] = i;
    }
}
void dfs2(int x, int t) {
    
    
    top[x] = t;
    dfn[x] = ++cnt;
    rnk[cnt] = x;
    if (son[x] == -1) return;
    dfs2(son[x], t);
    for (auto i : g[x]) {
    
    
        if (i == fa[x]) continue;
        if (i == son[x]) continue;
        dfs2(i, i);
    }
}
int Lc, Rc;
//看起来复杂,实际上就比普通维护区间和线段树多维护了区间左右颜色
struct SegTree {
    
    
    // sum[p]表示区间p的颜色段数
    // if tag[p]>0:表示区间p都修改成颜色tag[p]
    int sum[N << 2], tag[N << 2], L[N << 2],
        R[N << 2];  // L[p],R[p]表示区间p的左右端颜色
    void pushup(int p) {
    
    
        L[p] = L[lc], R[p] = R[rc];
        sum[p] = sum[lc] + sum[rc];
        if (R[lc] == L[rc]) sum[p]--;
    }
    void build(int p, int l, int r) {
    
    
        if (l == r) {
    
    
            L[p] = R[p] = w[rnk[l]];
            sum[p] = 1;
            return;
        }
        int mid = (l + r) >> 1;
        build(lc, l, mid);
        build(rc, mid + 1, r);
        pushup(p);
    }
    void pushdown(int p) {
    
    
        if (tag[p]) {
    
    
            sum[lc] = sum[rc] = 1;
            L[lc] = R[lc] = L[rc] = R[rc] = tag[p];
            tag[lc] = tag[rc] = tag[p];
            tag[p] = 0;
        }
    }
    //维护区间左右颜色,即可维护区间和
    void update(int p, int l, int r, int x, int y, int k) {
    
    
        if (x <= l && r <= y) {
    
    
            sum[p] = 1;
            tag[p] = k;
            L[p] = R[p] = k;  //这款线段树,只需要多维护L[p]和R[p]
            return;
        }
        pushdown(p);
        int mid = (l + r) >> 1;
        if (mid >= x) update(lc, l, mid, x, y, k);
        if (mid < y) update(rc, mid + 1, r, x, y, k);
        pushup(p);
    }
    //?这里应该还要修改一下
    int query(int p, int l, int r, int x, int y) {
    
    
        if (x <= l && r <= y) {
    
    
            if (l == x) Lc = L[p];
            if (r == y) Rc = R[p];
            //一般都是l<x&&y<r,只有可能在边界l==x||r==y
            return sum[p];
        }
        pushdown(p);
        int res = 0;
        int mid = (l + r) >> 1;
        if (mid >= x) res += query(lc, l, mid, x, y);
        if (mid < y) res += query(rc, mid + 1, r, x, y);
        if (mid >= x && mid < y) res -= (R[lc] == L[rc]);
        return res;
    }
} st;
void add(int x, int y, int k) {
    
    
    while (top[x] != top[y]) {
    
    
        if (dep[top[x]] < dep[top[y]]) swap(x, y);
        st.update(1, 1, n, dfn[top[x]], dfn[x], k);
        x = fa[top[x]];  //交换之后只对 x 操作,nice!
    }
    if (dep[x] > dep[y]) swap(x, y);
    st.update(1, 1, n, dfn[x], dfn[y], k);
}
int ask(int x, int y) {
    
    
    int res = 0, pre1 = -1, pre2 = -1;
    // dbg(1);
    while (top[x] != top[y]) {
    
    
        if (dep[top[x]] < dep[top[y]]) swap(x, y), swap(pre1, pre2);
        res += st.query(1, 1, n, dfn[top[x]], dfn[x]);
        if (Rc == pre1) res--;  // Rc即为dfn[x]的颜色
        pre1 = Lc;              //只操作 x 是有很多好处的
        x = fa[top[x]];
    }
    if (dep[x] > dep[y]) swap(x, y), swap(pre1, pre2);
    res += st.query(1, 1, n, dfn[x], dfn[y]);
    if (Lc == pre1) res--;
    if (Rc == pre2) res--;
    return res;
}
signed main() {
    
    
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> w[i];
    for (int i = 1; i < n; i++) {
    
    
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dep[1] = 1;
    dfs1(1, 0);
    dfs2(1, 1);
    st.build(1, 1, n);
    char ch;
    int a, b, c;
    while (m--) {
    
    
        cin >> ch;
        if (ch == 'C') {
    
    
            scanf("%d%d%d", &a, &b, &c);
            add(a, b, c);
        } else {
    
    
            scanf("%d%d", &a, &b);
            printf("%d\n", ask(a, b));
        }
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/I_have_a_world/article/details/119741169
今日推荐