线段树的一些延伸

一.动态开点线段树

简介

虽然思路简单,但对于一个习惯数组写法的人,这是一个比较难受的东西。

动态开点一般是用来解决空间上的问题的。

一般来说,普通的线段树是直接将一颗完整的线段建出来,但如碰到数据范围大或卡空间的时候,我们就只能在我们需要的时候再建,这个就叫做动态开点。(类似于 trie)

处理方法

  • 结构体

动态开点用数组是极不方便的,所以采用结构体写法。(本人真的很不习惯)

struct Tree
{
    int sum;
    int l,r;
}t[MAXN<<5];
  • 上传
inline void pushup(int p)
{
    t[p].sum=t[t[p].l].sum+t[t[p].r].sum;
    return;
}
  • 下传

由于跑到的节点可能没建过,所以要新建。

inline void pushdown(int p)
{
    if(!t[p].l) t[p].l=++tot;
    if(!t[p].r) t[p].r=++tot;
    return;
}
  • 单点修改

一般来说,我们这里算 \(mid\)mid=(l+r-1)>>1 ,这样可以处理 \(l,r\) 均为负数的情况。

inline void change(int p,int l,int r,int x,int k)
{
    if(l==r) {t[p].sum+=k;return;}
    int mid=(l+r-1)>>1; pushdown(p);
    if(x<=mid) change(t[p].l,l,mid,x,k);
    else change(t[p].r,mid+1,r,x,k);
    pushup(p); return;
}
  • 求和
inline int ask(int p,int l,int r,int a,int b)
{
    if(l>b || r<a) return 0;
    if(l>=a && r<=b) return t[p].sum;
    int mid=(l+r-1)>>1,ans=0; pushdown(p);
    if(a<=mid) ans+=ask(t[p].l,l,mid,a,b);
    if(b>mid) ans+=ask(t[p].r,mid+1,r,a,b);
    return ans;
}

二.权值线段树

简介

对于值域建立的线段树,用于统计区间内某个数出现次数。

支持维护同一个动态区间第 \(k\) 小(反之同理),支持单点修改。

修改与查询的复杂度均为 \(O(\log n)\)

处理方法

  • 建树

建树只需维护左右区间,无需赋值。

1. 用每个节点维护原序列中的值;
2. 记录每个区间每个数的出现次数;

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

特别地,因为权值线段树又名值域线段树,所以建树是基于值域建的。

如:

题目规定:\(a_i\leqslant 10^5\)

那么建树操作便为 build(1,1,100000)

  • 修改

权值线段树在修改中可以维护区间第 \(k\) 小于前 \(k\) 小的和,故需要统计一下。

inline void change(int p,int k)
{
    t[p].val+=k,t[p].num++;
    if(t[p].l==t[p].r) return;
    if(k>=t[p<<1|1].l) change(p<<1|1,k);
    else change(p<<1,k);
    return;
}
  • 查询

有了上面统计的那些信息,区间求值就很容易了。

区间前 \(k\) 小之和

inline int ask1(int p,int k)
{
    if(t[p].l==t[p].r) return t[p].val/t[p].num*k;
    if(k>t[p<<1].num) return t[p<<1].val+ask1(p<<1|1,k-t[p<<1].num);
    else return ask1(p<<1,k);
}

区间第 \(k\)

inline int ask2(int p,int k)
{
    if(t[p].l==t[p].r) return t[p].val/t[p].num;
    if(k>t[p<<1].num) return ask2(p<<1|1,k-t[p<<1].num);
    else return ask2(p<<1,k);
}

三.主席树

简介

又名可持久化权值线段树,用于动态维护任意区间第 \(k\) 大。

支持单点修改和区间查询。

处理方法

主席树的思想是对每一个前缀均建立一颗权值线段树。

  • 初始化

我们发现,由于空间复杂度的问题,我们需要动态开点,所以就无法直接算出一个节点的左右儿子,所以都要记录一下。

需要注意的是,主席树一般要开32倍空间。

\(rt_i\) 为版本 \(i\) 的标号。

int id[MAXN];

struct Tree
{
    int l,r,val;
}e[MAXN<<5];
  • 建树

我们无法直接算出左右儿子编号,所以建树可以只用统计 \(id\) 数组。

inline void build(int &p,int l,int r)
{
    p=++cnt;
    if(l==r) return;
    int mid=(l+r)>>1;
    build(t[p].l,l,mid);
    build(t[p].r,mid+1,r);
    return;
}

\(cnt\) 表示动态开点的编号。

  • 修改

与权值线段树唯一不同的是要再开一颗线段树。

inline int change(int p,int l,int r,int k)
{
    int rt=++cnt;
    t[rt]=t[p],t[rt].val++;
    if(l==r) return rt;
    int mid=(l+r)>>1;
    if(k<=mid) t[rt].l=change(t[p].l,l,mid,k);
    else t[rt].r=change(t[p].r,mid+1,r,k-x);
    return rt;
}
  • 查询

由于每一颗线段树的结构相同,具有可加减性。

并且我们建立的是关于前缀的线段树,所以我们可以用前缀和的思想。

inline int ask(int l,int r,int a,int b,int k)
{
    int ans=0,mid=(l+r)>>1;
    int x=t[t[b].l].val-t[t[a].l].val;
    if(l==r) return l;
    if(k<=x) ans=ask(l,mid,t[a].l,t[b].l,k);
    else ans=ask(mid+1,r,t[a].r,t[b].r,k);
    return ans;
}
  • 离散化

考虑到空间问题,我们需要离散化。

inline void dist()
{
    sort(b+1,b+n+1);
    m=unique(b+1,b+n+1)-b-1;
    build(id[0],1,m);
    for(int i=1;i<=n;i++)
    {
        int p=lower_bound(b+1,b+m+1,a[i])-b;
        id[i]=change(id[i-1],1,m,p);
    }
    return;
}

到这里,主席树的模板就没了,但还有一些操作需要自己去探索。

例题:

四.线段树合并

思想

前置知识:动态开点线段树权值线段树

顾名思义,就是建立一颗新的线段树,保存原有两颗线段树的信息。

这个思想比较简单,假设我们现在合并到了两棵线段树 \(a,b\)\(p\) 位置,那么:

1.如果 \(a\)\(p\) 位置,\(b\) 没有,那么新的线段树 \(p\) 位置赋成 \(a\),返回;
2.如果 \(b\)\(p\) 位置,\(a\) 没有,赋成 \(b\),返回;
3.如果此时已经合并到两棵线段树的叶子节点了,就把 \(b\)\(p\) 的值加到 \(a\) 上,把新线段树上的 \(p\) 位置赋成 \(a\),返回;
4.递归处理左子树,递归处理右子树;
5.用左右子树的值更新当前节点;
6.将新线段树上的 \(p\) 位置赋成 \(a\),返回;

处理方法

  • 合并

线段树合并的核心操作。

inline int merge(int a,int b,int l,int r)
{
    if(!a) return a;
    if(!b) return b;
    if(l==r) return a;
    int mid=(l+r)>>1;
    t[a].l=merge(t[a].l,t[b].l,l,mid);
    t[a].r=merge(t[a].r,t[b].r,mid+1,r);
    pushup(a); return a;
}

假设要插入的点数为 \(n\),那么时空复杂度均为 \(\Theta(n\log n)\)

例题

P4556 【模板】线段树合并

首先有个暴力,利用树上差分的思想,把 \(u,v\) 上放一个数字改成 \(u\)\(1\) 放一个数字,\(v\)\(1\) 放一个数字,\(u,v\)\(lca\)\(1\) 撤回一个数字,\(u,v\)\(lca\)\(fa\)\(1\) 撤回一个数字这四个操作。

接下来我们每个节点上开一个 \(cnt_i\),表示 \(i\) 这个数字出现了多少次,那么节点 \(i\) 上出现最多的数字就是 \(cnt\) 数组的最大值。

那么我们求 \(i\) 节点的 \(cnt\) 数组可以暴力的把它的所有孩子的 \(cnt\) 数组按位相加起来来进行求解,然后如果这个节点上有插入或者删除数字的操作我们再对 \(cnt\) 数组进行几次操作就行了。

然后考虑优化。

每个点开一颗权值线段树表示这个点上有什么数字,每个数字出现了几次,然后求点 \(i\) 的权值线段树,就是将它所有孩子的线段树合并到一起,然后再对合并出来的线段树进行一些在这个节点的插入和删除操作。

(不愧是紫牌题)。

点击查看代码

P3521 [POI2011] ROT-Tree Rotations

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;

int n,m;

struct edge
{
    int to,nxt;
}e[MAXN<<1];

int head[MAXN],cnt;

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

int dep[MAXN],fa[MAXN],hson[MAXN],siz[MAXN];

inline void dfs1(int x,int f)
{
    dep[x]=dep[f]+1;
    fa[x]=f,siz[x]=1;
    int maxson=-1;
    for(int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to;
        if(y==f) continue;
        dfs1(y,x);
        siz[x]+=siz[y];
        if(maxson<siz[y])
        {
            maxson=siz[y];
            hson[x]=y;
        }
    }
    return;
}

int top[MAXN];

inline void dfs2(int x,int ltop)
{
    top[x]=ltop;
    if(!hson[x]) return;
    dfs2(hson[x],ltop);
    for(int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to;
        if(y==fa[x] || y==hson[x]) continue;
        dfs2(y,y);
    }
    return;
}

inline int LCA(int x,int y)
{
    while(top[x]!=top[y])
    {
        if(dep[top[x]]<dep[top[y]]) swap(x,y);
        x=fa[top[x]];
    }
    if(dep[x]<dep[y]) return x;
    return y;
}

struct Tree
{
    int sum,num;
    int l,r;
}t[MAXN*60];

int tot;
int rt[MAXN];

inline void pushup(int p)
{
    if(t[t[p].l].sum>t[t[p].r].sum || (t[t[p].l].num<t[t[p].r].num && t[t[p].l].sum==t[t[p].r].sum))
        t[p].sum=t[t[p].l].sum,t[p].num=t[t[p].l].num;
    else t[p].sum=t[t[p].r].sum,t[p].num=t[t[p].r].num;
    return;
}

inline void change(int &p,int l,int r,int x,int k)
{
    if(!p) p=++tot;
    if(l==r) {t[p].num=x,t[p].sum+=k;return;}
    int mid=(l+r)>>1;
    if(x<=mid) change(t[p].l,l,mid,x,k);
    else change(t[p].r,mid+1,r,x,k);
    pushup(p); return;
}

inline int merge(int a,int b,int l,int r)
{
    if(!a) return b;
    if(!b) return a;
    if(l==r) {t[a].sum+=t[b].sum;return a;}
    int mid=(l+r)>>1;
    t[a].l=merge(t[a].l,t[b].l,l,mid);
    t[a].r=merge(t[a].r,t[b].r,mid+1,r);
    pushup(a); return a;
}

int ans[MAXN];

inline void dfs(int x,int f)
{
    for(int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to;
        if(y==f) continue;
        dfs(y,x);
        rt[x]=merge(rt[x],rt[y],1,MAXN);
    }
    ans[x]=t[rt[x]].num;
    return;
}

int main()
{
    ios_base::sync_with_stdio(false);cin.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n-1;i++)
    {
        int x,y; cin>>x>>y;
        add(x,y),add(y,x);
    }
    dfs1(1,0);dfs2(1,1);
    for(int i=1;i<=m;i++)
    {
        int x,y,z; cin>>x>>y>>z;
        int lca=LCA(x,y);
        change(rt[x],1,MAXN,z,1);
        change(rt[y],1,MAXN,z,1);
        change(rt[lca],1,MAXN,z,-1);
        change(rt[fa[lca]],1,MAXN,z,-1);
    }
    dfs(1,0);
    for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
    return 0;
}

P3605 [USACO17JAN] Promotion Counting P

这道题比较裸,直接权值线段树套线段树合并,查询就直接查当前节点的线段树,值域比较大,要离散化一下。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;

struct edge
{
    int to,nxt;
}e[MAXN<<1];

int head[MAXN],cnt;

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

int n,m,a[MAXN],b[MAXN];
int rt[MAXN],ans[MAXN];

struct Tree
{
    int cnt,l,r;
}t[MAXN<<5];

int dat[MAXN<<5];

inline void pushup(int p)
{
    t[p].cnt=t[t[p].l].cnt+t[t[p].r].cnt;
    return;
}

int tot;

inline void change(int &p,int l,int r,int k)
{
    if(!p) p=++tot;
    if(l==r) {t[p].cnt++,dat[p]=k;return;}
    int mid=(l+r)>>1;
    if(k<=mid) change(t[p].l,l,mid,k);
    else change(t[p].r,mid+1,r,k);
    pushup(p); return;
}

inline int merge(int a,int b,int l,int r)
{
    if(!a) return b;
    if(!b) return a;
    if(l==r) {t[a].cnt+=t[b].cnt,dat[a]=dat[b];return a;}
    int mid=(l+r)>>1;
    t[a].l=merge(t[a].l,t[b].l,l,mid);
    t[a].r=merge(t[a].r,t[b].r,mid+1,r);
    pushup(a); return a;
}

inline int ask(int p,int l,int r,int a,int b)
{
    if(l>b || r<a) return 0;
    if(l>=a && r<=b) return t[p].cnt;
    int mid=(l+r)>>1,ans=0;
    if(a<=mid) ans+=ask(t[p].l,l,mid,a,b);
    if(b>mid) ans+=ask(t[p].r,mid+1,r,a,b);
    return ans;
}

inline void dfs(int x,int fa)
{
    for(int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to;
        if(y==fa) continue;
        dfs(y,x);
        rt[x]=merge(rt[x],rt[y],1,m);
    }
    ans[x]=ask(rt[x],1,m,a[x]+1,m);
    return;
}

int main()
{
    ios_base::sync_with_stdio(false);cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i],b[i]=a[i];
    sort(b+1,b+n+1);
    m=unique(b+1,b+n+1)-b-1;
    for(int i=1;i<=n;i++)
    {
        a[i]=lower_bound(b+1,b+n+1,a[i])-b;
        change(rt[i],1,m,a[i]);
    }
    for(int i=2;i<=n;i++)
    {
        int x; cin>>x;
        add(x,i),add(i,x);
    }
    dfs(1,0);
    for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
    return 0;
}
posted @ 2023-08-09 22:00  Code_AC  阅读(14)  评论(0编辑  收藏  举报