树链剖分浅析——(板子+[NOI2015]软件包管理器)

一.先知其用

树链剖分有两个用途:
①求树上节点x到节点y的路径上的点权和
②改变树上节点x到节点y的路径上所有点的点权
但是,这两个用途不都可以用LCA实现吗?
是的,但是我们要求的是在线的修改和查询,LCA这种东西除非你敢修改一次就重新建一次LCA,这时间一下子就飙上去了。
所以,这正是我们学树链剖分的意图。

二.预备知识

1.预备概念

重儿子:父亲结点的所有儿子结点中,子树拥有结点数最多的结点。
轻儿子:父亲结点除了重儿子以外的所有儿子。
重边:父亲结点与重儿子的连边。
轻边:父亲结点与轻儿子的连边。
重链:所有重边所组成的一条链。

2.变量声明

f[u]:保存点u的父亲结点
d[u]:保存点u的深度
size[u]:保存以u为根节点的子树的结点个数
son[u]:保存点u的重儿子
top[u]:保存点u所在重链的顶端结点
id[u]:保存点u进行重新编号后的新的编号

三.操作过程

这个其实不太难,但一定要把上面的概念理解清楚。

1.预处理

既然我们定义了这么多变量,定然是要一个一个处理出来的撒。

1.1.dfs1

首先,大家看,第一遍DFS能够处理出来那些数据:
定然是:f[u], d[u], size[u], son[u]
Code:

void dfs1 (int x, int fa, int dep){
   //x是指此节点,fa是指父节点,dep是指当前深度
    d[x] = dep;
    siz[x] = 1;
    f[x] = fa;
    int maxs = -1;
    for (int i = 0; i < G[x].size (); i ++){
   
        int tmp = G[x][i];
        if (tmp != fa){
   
            dfs1 (tmp, x, dep + 1);
            if (siz[tmp] > maxs)//建议大家像这样写,其他写法可能有bug
                son[x] = tmp, maxs = siz[tmp];
            siz[x] += siz[tmp];
        }
    }
}

1.2.dfs2

然后,看懂了第一个深搜,就来处理top[u], id[u]
注意,我们处理出来之后,同一条重链上的点的id是从上到下递增的。
这就有点考技术,大家想一想,详见代码。
Code:

void dfs2 (int x, int fa){
   //我这里id是用dfn代替的,这个fa其实并不代表父亲节点,而是代表重链的顶点
    cnt ++;
    top[x] = fa;
    dfn[x] = cnt;
    w[cnt] = val[x];
    if (! son[x])//如果重链到头了,就返回
        return ;
    dfs2 (son[x], fa);
    for (int i = 0; i < G[x].size (); i ++){
   
        int tmp = G[x][i];
        if (tmp != f[x] && tmp != son[x]){
   //枚举下一条重链的开端
            dfs2 (tmp, tmp);
        }
    }
}

2.开始操作

重头戏来了,现在,我们正式开始操作。

2.1.操作1:求节点x到节点y的路径上所有点权的总和

这里我们就要结合线段树了。
我们建一棵线段树出来(注意,按每个点的新编号进行建树
然后,选节点x和节点y中深度较深的一个点出来进行爬树操作(爬到它那条重链的顶点的父亲节点
然后一边爬一边通过线段树求和就行了。
爬到什么时候为止呢?
自然是两个节点已经到了同一条重链的时候结束。
最后再加上爬完树之后两点之间的点权和就行了。
其实挺好理解的。
Code

int get_sum (int x, int y){
   
    int Sum = 0;
    while (top[x] != top[y]){
   
        if (d[top[x]] < d[top[y]])//让节点x成为深度最深的节点
            swap (x, y);
        Sum = (Sum + Query (1, dfn[top[x]], dfn[x])) % p;//就和
        x = f[top[x]];//爬到重链顶尖的父节点
    }
    if (dfn[x] > dfn[y])//最后再加上爬完树之后两点之间的点权和
        Sum = (Sum + Query (1, dfn[y], dfn[x])) % p;
    else
        Sum = (Sum + Query (1, dfn[x], dfn[y])) % p;
    return Sum;
}

2.2.操作2:修改节点x到节点y的路径上所有点权

显而易见,就是把求和改成了修改,将上面的代码改一下就行了。
Code:

int Add (int x, int y, int z){
   
    int Sum = 0;
    while (top[x] != top[y]){
   
        if (d[top[x]] < d[top[y]])
            swap (x, y);
        Update (1, dfn[top[x]], dfn[x], z);
        x = f[top[x]];
    }
    if (d[x] > d[y])
        swap (x, y);
    Update (1, dfn[x], dfn[y], z);
}

3.线段树温馨提醒

如果线段树给打错了,肯定是十分恼火的。因为有时候这本是一道树链剖分的题,被你给做成了一道线段树的黑题。
错误一般都集中在建树和下传懒标记上,下面给出代码:

//建树
void build (int l, int r, int Index){
   
    a[Index].L = l;
    a[Index].R = r;
    a[Index].lazy = a[Index].s = 0;
    if (l != r){
   
        int mid = (l + r) / 2;
        build (l, mid, Index * 2);
        build (mid + 1, r, Index * 2 + 1);
        a[Index].s = (a[Index * 2].s + a[Index * 2 + 1].s
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值