树链 剖分

重链剖分介绍

重链剖分是将一棵树剖分成若干条重链的一种算法。在了解重链剖分前,我们需要知道一些基本的概念:

  1. 重儿子:一个结点的所有儿子中,size最大的那个(若有多 个任选一个)。
  2. 重链:由链顶和连续的重儿子所组成的链称为重链。
  3. 链顶:一条重链中深度最小的点。

例如在右图中,1的重儿子为2,5的重儿子为9,其中1,3,4,7,8,11,12都是链顶,1->2->5->9->10->13是一条重链,8自身构成一条重链。

重链剖分的性质

  1. 任意一条简单路径至多经过logn条重链,实际上少得多。因为每次切换一条重链,会使得子树大小减小一半,于是对于任意一条路径“重链切换次数”不会超过logn次。
  2. 每个节点都属于且仅属于一条重链。
  3. 重链会将整棵树完全部分。
  4. 在一条重链上,其结点的dfs序是自上而下连续的。

如何找到重链

top[x] 表示x节点的链顶节点编号,只要处理出了这个数组,就认为是完成剖分了。

通过两次dfs来找重链:

第一次dfs处理出重儿子、子树大小、节点深度、父节点等基础信息;

第二次dfs计算出所有结点的链顶和dfn,具体流程请看代码。

void dfs1(int x, int father) {
    fa[x] = father; // 记录父节点
    sz[x] = 1; // 子树大小
    dep[x] = dep[fa[x]] + 1; // 节点深度
    for(const auto &y : g[x]) {
        if(y == fa[x]) continue;
        dfs1(y, x);
        sz[x] += sz[y];
        if(sz[y] > sz[son[x]]) son[x] = y;
    }
}

// 计算得到top
int tot = 0;
void dfs2(int x, int t) {
    top[x] = t; // t表示链顶
    dfn[x] = ++ tot;
    idx[dfn[x]] = x;
    if(son[x]) dfs2(son[x], t); // 重儿子的链顶继承
    for(const auto &y : g[x]) {
        if(y == fa[x] || y == son[x]) continue;
        dfs2(y, y); // 轻边,重新开一个链顶
    }
}

重链剖分的应用

求LCA

  1. 先跳到同一条重链上,即每次让top深度大的往上跳,跳到top的father位置。这个过程也是重链剖分的核心操作。
  2. 当两个点到同一条重链后,深度较小的就是lca。
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] ? u : v;
}

维护路径信息(通常结合线段树)

  1. 每次将top深度较大的链维护,然后再往上跳,直到两个点在同一条重链上。
  2. 当两点在同一条重链上时直接维护信息即可。
ll u, v, w; cin >> u >> v >> w;
while(top[u] != top[v]) {
    if(dep[top[u]] < dep[top[v]]) swap(u, v);
    st.update(dfn[top[u]], dfn[u], w);
    u = fa[top[u]];
}
if(dep[u] < dep[v]) swap(u, v);
st.update(dfn[v], dfn[u], w);

例子
在这里插入图片描述

#include <bits/stdc++.h>
using namespace std;
#define asd(i,a,b) for(int i=a;i<=b;i++)
#define int long long
const int N = 1e6 + 5;
int dfn[N]; int idx[N];
int n; vector<int>b[N];
int a[N]; int tot;
int fa[N]; int top[N]; int son[N]; int dep[N]; int sz[N];
struct tree {//线段树模板
    int t[N]; int lz[N];
    void pushup(int o) {
        t[o] = t[o << 1] + t[o << 1 | 1];
    }
    void build(int s = 1, int e = n, int o = 1) {
        if (s == e) {
            t[o] = a[idx[s]];
            return;
        }
        int mid = (s + e) >> 1;
        build(s, mid, o << 1);
        build(mid + 1, e, o << 1 | 1);
        pushup(o);
    }
    void pushdown(int s, int e, int o) {
        if (lz[o]) {
            int mid = (s + e) >> 1;
            t[o << 1] += lz[o] * (mid - s + 1);
            t[o << 1 | 1] += lz[o] * (e - mid);
            lz[o << 1] += lz[o];
            lz[o << 1 | 1] += lz[o];
            lz[o] = 0; 
        }
    }
    void updata(int l, int r, int val, int s = 1, int e = n, int o = 1) {
        if (l <= s && e <= r) {
            t[o] += (e - s + 1) * val;
            lz[o] += val;
            return;
        }
        int mid = (s + e) >> 1;
        pushdown(s, e, o);
        if (mid >= l) updata(l, r, val, s, mid, o << 1);
        if (mid + 1 <= r) updata(l, r, val, mid + 1, e, o << 1 | 1);
        pushup(o);
    }
    int query(int l, int r, int s = 1, int e = n, int o = 1) {
        if (l <= s && e <= r) {
            return t[o];
        }
        pushdown(s, e, o);
        int res = 0;
        int mid = (s + e) >> 1;
        if (mid >= l) res += query(l, r, s, mid, o << 1);
        if (mid + 1 <= r) res += query(l, r, mid + 1, e, o << 1 | 1);
        return res;
    }
}st;
void dfs1(int x, int pre)//重链模板
{
    fa[x] = pre;
    dep[x] = dep[pre] + 1;
    sz[x] = 1;
    for (int y : b[x])
    {
        if (y == pre)continue;
        dfs1(y, x);
        sz[x] += sz[y];
        if (sz[y] > sz[son[x]])son[x] = y;
    }
}
void dfs2(int x, int t)
{
    top[x] = t;
    dfn[x] = ++tot;
    idx[dfn[x]] = x;
    if (son[x])dfs2(son[x], t);
    for (int y : b[x])
    {
        if (y == fa[x] || y == son[x])continue;
        dfs2(y, y);
    }
}

signed main()
{
    ios::sync_with_stdio(0), cin.tie(0), cin.tie(0);
    cin >> n;
    asd(i, 1, n)cin >> a[i];
    asd(i, 1, n - 1)
    {
        int u, v; cin >> u >> v;
        b[u].push_back(v);
        b[v].push_back(u);
    }
    dfs1(1, 0); dfs2(1, 1);
    st.build();
    int q; cin >> q;
    while (q--)
    {
        int op; cin >> op;
        if (op == 1) {
            int u, v, w; cin >> u >> v >> w;
            while (top[u] != top[v]) {
                if (dep[top[u]] < dep[top[v]]) swap(u, v);
                st.updata(dfn[top[u]], dfn[u], w);
                u = fa[top[u]];
            }
            if (dep[u] < dep[v]) swap(u, v);
            st.updata(dfn[v], dfn[u], w);
        }
        else if (op == 2) {
            int u, w; cin >> u >> w;
            st.updata(dfn[u], dfn[u] + sz[u] - 1, w);
        }
        else if (op == 3) {
            int u, v; cin >> u >> v; int ans = 0;
            while (top[u] != top[v]) {
                if (dep[top[u]] < dep[top[v]]) swap(u, v);
                ans += st.query(dfn[top[u]], dfn[u]);
                u = fa[top[u]];
            }
            if (dep[u] < dep[v]) swap(u, v);
            ans += st.query(dfn[v], dfn[u]);
            cout<<ans<<endl;
        }
        else {
            int u; cin >> u;
            cout << st.query(dfn[u], dfn[u] + sz[u] - 1) << endl;
        }

    }
    return 0;
}

在这里插入图片描述

#include <bits/stdc++.h>
using namespace std;
#define int long long  // 使用long long类型
const int N = 1e6 + 5;  // 定义最大节点数

// 树链剖分相关数组
int dfn[N];      // DFS序编号
int idx[N];      // 编号对应的原节点
int n, q;        // 节点数和查询数
vector<pair<int, int>> b[N];  // 邻接表存储树结构,pair<节点,边权>
int a[N];        // 节点初始权值
int tot;         // DFS序计数器
int c[N];        // 边权数组(按DFS序存储)
int fa[N];       // 父节点
int top[N];      // 链顶节点
int son[N];      // 重儿子
int dep[N];      // 节点深度
int sz[N];       // 子树大小

// 线段树结构体
struct tree {
    struct node {
        int num, sum;  // num存储点权,sum存储边权
    } t[N * 4];
    int lznum[N * 4], lzsum[N * 4];  // 点权和边权的懒标记

    // 上推更新节点信息
    void pushup(int o) {
        t[o].num = t[o << 1].num ^ t[o << 1 | 1].num;  // 点权异或和
        t[o].sum = t[o << 1].sum ^ t[o << 1 | 1].sum;  // 边权异或和
    }

    // 构建线段树
    void build(int s = 1, int e = n, int o = 1) {
        lznum[o] = lzsum[o] = 0;  // 初始化懒标记
        if (s == e) {
            t[o].num = a[idx[s]];   // 初始化点权
            t[o].sum = c[s];        // 初始化边权
            return;
        }
        int mid = (s + e) >> 1;
        build(s, mid, o << 1);      // 构建左子树
        build(mid + 1, e, o << 1 | 1);  // 构建右子树
        pushup(o);                  // 更新当前节点
    }

    // 下推懒标记
    void pushdown(int s, int e, int o) {
        if (lznum[o] || lzsum[o]) {
            int mid = (s + e) >> 1;
            if (lznum[o]) {  // 处理点权懒标记
                t[o << 1].num ^= ((mid - s + 1) % 2) * lznum[o];  // 左子节点更新
                t[o << 1 | 1].num ^= ((e - mid) % 2) * lznum[o]; // 右子节点更新
                lznum[o << 1] ^= lznum[o];      // 下推懒标记
                lznum[o << 1 | 1] ^= lznum[o];
                lznum[o] = 0;                   // 清空当前懒标记
            }
            if (lzsum[o]) {  // 处理边权懒标记
                t[o << 1].sum ^= ((mid - s + 1) % 2) * lzsum[o];
                t[o << 1 | 1].sum ^= ((e - mid) % 2) * lzsum[o];
                lzsum[o << 1] ^= lzsum[o];
                lzsum[o << 1 | 1] ^= lzsum[o];
                lzsum[o] = 0;
            }
        }
    }

    // 区间更新
    void update(int l, int r, int val, int s, int e, int o, int type) {
        if (l > r) return;  // 无效区间直接返回
        if (l <= s && e <= r) {  // 完全包含区间
            if (type == 0) {  // 更新点权
                t[o].num ^= ((e - s + 1) % 2) * val;  // 根据区间长度奇偶性决定是否异或
                lznum[o] ^= val;  // 设置懒标记
            }
            else {  // 更新边权
                t[o].sum ^= ((e - s + 1) % 2) * val;
                lzsum[o] ^= val;
            }
            return;
        }
        pushdown(s, e, o);  // 下推懒标记
        int mid = (s + e) >> 1;
        if (l <= mid) update(l, r, val, s, mid, o << 1, type);  // 更新左子树
        if (r > mid) update(l, r, val, mid + 1, e, o << 1 | 1, type);  // 更新右子树
        pushup(o);  // 上推更新
    }

    // 区间查询
    int query(int l, int r, int s, int e, int o, int type) {
        if (l > r) return 0;  // 无效区间返回0
        if (l <= s && e <= r) {  // 完全包含区间
            return type == 0 ? t[o].num : t[o].sum;  // 返回点权或边权
        }
        pushdown(s, e, o);  // 下推懒标记
        int mid = (s + e) >> 1;
        int res = 0;
        if (l <= mid) res ^= query(l, r, s, mid, o << 1, type);  // 查询左子树
        if (r > mid) res ^= query(l, r, mid + 1, e, o << 1 | 1, type);  // 查询右子树
        return res;
    }
} st;

// 第一次DFS:计算父节点、深度、子树大小和重儿子
void dfs1(int x, int pre) {
    fa[x] = pre;            // 记录父节点
    dep[x] = dep[pre] + 1;  // 计算深度
    sz[x] = 1;              // 初始化子树大小
    son[x] = 0;             // 初始化重儿子
    for (auto y : b[x]) {   // 遍历所有子节点
        if (y.first == pre) continue;  // 跳过父节点
        dfs1(y.first, x);              // 递归处理子节点
        sz[x] += sz[y.first];          // 累加子树大小
        if (sz[y.first] > sz[son[x]]) son[x] = y.first;  // 更新重儿子
    }
}

// 第二次DFS:进行树链剖分
void dfs2(int x, int t) {
    top[x] = t;             // 记录链顶
    dfn[x] = ++tot;         // 分配DFS序编号
    idx[dfn[x]] = x;        // 记录编号对应的原节点
    if (son[x]]) {          // 如果有重儿子
        dfs2(son[x], t);    // 优先处理重儿子(保持链连续)
        for (auto y : b[x]) {  // 处理轻儿子
            if (y.first == fa[x] || y.first == son[x]) continue;
            dfs2(y.first, y.first);  // 轻儿子作为新链的起点
        }
    }
    // 存储边权(轻边和重边)
    for (auto y : b[x]) {
        if (y.first == fa[x]) continue;  // 跳过父节点
        c[dfn[y.first]] = y.second;       // 将边权存储到子节点的DFS序位置
    }
}

signed main() {
    ios::sync_with_stdio(false);  // 加速输入输出
    cin.tie(0);
    cout.tie(0);

    // 输入树的基本信息
    cin >> n >> q;
    for (int i = 1; i <= n; ++i) cin >> a[i];  // 节点权值
    for (int i = 1; i < n; ++i) {  // 树的边
        int u, v, c;
        cin >> u >> v >> c;
        b[u].push_back({ v, c });
        b[v].push_back({ u, c });
    }

    // 树链剖分预处理
    dfs1(1, 0);  // 第一次DFS
    dfs2(1, 1);  // 第二次DFS
    st.build();  // 构建线段树

    // 处理查询
    while (q--) {
        int op, u, v, w;
        cin >> op;
        if (op == 1) {  // 操作1:路径边权异或
            cin >> u >> v >> w;
            while (top[u] != top[v]) {  // 当不在同一条链上
                if (dep[top[u]] < dep[top[v]]) swap(u, v);  // 选择深度较大的链
                st.update(dfn[top[u]], dfn[u], w, 1, n, 1, 1);  // 更新当前链的边权
                u = fa[top[u]];  // 跳到链顶的父节点
            }
            if (u == v) continue;  // 相同节点不需要处理
            if (dep[u] < dep[v]) swap(u, v);
            st.update(dfn[v] + 1, dfn[u], w, 1, n, 1, 1);  // 更新最后一段的边权(不包括LCA)
        }
        else if (op == 2) {  // 操作2:路径点权异或
            cin >> u >> v >> w;
            while (top[u] != top[v]) {
                if (dep[top[u]] < dep[top[v]]) swap(u, v);
                st.update(dfn[top[u]], dfn[u], w, 1, n, 1, 0);  // 更新当前链的点权
                u = fa[top[u]];
            }
            if (dep[u] < dep[v]) swap(u, v);
            st.update(dfn[v], dfn[u], w, 1, n, 1, 0);  // 更新最后一段的点权(包括LCA)
        }
        else if (op == 3) {  // 操作3:查询路径点权异或和
            cin >> u >> v;
            int ans = 0;
            while (top[u] != top[v]) {
                if (dep[top[u]] < dep[top[v]]) swap(u, v);
                ans ^= st.query(dfn[top[u]], dfn[u], 1, n, 1, 0);  // 查询当前链的点权异或和
                u = fa[top[u]];
            }
            if (dep[u] < dep[v]) swap(u, v);
            ans ^= st.query(dfn[v], dfn[u], 1, n, 1, 0);  // 查询最后一段的点权异或和
            cout << ans << '\n';
        }
        else if (op == 4) {  // 操作4:查询节点权值与相邻边权异或和
            cin >> u;
            int ans = st.query(dfn[u], dfn[u], 1, n, 1, 0);  // 节点权值
            for (auto y : b[u]) {  // 遍历所有相邻边
                if (y.first == fa[u]) {  // 父节点方向的边
                    ans ^= st.query(dfn[u], dfn[u], 1, n, 1, 1);  // 查询边权
                }
                else {  // 子节点方向的边
                    ans ^= st.query(dfn[y.first], dfn[y.first], 1, n, 1, 1);
                }
            }
            cout << ans << '\n';
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值