树剖学习笔记(一)

震惊!wyj 的线段树模板居然是错的!
这个是 线段树模板


Luogu P3384 【模板】树链剖分

如题,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z
操作2: 格式: 2 x y 表示求树从x到y结点最短路径上所有节点的值之和
操作3: 格式: 3 x z 表示将以x为根节点的子树内所有节点值都加上z
操作4: 格式: 4 x 表示求以x为根节点的子树内所有节点值之和

树链剖分(二)——By zxn

考虑用一些数据结构,但是显然这里线段树什么的是不合理的,因为它不是一条链。
但是我们可以把一棵树砍成几条链,连起来组成一条长链。
这就是树链剖分的基本原理。

剖的方法: 轻重边划分
size[x] 定义为以 x 为子树的节点个数。
重儿子:当前节点下 size 值最大的儿子(若有 size 相等则随便连一个)。
重边:连接当前节点和其重孩子的边。
轻边:连接当前节点和除了重孩子的边。

需要使用到的变量有:

tim // 时间戳,对应的当前的链上的位置
size[] // 当前节点的 size 数
son[] // 记录当前节点的重儿子
fa[] // 记录当前节点的父亲
dep[] // 当前节点的深度
top[] // 当前重链的最顶部(距离根节点最近的点)
tid[] // 到达当前节点的时间,即当前节点在线段树中的编号
Rank[] // 当前时间对应着哪个节点,即当前节点在线段树中的编号对应在原树中的编号

在树链剖分时,首先我们要确定每一个节点的 size 从而确定它的重儿子,然后连成重边。
之后我们再把重边都连起来拉成重链。
这个过程需要两遍 dfs

int fa[N], dep[N], size[N], son[N];
void dfs1(int u, int f, int depth) {
    fa[u] = f;
    dep[u] = depth;
    size[u] = 1;
    for(int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if(v != fa[u]) {
            dfs1(v, u, depth + 1);
            size[u] += size[v];
            if(son[u] == -1 || size[son[u]] < size[v])
                son[u] = v;
        }
    }
}
int tim = 0;
int top[N], tid[N], Rank[N];
void dfs2(int u, int tp) {
    top[u] = tp;
    tid[u] = ++ tim;
    Rank[tim] = u;
    if(son[u] == - 1) return ;
    dfs2(son[u], tp);
    for(int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if(v != fa[u] && v != son[u])
            dfs2(v, v);
    }
}

tid && Rank

tim 是访问这个节点的时间。
树链剖分是按照时间来建造整棵树的,tid[x] = tim,tid[x] 里存的是访问到 x 这个节点的时间,之后我们根据这个 tid[] 值建树,所以这个 tid[x] 就是 x 在线段树中的编号。
而 Rank[tid[x]] 就是 x 在线段树中的编号,对应在原树中的编号。
所以有:
x = Rank[tid[x]];


区间修改的思路:
x,y 之间的路径进行区间修改。

void chage(){
    while(x,y不在一条重链上){
        dep[top[x]] < dep[top[y]] ? swap(x,y) : 1;
        对x---top[x]所在的链进行修改;
        x = fa[top[x]];
    }
    对x---y所在的链进行修改。
}

以 x 为根节点的子树内所有节点的 tim 值一定是连续的,所以我们在对以 x 为根节点的子树内所有节点进行操作时,区间即为 (tid[x],tid[x]+size[x]1)


通常情况下,我们会将树中的所有链剖分出来,存在线段树里进行操作,那么这样整个程序的复杂度大致 O(nlog2n)

注意赋叶子结点的值的时候和线段树不一样,应该赋的值是 a[Rank[l]]

// 记得建树!!! 
#include <bits/stdc++.h>
#define ll long long

using namespace std;
const int N = 1e5 + 5;

struct Edge {
    int to, next, w;
}e[N << 1];

int cnt = 0;
int head[N];
int n, m, r, p;

void add(int x, int y) {
    e[++ cnt].to = y;
    e[cnt].next = head[x];
    head[x] = cnt;
}

//--------

int fa[N], dep[N], size[N], son[N];
void dfs1(int u, int f, int depth) {
    fa[u] = f;
    dep[u] = depth;
    size[u] = 1;
    for(int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if(v != fa[u]) {
            dfs1(v, u, depth + 1);
            size[u] += size[v];
            if(son[u] == -1 || size[son[u]] < size[v])
                son[u] = v;
        }
    }
}

int tim = 0;
int top[N], tid[N], Rank[N];
void dfs2(int u, int tp) {
    top[u] = tp;
    tid[u] = ++ tim;
    Rank[tim] = u;
    if(son[u] == - 1) return ;
    dfs2(son[u], tp);   
    for(int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if(v != fa[u] && v != son[u])
            dfs2(v, v);
    }
}

//------

struct node {
    ll sum, lazy;
}tree[N << 2];

ll a[N];

void push_up(int i) {
    tree[i].sum = tree[i << 1].sum + tree[i << 1 | 1].sum; 
}

void build(int i, int l, int r) {
    tree[i].lazy = 0; // lazy 初值记得赋值为 0 
    if(l == r) tree[i].sum = a[Rank[l]]; // 这里赋值的时候和线段树不大一样要注意
    else {
        ll mid = (l + r) >> 1;
        build(i << 1, l, mid);
        build(i << 1 | 1, mid + 1, r);
        push_up(i);
    }
}

void push_down(int i, int l, int r) {
    if(tree[i].lazy) {
        tree[i << 1].lazy += tree[i].lazy;
        tree[i << 1 | 1].lazy += tree[i].lazy;
        ll mid = (l + r) >> 1;
        tree[i << 1].sum += tree[i].lazy * (mid - l + 1);
        tree[i << 1 | 1].sum += tree[i].lazy * (r - mid);
        tree[i].lazy = 0;
    }
}

void update(int i, int l, int r, int ql, int qr, int val) {
    if(ql > r || qr < l) return ;
    if(ql <= l && r <= qr) {
        tree[i].lazy += val;
        tree[i].sum += (r - l + 1) * val;
        return ;
    }
    push_down(i, l, r);
    ll mid = (l + r) >> 1;
    update(i << 1, l, mid, ql, qr, val);
    update(i << 1 | 1, mid + 1, r, ql, qr, val);
    push_up(i);
}

ll query(int i, int l, int r, int ql, int qr) {
    if(ql > r || qr < l) return 0;
    if(ql <= l && r <= qr) return tree[i].sum;
    push_down(i, l, r);
    ll mid = (l + r) >> 1;
    return (query(i << 1, l, mid, ql, qr) + query(i << 1 | 1, mid + 1, r, ql, qr)) %  p;    
}

//-------

void add1() {
    int u, v, w;
    scanf("%d%d%d", &u, &v, &w);
    while(top[u] != top[v]) {
        if(dep[top[u]] < dep[top[v]]) swap(u, v);
        update(1, 1, n, tid[top[u]], tid[u], w);
        u = fa[top[u]];
    }
    if(dep[u] < dep[v]) swap(u, v);
    update(1, 1, n, tid[v], tid[u], w);
}

void query1() {
    int u, v, ans = 0;
    scanf("%d%d", &u, &v);
    while(top[u] != top[v]) {
        if(dep[top[u]] < dep[top[v]]) swap(u, v);
        (ans += query(1, 1, n, tid[top[u]], tid[u])) %= p;
        u = fa[top[u]];
    }
    if(dep[u] < dep[v]) swap(u, v);
    (ans += query(1, 1, n, tid[v], tid[u])) %= p;
    printf("%d\n", ans);
}

void add2() {
    int x, w;
    scanf("%d%d", &x, &w);
    update(1, 1, n, tid[x], tid[x] + size[x] - 1, w);
}

void query2() {
    int x;
    scanf("%d", &x);
    printf("%d\n", query(1, 1, n, tid[x], tid[x] + size[x] - 1) % p);
}

//-------

int main() {
    memset(head, 0, sizeof(head));
    memset(son, -1, sizeof(son));
    scanf("%d%d%d%d", &n, &m, &r, &p);
    for(int i = 1; i <= n; i ++)
        scanf("%d", &a[i]);
    for(int i = 1; i < n; i ++) {
        int x, y;
        scanf("%d%d", &x, &y);
        add(x, y), add(y, x);
    }
    dfs1(r, 0, 1);
    dfs2(r, r);
    build(1, 1, n);
    for(int i = 1; i <= m; i ++) {
        int q;
        scanf("%d", &q);
        if(q == 1) add1();
        else if(q == 2) query1();
        else if(q == 3) add2();
        else query2();
    }
    return 0;
}

LCA

树剖还可以用来求 LCA

int lca(int a, int b) {
    while(top[a] != top[b]) {
        if(dep[top[a]] > dep[top[b]]) a = fa[top[a]];
        else b = fa[top[b]];    
    }
    return (dep[a] < dep[b]) ? a : b;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值