算法学习笔记之树链剖分

算法学习笔记之(熟练跑分)树链剖分

PART 1

首先是第一部份,也就是熟练跑分最最最基础的用法 —— 求 \(LCA\)

首先是树链剖分

//图片出自 董晓算法

大概就是这样

本质就是根据子树大小将一颗树剖分成若干条链

然后更加方便地 处理/加速处理 信息

所以

直接

上代码?

不,还要证明树链剖分求LCA 的正确性

这个嘛

因为树剖求是不断将将链头深度大的跳,

由于链的定义,最后一定会跳到同一链上,这就是LCA

算了,不好讲,自己画一下图理解一下吧

还可以照着代码理解一下

点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, s;
const int min_ = -999999999;
struct node
{
    vector<int> son;
    int fa;    //父节点
    int size_; //一次节点为根的子树大小
    int top;   //所在链的顶端
    int hson=0;  //重儿子 heavy son 直接存节点编号
    int deep;  //此节点的深度
};
node nodes[1000000];

void build_tree(int now_) //建树,同时记录重儿子,大小和深度 相比其他模板我的版本码量稍大,但更好理解
{
    nodes[now_].deep = nodes[nodes[now_].fa].deep + 1; //记录深度
    nodes[now_].size_ = 1;                             //初始大小为一
    int to_, max_ = min_, wma=0;                         //最大值,及其位置;
    for (int yy = 0; yy < nodes[now_].son.size(); yy++)
    {
        to_ = nodes[now_].son[yy];
        if (to_ != nodes[now_].fa)
        {
            nodes[to_].fa = now_; //设置儿子结点的父亲节点
            build_tree(to_);
            nodes[now_].size_ += nodes[to_].size_; //加上儿子结点的大小
            if (nodes[to_].size_ > max_)           //记录重儿子
            {
                max_ = nodes[to_].size_;
                wma = to_;
            }
        }
    }
    nodes[now_].hson = wma;
}
void get_top(int now_, int top_) //剖分一棵树,top_为链根
{
   // cout<<now_<<" getting"<<endl;
    nodes[now_].top = top_;    //记录本节点top
    if (nodes[now_].hson == 0) //如果没有重儿子
    {
        return;
    }
    get_top(nodes[now_].hson, top_); //搜重儿子
    int to_;
    for (int ww = 0; ww < nodes[now_].son.size(); ww++) //搜所有子节点
    {
        to_ = nodes[now_].son[ww];
        if (to_ != nodes[now_].fa && to_ != nodes[now_].hson) //搜所有轻儿子
        {
            get_top(to_, to_);
        }
    }
}
int get_lca(int a, int b)//求LCA
{
    while (nodes[a].top != nodes[b].top)//他们还不在一条链上
    {
        if (nodes[nodes[a].top].deep < nodes[nodes[b].top].deep) //将链顶深度深的放在a
        {
            swap(a, b);
        }
        a = nodes[nodes[a].top].fa;//跳链
    }
    return nodes[a].deep < nodes[b].deep ? a : b;//返回LCA
}
int main()
{
    ios::sync_with_stdio(false);
    cin >> n >> m >> s;
    int a, b;
    for (int ww = 1; ww < n; ww++)
    {
        cin >> a >> b;
        nodes[a].son.push_back(b);
        nodes[b].son.push_back(a);
    }
    build_tree(s);
    get_top(s, s);
    for (int yy = 1; yy <= m; yy++)
    {
        cin >> a >> b;
        cout << get_lca(a, b) << "\n";
    }
    return 0;
}
//例1
//求LCA
//例2
//熟练跑分基础运用
// P3384

PART 2

  • 加上线段树!
  • 怎么说
  • 是这样的 我们在dfs求链头的时候 会有一个访问的先后顺序,不难看出,同一条重链上的编号一定是连续的

如下图

我们可以把树上所有的点按深搜到的顺序有序的映射到一个数组上,将树上的修改问题变为了数组中的区间修改/查询

然后就可以用线段树维护信息了

细节看代码(区区200行而已!)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, r;
long long  p;
const long long  min_ = -99999999999;
struct tree //线段树部分
{
    int l, r;
    long long  sum;
    long long  tag;
};
tree t[410000];

struct node //树剖部分
{
    int top;
    int fa;
    int hson;
    long long  deep;
    long long size_;
    vector<int> son;
};
int cnt = 0;
long long  w[200000];  //旧权值
long long  id[200000]; //用来存剖分后每个结点的新编号
long long  nw[200000]; //新权值
node nodes[1000000];
//以下为线段树,无需多言
void update(int p)
{
    t[p].sum = t[p << 1].sum + t[p << 1 | 1].sum;
}
void build(int p, int l, int r)
{
    t[p].l = l;
    t[p].r = r;
    if (l == r)
    {
        t[p].sum = nw[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(p << 1, l, mid);
    build(p << 1 | 1, mid + 1, r);
    update(p);
}
void pushdown(int p)
{
    if (t[p].tag)
    {
        t[p << 1].sum += (t[p << 1].r - t[p << 1].l + 1) * t[p].tag;
        t[p << 1].tag += t[p].tag;
        t[p << 1 | 1].sum += (t[p << 1 | 1].r - t[p << 1 | 1].l + 1) * t[p].tag;
        t[p << 1 | 1].tag += t[p].tag;
        t[p].tag = 0;
    }
}
void change(int p, int l, int r, long long delt)
{
    if (l <= t[p].l && r >= t[p].r)
    {
        t[p].tag += delt;
        t[p].sum += (t[p].r - t[p].l + 1) * delt;
        return;
    }
    pushdown(p);
    int mid = (t[p].l + t[p].r) >> 1;
    if (l <= mid)
    {
        change(p << 1, l, r, delt);
    }
    if (r > mid)
    {
        change(p << 1 | 1, l, r, delt);
    }
    update(p);
    return;
}
long long query(int p, int l, int r)
{
    if (l <= t[p].l && r >= t[p].r)
    {
        return t[p].sum;
    }
    pushdown(p);
    long long ans = 0;
    int mid = (t[p].l + t[p].r) >> 1;
    if (l <= mid)
    {
        ans += query(p << 1, l, r);
    }
    if (r > mid)
    {
        ans += query(p << 1 | 1, l, r);
    }
    return ans;
}

//以下为树剖正常build_tree 见PART1
void build_tree(int now_)
{
    nodes[now_].deep = nodes[nodes[now_].fa].deep + 1;
    nodes[now_].size_ = 1;
    long long  to_, max_ = min_, wma = 0;
    for (int rr = 0; rr < nodes[now_].son.size(); rr++)
    {
        to_ = nodes[now_].son[rr];
        if (to_ != nodes[now_].fa)
        {
            nodes[to_].fa = now_;
            build_tree(to_);
            nodes[now_].size_ += nodes[to_].size_;
            if (nodes[to_].size_ > max_)
            {
                max_ = nodes[to_].size_;
                wma = to_;
            }
        }
    }
    nodes[now_].hson = wma;
}
void get_top(int now, int top) //在要修改环境之下要魔改
{
    nodes[now].top = top;
    id[now] = ++cnt;//dfs序号
    nw[id[now]] = w[now];//映射新值

    if (nodes[now].hson == 0)//没有重儿子
    {
        return;
    }
    get_top(nodes[now].hson, top);
    int to_;
    for (int yy = 0; yy < nodes[now].son.size(); yy++)
    {
        to_ = nodes[now].son[yy];
        if (to_ != nodes[now].fa && to_ != nodes[now].hson)
        {
            get_top(to_, to_);
        }
    }
}

long long query_path(int a, int b) //查询路径长
{
    long long ans = 0;
    while (nodes[a].top != nodes[b].top)//跳链
    {
        if (nodes[nodes[a].top].deep < nodes[nodes[b].top].deep)
        {
            swap(a, b);
        }
        ans += query(1, id[nodes[a].top], id[a]);//加上跳的这一段距离 易证这个节点所在链的链头的 dfs序 小于 这个节点的 dfs序
        a = nodes[nodes[a].top].fa;
    }
    if (nodes[a].deep < nodes[b].deep)
    {
        swap(a, b);
    }
    ans += query(1, id[b], id[a]);//加上最后的一段距离
    return ans;
}
void change_path(int a, int b, long long delt)//更改整条路经
{
    while (nodes[a].top != nodes[b].top)
    {
        if (nodes[nodes[a].top].deep < nodes[nodes[b].top].deep)
        {
            swap(a, b);
        }
        change(1, id[nodes[a].top], id[a], delt);//更改跳的这一段 易证这个节点所在链的链头的 dfs序 小于 这个节点的 dfs序
        a = nodes[nodes[a].top].fa;
    }

    change(1, min(id[a],id[b]), max(id[a],id[b]), delt);
}
void change_tree(int p, long long  delt) //更改子树
{
    change(1, id[p], id[p] + nodes[p].size_ - 1, delt);//易证一个子树的 dfs序 一定在 [根的dfs序,到根的dfs序+此树大小] 这一段
    //这是因为当搜索一个子树时会把这个子树遍历完再回溯出去,一定经过了 此树大小 个节点 
}
long long query_tree(int p)//查询子树
{
    return query(1, id[p], id[p] + nodes[p].size_ - 1);
}
int main()
{
    ios::sync_with_stdio(false);
    cin >> n >> m >> r >> p;
    for (int yy = 1; yy <= n; yy++)
    {
        cin >> w[yy];
    }
    int a, b, c;
    long long  d;
    for (int yy = 1; yy < n; yy++)
    {
        cin >> a >> b;
        nodes[a].son.push_back(b);
        nodes[b].son.push_back(a);
    }
    build_tree(r);
    get_top(r, r);
    build(1, 1, n);

    for (int qq = 1; qq <= m; qq++)
    {
        cin >> a;
        if (a == 1)
        {
            cin >> b >> c >> d;
            change_path(b, c, d);
        }
        if (a == 2)
        {
            cin >> b >> c;
            cout << query_path(b, c)%p << "\n";
        }
        if (a == 3)
        {
            cin >> b >> d;
            change_tree(b, d);
        }
        if (a == 4)
        {
            cin >> b;
            cout << query_tree(b) %p<< "\n";
        }
    }

    return 0;
}

PART3

有时在进行树链剖分时的权值是边权,这个时候就需要我们将边权变为点权

看似没什么问题

然而

在区间求和和区间修改时会发现,我们跑对点 \(a\) , \(b\) 之间的路径进行 查讯/修改 跑出来的值/结果 多 加上/修改 了 \(lca(a,b)\)

而点 \(a\) , \(b\) 之间的路径不会经过 \(lca(a,b)\) 上面这条边,也就不会影响到 点 \(lca(a,b)\) 对应的权值

对于这种情况,区间查询时应将 \(lca(a,b)\) 的权值删去,区间修改时不应将 \(lca(a,b)\) 的权值进行修改

总之,自己画一下图,能够更好的理解这个细节

完结散花!

posted @ 2024-08-17 19:40  sea-and-sky  阅读(40)  评论(1编辑  收藏  举报