AcWing
  • 首页
  • 课程
  • 题库
  • 更多
    • 竞赛
    • 题解
    • 分享
    • 问答
    • 应用
    • 校园
  • 关闭
    历史记录
    清除记录
    猜你想搜
    AcWing热点
  • App
  • 登录/注册

线段树+重链剖分

作者: 作者的头像   銘权 ,  2019-09-13 07:29:35 ,  所有人可见 ,  阅读 1033


4


1

线段树

线段树的打法大致有两种
以区间信息加法统计为例

第一种



const int N

int a[N];

struct TREE
{
    int l,r,dat,add;
}t[N*4];

void build(int rt,int l,int r)
{
    //建树
    t[rt].l=l,t[rt].r=r;
    if(l==r)
    {
        t[rt].dat=a[l];
        return ;
    }
    int mid=(l+r)>>1;
    build(rt<<1,l,mid);
    build(rt<<1|1,mid+1,r);
    t[rt].dat=t[rt<<1].dat+t[rt<<1|1].dat;
}

void spread(int rt)
{
    //懒标记传递
    if(t[rt].add)
    {
        t[rt<<1].dat+=t[rt].add*(t[rt<<1].r-t[rt<<1].l+1),t[rt<<1].add+=t[rt].add;
        t[rt<<1|1].dat+=t[rt].add*(t[rt<<1|1].r-t[rt<<1|1].r+1),t[rt<<1|1].add+=t[rt].add;
        t[rt].add=0;
    }
}

void change(int rt,int l,int r,int k)
{
    //区间修改
    if(l<=t[rt].l&&r>=t[rt].r)
    {
        t[rt].dat+=k*(t[rt].r-t[rt].l+1);
        t[rt].add+=k;
        return ;
    }
    spread(rt);
    int mid=(t[rt].l+t[rt].r)>>1;
    if(l<=mid) change(rt<<1,l,r,k);
    if(r>=mid+1) change(rt<<1|1,l,r,k);
    t[rt].dat=t[rt<<1].dat+t[rt<<1|1].dat;
}

int query(int rt,int l,int r)
{
    //区间查询
    if(l<=t[rt].l&&r>=t[rt].r)
    {
        return t[rt].dat;
    }
    spread(rt);
    int mid=(t[rt].l+t[rt].r)>>1;
    int ans=0;
    if(l<=mid) ans+=query(rt<<1,l,r);
    if(r>=mid+1) ans+=query(rt<<1|1,l,r);
    return ans;
}

我们可以发现这是一种中规中矩的写法,就是结构体建树,保存每个点左右儿子,权值和懒标记。


第二种

#include<bits/stdc++.h>
#define ll long long
#define lson rt<<1,l,mid
#define rson rt<<1|1,mid+1,r
using namespace std;

const int N=100000+10;

int a[N];
ll dat[N*4],add[N*4];

inline void build(int rt,int l,int r)
{
    if(l==r)
    {
        dat[rt]=a[l];
        return ;
    }
    int mid=(l+r)>>1;
    build(lson);
    build(rson);
    dat[rt]=dat[rt<<1]+dat[rt<<1|1];
}

inline void spread(int rt,int l,int r)
{
    //l,r 表示为 rt 的左右儿子
    if(add[rt])
    {
        int mid=(l+r)>>1;
        dat[rt<<1]+=(ll)add[rt]*(mid-l+1),add[rt<<1]+=add[rt];
        dat[rt<<1|1]+=(ll)add[rt]*(r-mid),add[rt<<1|1]+=add[rt];
        add[rt]=0;
    }
}

inline void change(int rt,int l,int r,int L,int R,int k)
{
    //l,r 表示为 rt 的左右儿子,下同
    if(L<=l&&R>=r)
    {
        dat[rt]+=(ll)k*(r-l+1);
        add[rt]+=k;
        return ;
    }
    spread(rt,l,r);
    int mid=(l+r)>>1;
    if(L<=mid) change(lson,L,R,k);
    if(R>=mid+1) change(rson,L,R,k);
    dat[rt]=dat[rt<<1]+dat[rt<<1|1];
}

inline ll query(int rt,int l,int r,int L,int R)
{
    if(L<=l&&R>=r)
    {
        return dat[rt];
    }
    spread(rt,l,r);
    int mid=(l+r)>>1;
    ll ans=0;
    if(L<=mid) ans+=query(lson,L,R);
    if(R>=mid+1) ans+=query(rson,L,R);
    return ans;
}

int main()
{

    int n,m;

    scanf("%d%d",&n,&m);

    for(int i=1;i<=n;i++) scanf("%d",&a[i]);

    build(1,1,n);

    for(int i=1,opt,l,r,v;i<=m;i++)
    {

        scanf("%d%d%d",&opt,&l,&r);

        if(opt==1) scanf("%d",&v),change(1,1,n,l,r,v);

        else printf("%lld\n",query(1,1,n,l,r));

    }

    return 0;
}



第二种打法省去了左右儿子,而在函数中表现出来
虽然节省了空间,但参数变多了,容易在递归过程中爆栈,所以用 inline 优化(虽然好像没什么大用)

重链剖分


const int N=100000+10;

int head[N],ver[N*2],nex[N*2];
int dep[N],dfn[N],wson[N],size[N],top[N],fa[N];

void dfs1(int x)
{
    dep[x]=dep[fa[x]]+1;
    size[x]=1;
    for(int i=head[x];i;i=nex[i])
    {
        int y=ver[i];
        if(y==fa[x]) continue;
        fa[y]=x;
        dfs1(y);
        size[x]+=size[y];
        if(size[y]>size[wson[x]]) wson[x]=y;
    }
}

void dfs2(int x,int nowtop)
{
    dfn[x]=++num;
    w[num]=a[x];
    top[x]=nowtop;
    if(wson[x]) dfs2(wson[x],nowtop);
    for(int i=head[x];i;i=nex[i])
    {
        int y=ver[i];
        if(y==fa[x]||y==wson[x]) continue;
        dfs2(y,y);
    }
}

我们发现在 dfs2 中,我们预处理了搜索序,使得树可以用线段树维护

那么,线段树和重链剖分就可以结合起来,计算树上信息统计问题

唯一一道例题:


题目描述

如题,已知一棵包含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为根节点的子树内所有节点值之和

输入格式

第一行包含4个正整数N、M、R、P,分别表示树的结点个数、操作个数、根节点序号和取模数(即所有的输出结果均对此取模)。

接下来一行包含N个非负整数,分别依次表示各个节点上初始的数值。

接下来N-1行每行包含两个整数x、y,表示点x和点y之间连有一条边(保证无环且连通)

接下来M行每行包含若干个正整数,每行表示一个操作,格式如下:

操作1: 1 x y z

操作2: 2 x y

操作3: 3 x z

操作4: 4 x

输出格式

输出包含若干行,分别依次表示每个操作2或操作4所得的结果(对P取模)

原题:
https://www.luogu.org/problem/P3384


分析:


一道模板题
我们可以发现,两点之间的路径可以用 lca 表达出来
如图:运输计划.png
但是树上点到 lca 的距离不一定等于 线段树上 点到 lca 距离
那么我们就模拟一遍求 lca 的过程,把权值分段求出来

重链剖分求 lca 的实现步骤:
1) 将两点中链头更深的点跳到链头
2) 若两点不在同一重链上,更深点跳转到父节点
3) 重复操作 1)、2)直到两点处于同一条重链上,此时深度较浅的点即为所求

关于 3) 的证明:
很明显当两点处于同一条重链上时,它们的父节点也处于这条重链上,并且这个父节点,就是这两点中深度较浅的点。


代码

#include<bits/stdc++.h>
using namespace std;

const int N=100000+10;

int tot,num;
int n,m,r,p;

int w[N],a[N],dat[N*4],add[N*4];
int head[N],ver[N*2],nex[N*2];
int dep[N],dfn[N],wson[N],size[N],top[N],fa[N];
//  深度   搜索序 重儿子 子树大小 链头  父节点

inline void adde(int x,int y)
{
    ver[++tot]=y;
    nex[tot]=head[x];
    head[x]=tot;
} 

void dfs1(int x)
{
    dep[x]=dep[fa[x]]+1;
    size[x]=1;
    for(int i=head[x];i;i=nex[i])
    {
        int y=ver[i];
        if(y==fa[x]) continue;
        fa[y]=x;
        dfs1(y);
        size[x]+=size[y];
        if(size[y]>size[wson[x]]) wson[x]=y;
    }
}

void dfs2(int x,int nowtop)
{
    dfn[x]=++num;
    w[num]=a[x];
    //以搜索序重排权值
    top[x]=nowtop;
    if(wson[x]) dfs2(wson[x],nowtop);
    for(int i=head[x];i;i=nex[i])
    {
        int y=ver[i];
        if(y==fa[x]||y==wson[x]) continue;
        dfs2(y,y);
    }
}

inline void build(int rt,int l,int r)
{
    if(l==r)
    {
        dat[rt]=w[l];
        return ;
    }
    int mid=(l+r)>>1;
    build(rt<<1,l,mid);
    build(rt<<1|1,mid+1,r);
    dat[rt]=dat[rt<<1]+dat[rt<<1|1];
    dat[rt]%=p;
}

inline void spread(int rt,int l,int r)
{
    if(add[rt])
    {
        int mid=(l+r)>>1;
        dat[rt<<1]+=add[rt]*(mid-l+1),dat[rt<<1]%=p,add[rt<<1]+=add[rt];
        dat[rt<<1|1]+=add[rt]*(r-mid),dat[rt<<1|1]%=p,add[rt<<1|1]+=add[rt];
        add[rt]=0;
    }
}

inline void change(int rt,int l,int r,int ls,int rs,int k)
{
    if(l<=ls&&r>=rs)
    {
        dat[rt]+=k*(rs-ls+1);
        dat[rt]%=p;
        add[rt]+=k;
        add[rt]%=p;
        return ;
    }
    spread(rt,ls,rs);
    int mid=(ls+rs)>>1;
    if(l<=mid) change(rt<<1,l,r,ls,mid,k);
    if(r>=mid+1) change(rt<<1|1,l,r,mid+1,rs,k);
    dat[rt]=dat[rt<<1]+dat[rt<<1|1];
    dat[rt]%=p;
}

inline int query(int rt,int l,int r,int ls,int rs)
{
    if(l<=ls&&r>=rs)
    {
        return dat[rt];
    }
    spread(rt,ls,rs);
    int mid=(ls+rs)>>1;
    int ans=0;
    if(l<=mid) ans+=query(rt<<1,l,r,ls,mid),ans%=p;
    if(r>=mid+1) ans+=query(rt<<1|1,l,r,mid+1,rs),ans%=p;
    return ans;
}

inline int qsum(int x,int y)
{
    //两点间的修改
    int ans=0;
    while(top[x]!=top[y])
    {
        if(dep[top[x]]<dep[top[y]]) swap(x,y);
        ans+=query(1,dfn[top[x]],dfn[x],1,n);
        ans%=p;
        x=fa[top[x]];
    }
    if(dep[x]>dep[y]) swap(x,y);
    ans+=query(1,dfn[x],dfn[y],1,n);
    return ans%p;
}

inline void padd(int x,int y,int k)
{
    //树上两点距离
    k%=p;
    while(top[x]!=top[y])
    {
        if(dep[top[x]]<dep[top[y]]) swap(x,y);
        change(1,dfn[top[x]],dfn[x],1,n,k);
        x=fa[top[x]];
    }
    if(dep[x]>dep[y]) swap(x,y);
    change(1,dfn[x],dfn[y],1,n,k);
}

inline int qson(int rt)
{
    //由搜索序的特点可得,子树的搜索序一定比根大
    return query(1,dfn[rt],dfn[rt]+size[rt]-1,1,n);
}

inline void sonadd(int rt,int k)
{
    k%=p;
    change(1,dfn[rt],dfn[rt]+size[rt]-1,1,n,k);
}

int main()
{
    scanf("%d%d%d%d",&n,&m,&r,&p);

    for(int i=1;i<=n;i++) scanf("%d",&a[i]);

    for(int i=1,x,y;i<n;i++)
    {
        scanf("%d%d",&x,&y);
        adde(x,y);
        adde(y,x);
    }

    dfs1(r);
    dfs2(r,r);
    build(1,1,n);
    //初始化

    for(int i=1,opt,x,y,z;i<=m;i++)
    {
        scanf("%d",&opt);
        if(opt==1)
        {
            scanf("%d%d%d",&x,&y,&z);
            padd(x,y,z);
        }else if(opt==2)
        {
            scanf("%d%d",&x,&y);
            printf("%d\n",qsum(x,y));
        }else if(opt==3)
        {
            scanf("%d%d",&x,&z);
            sonadd(x,z);
        }else 
        {
            scanf("%d",&x);
            printf("%d\n",qson(x));
        }
    }

    return 0;
}

基于线段树有很多讲解,所以具体细节就不再详述了
至于树链剖分,可以看这里:
https://www.acwing.com/blog/content/350/

4 评论


用户头像
wzc1995   2019-09-13 13:08         踩      回复

inline 很难优化递归函数,避免递归爆栈的唯一方法是将栈变大。

用户头像
銘权   2019-09-13 15:47         踩      回复

那该怎样将栈变大呢?

用户头像
wzc1995   2019-09-13 22:58    回复了 銘权 的评论         踩      回复

一般linux环境下默认的栈就够用了

用户头像
銘权   2019-09-14 14:08    回复了 wzc1995 的评论         踩      回复

好,谢谢


App 内打开
你确定删除吗?
1024
x

© 2018-2025 AcWing 版权所有  |  京ICP备2021015969号-2
用户协议  |  隐私政策  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标 qq图标
请输入绑定的邮箱地址
请输入注册信息