线段树总结(未完结)

线段树总结

posted on 2022.5.24:感觉写的好乱……真就是早期总结……大概日后会考虑重写/大手术修改,,,但是先咕着吧。(雾)

最近写线段树的题,感觉好多题其实有些地方根本没懂,然后好多题都不会直接看了题解。感觉线段树挺重要的,总结一下吧。

注:本文主要适用于已经掌握线段树基本操作的同学,如果是初学可能并不适合。


  • 普通线段树

例题:P3372 【模板】线段树 1

普通线段树感觉就不用多说了,毕竟自己写了那么多理解的也差不多了。

主要功能就是维护区间查询,区间修改。当然也支持单点查询和单点修改。

基本操作

  • 建树
void build(int now,int l,int r)
{
    if(l==r){sum[now]=read();return ;}
    build(ls,l,mid);//建立左子树
    build(rs,mid+1,r);//右子树
    pu(now,l,r);//pushup
    return ;
}
  • 单点修改
void ins(int now,int l,int r,int k,int v)
{
    if(l==r){sum[now]=v;return ;}
    //注意不需要lazy和pushdown!
    if(k<=mid)ins(ls,l,mid,k,v);
    else ins(rs,mid+1,k,v);
    pu(now,l,r);
}
  • 单点查询
int query(int now,int l,int r,int k)
{
    if(l==r){return sum[now];}
    int ret=0;
    if(k<=mid)ret+=query(ls,l,mid,k);
    else ret+=query(rs,mid+1,r,k);
    return ret;
}
  • 区间修改
//这里以区间加为例,当然区间乘/最大值/最小值等情况完全可以类比

void ins(int now,int l,int r,int L,int R,int v)
{
    if(L<=l&&r<=R){sum[now]+=(r-l+1)*v;lazy[now]+=v;return ;}//如果当前区间已经完全包含于被查询区间,那么直接加,注意此处必须乘上r-l+1,因为是给这个区间内所有元素+v。另一个关键是lazy标记,这一点一会会说到。
    if(lazy[now])pd(now,l,r);//如果lazy!=0,标记下传(pushdown一会也会说到)
    if(L<=mid)ins(ls,l,mid,L,R,v);
    if(R>=mid+1)ins(rs,mid+1,r,L,R,v);
    pu(now,l,r);
}
  • 区间查询
int query(int now,int l,int r,int L,int R)
{
    if(L<=l&&r<=R){return sum[now];}//与区间查询同理,如果当前区间已经完全包含于要查询区间中,直接返回这个区间的和。
    if(lazy[now])pd(now,l,r);
    int ret=0;
    if(L<=mid)ret+=query(ls,l,mid,L,R);
    if(R>=mid+1)ret+=query(rs,mid+1,r,L,R);
    return ret;
}

以上三个就是线段树的基本操作。

建树主要是用来初始化线段树维护的一些东西。经过这段时间的做题,感觉初始化的对象主要是下面几个:

  1. 与区间修改的对象有关: 线段树1(区间和),最大数(最大值),列队(节点子树大小sum和当前节点的具体值);

  2. lazy标记:线段树2(mul乘法懒标记),上帝造题的七分钟 2(区间和,最大值懒标记);

  3. 动态开点线段树新建节点:凡是用动态开点线段树都要这一步操作。

区间修改主要是修改区间某一段的某个对象,比如区间和/积,最大值/最小值,某一段区间的具体值。这个根据具体题里维护的对象是不同的。要注意的是有些操作不能直接val[]=v;而应该进行一些变化,比如线段树区间加里面是sum[]+=(l-mid+1)*v。

这个感觉问题并不是很大,就不多说了。

区间查询这个,其实应该说是很死的,因为几乎不管是什么题都几乎是一样的板子,连板子都几乎原封不动的套上去。所以这里也就不多赘述了(雾)。

pushup和pushdown

pushup:

内部机制是,在每次到左子树和右子树分别更新了sum[ls]和sum[rs]之后,sum[now]也会受到影响,pushup用来更新sum[now],易知sum[now]=sum[ls]+sum[rs];

主要用于再修改之后更新sum的值,也就是build和ins操作中,因为只有这两个操作可能修改sum的值。

code:

void pu(int now,int l,int r)
{
    sum[now]=sum[ls]+sum[rs];
    return ;
}

pushdown:

此操作是针对lazy标记的。

先说一下lazy标记吧。比如你现在要修改节点编号是3的点,它的左儿子是6,右儿子是7,按照一般的方法肯定是递归到3的左儿子和右儿子分别修改一下然后再回来。但这样做的话,显然时间复杂度和暴力差不多,就像是一个大常数暴力,是起不到作为线段树的效果的。

但我们会发现,当我们修改3这个地方的sum时,影响的只有它和它的子树。如果我们不去管它的子树,那么我们会发现这时候只有它的子树的答案是错的。也就是说除了它的子树外,这棵线段树上的其他点概不影响。

那么我们就想到了一个好的方法:lazy标记。我们只需要在3这里标记一下lazy[now]+=v;指的是3和它的子树都加上v。如果我们查找不到它的子树,那么根本不用管它的子树会如何。而当我们需要查找到它的子树时,只需要让它的子节点每次把它的父亲的lazy继承过来即可!这样的做法免去了再次递归下去的必要,降低了时间复杂度。我们每次查找一个节点的子树,只需要每次将这个lazy标记向下移动一次即可。

而pushdown就是让这个lazy标记下移的操作,也就是每次需要递归它的左右儿子时,把sum更新,lazy下移。

注意:许多同学再写build操作时,进行了pushdown操作,但这个其实是完全没有必要的。因为你在build的时候,本身就是已经递归到叶子节点了,你跟本不需要lazy标记,哪里来的pushdown!

所以在线段树基本操作中,只有ins操作和query操作需要pushdown。更一般地,也就是说pushdown只需要在有类似于区间修改和查询的操作,如果仅仅是单点修改,并不需要。

于是我又傻不拉几的在最大数那道题里面用了lazy标记/kk

code:

void pd(int now,int l,int r)
{
    sum[ls]+=(mid-l+1)*lazy[now];sum[rs]+=(r-mid)*lazy[now];
    lazy[ls]+=lazy[now];lazy[rs]+=lazy[now];
    lazy[now]=0;
    return ;
}
  • 动态开点线段树

这玩意是我自始至终学的云里雾里的东西。详细讲一下吧,也为了让我自己梳理清楚。(雾)

首先我们要弄清楚:动态开点线段树与普通线段树的区别主要在哪,它的优势在哪,为什么而存在。

我们考虑一下线段树的空间复杂度:O(4*n) ,证明见此文:线段树空间复杂度为什么是O(4*N-1)?

显然如果n很大,那么空间一定会炸掉。那么怎么样让它不炸掉呢?线段树优化——动态开点线段树就应运而生。

我们会发现在普通线段树中,虽然我们事先建了最多4*n个点,但其实有好多点是用不到的,就相当于是白白占了空间。那么我们就可以考虑,并不是刚开始就建好一棵完整的树,我们可以在计算和操作的过程中建立树,也就是说,刚开始的线段树并非一棵完整的线段树,是残缺的,只有在需要的时候,才建立新节点,否则如果根本不需要就不用建立新节点。

那么我们可以每次操作时,往下递归左儿子和右儿子,如果没有左儿子和右儿子,则新建这个节点的左儿子和右儿子即可。

我们可以使用两个数组ls[]和rs[]分别表示每个节点的左右儿子。

由于动态开点的主要代码实现和普通线段树没什么区别,只是把rs改成了数组,所以不再赘述,这里只放出代码和少量注释。

//pushup

void pu(int now,int l,int r){sum[now]=sum[ls[now]]+sum[rs[now]];return ;}
//pushdown

void pd(int now,int l,int r)
{
    lazy[ls[now]]+=lazy[now];lazy[rs[now]]+=lazy[now];
    sum[ls[now]]+=(mid-l+1)*lazy[now];sum[rs[now]]+=(r-mid)*lazy[now];
    lazy[now]=0;
    return ;
} 
//build

void build(int &now,int l,int r)
{
    if(!now)New(now);
    if(!ls[now])New(ls[now]);//如果左/右儿子不存在,把它建出来。
    if(!rs[now])New(rs[now]);
    if(l==r){sum[now]=read();return ;}
    build(ls[now],l,mid);
    build(rs[now],mid+1,r);
    pu(now,l,r);
    return ; 
}
//区间修改

void ins(int &now,int l,int r,int L,int R,int v)
{
    if(!now)New(now);
    if(!ls[now])New(ls[now]);
    if(!rs[now])New(rs[now]);
    if(L<=l&&r<=R){sum[now]+=(r-l+1)*v;lazy[now]+=v;return ;}
    if(lazy[now])pd(now,l,r);
    if(L<=mid)ins(ls[now],l,mid,L,R,v);
    if(R>mid)ins(rs[now],mid+1,r,L,R,v);
    pu(now,l,r);
    return ;
}
//区间查询

int query(int &now,int l,int r,int L,int R)
{
    if(!now)New(now);
    if(!ls[now])New(ls[now]);
    if(!rs[now])New(rs[now]);
    if(L<=l&&r<=R){return sum[now];}
    if(lazy[now])pd(now,l,r);
    int ret=0;
    if(L<=mid)ret+=query(ls[now],l,mid,L,R);
    if(R>mid)ret+=query(rs[now],mid+1,r,L,R);
    return ret;
}

总结一下动态开点线段树的功能:

  1. 省空间;

  2. 用于主席树(后面会提到)。

  • 主席树(可持久化线段树)

主席树,也叫可持久化线段树。

为什么叫可持久化呢?我们之前所学习的线段树,实际上维护的是“数据集的最新状态”,也就是每次更新之后的状态。但是如果我们无法得知它的历史状态。为了知道它的历史状态,我们就要可持久化。

我们考虑这样一个问题,给你n个数,m组询问,每组询问给你一个区间[l,r],问区间[l,r]内的第k大数是多少?

数据范围: 1≤n,m≤2×10^5

最简单的方法就是考虑暴力。但是显然时间复杂度会炸。

考虑线段树。如果我们可以维护n个线段树,每个线段树存的是[1,i]的siz,那么如果我们要求区间内第k大,应该怎么求呢?

很容易想到我们在初学前缀和时,用的思想。

第一棵线段树表示[1,l]这段区间的答案,第二棵线段树表示的是[1,r]这段区间的答案。

那么答案就是[1,r]-[1,l-1],也就是第二棵线段树的答案减去第一棵线段树。

考虑怎么实现。

如果直接建立 n+1 棵线段树,那么空间肯定会炸掉,我们考虑每次更改一个节点,会更改哪些节点的值。

(更改了节点8的值,对应更新的是1,2,4,8的值。)

可以看出,当我们更改其中一个节点的值,会更改的答案是这个节点所在这条链的值,也就是说,我们最多可以更改 O(\log n) 个节点的值。

那么我们可以考虑,每次在更新其中一个节点的值时,我们只需要新建那些值改变的节点,而不需要新建整棵树,只需要把新建节点连向原来没有被修改过的节点即可。也就是说可以共用节点,没有修改过值的节点,就是共用节点。

使用动态开点线段树实现即可。

代码实现如下:

//单点修改

void ins(int pre,int &now,int l,int r,int pos)
{
    if (!now) now=++tot;
    if (l==r){sum[now]=sum[pre]+1;return;}
    if (pos<=mid) {ins(ls[pre],ls[now],l,mid,pos); rs[now]=rs[pre];}
    else {ins(rs[pre],rs[now],mid+1,r,pos); ls[now]=ls[pre];}
    pu(now,l,r);
    return;
}
//查询

int query(int pre,int now,int l,int r,int k)
{
    if (l==r) return l;
    if (sum[ls[now]]-sum[ls[pre]]>=k) return query(ls[pre],ls[now],l,mid,k);
    return query(rs[pre],rs[now],mid+1,r,k-(sum[ls[now]]-sum[ls[pre]]));
}

例题:P3834 【模板】可持久化线段树 2

P3919 【模板】可持久化线段树 1(可持久化数组)

  • 树链剖分

树链剖分 就是对一棵树分成几条链,把树形变为线性,减少处理难度

解决的主要问题:

  1. 将树从x到y结点最短路径上所有节点的值都加上v;
  2. 求树从x到y结点最短路径上所有节点的值之和;
  3. 将以x为根节点的子树内所有节点值都加上v;
  4. 求以x为根节点的子树内所有节点值之和;

先给出一些概念:

  • 重儿子:父亲节点的所有儿子中子树结点数目最多(size最大)的结点;

  • 轻儿子:父亲节点中除了重儿子以外的儿子;

  • 重边:父亲结点和重儿子连成的边;

  • 轻边:父亲节点和轻儿子连成的边;

  • 重链:由多条重边连接而成的路径;

  • 轻链:由多条轻边连接而成的路径;

如图所示:

实现

树剖的实现分为两个dfs,如下:

dfs1

需要用的变量:

dep:每个节点的深度;

fa:每个节点的父亲;

siz:子树大小;

heavson:重儿子;


早期文章,未完结。

咕咕咕咕……

To be continued……

posted @ 2022-03-20 23:49  向日葵Reta  阅读(64)  评论(0编辑  收藏  举报