超全面的线段树:从入门到入坟

超全面的线段树:从入门到入坟

\(Pre\):其实线段树已经学了很久了,突然线段树这个数据结构比较重要吧,现在想写篇全面的总结,帮助自己复习,同时造福广大\(Oier\)虽然线段树的思维难度并不高)。本篇立志做一篇最浅显易懂,最全面的线段树讲解,采用\(lyd\)写的《算法竞赛进阶指南》上的顺序,从最基础的线段树到主席树,本篇均会涉及,并且附有一定量的习题,以后可能会持续更新,那么现在开始吧!


目录

  • 更新日志
  • 线段树想\(AC\)之基本原理(雾*1
  • 线段树想偷懒之懒标记(雾*2
  • 线段树想应用之扫描线(雾*3
  • 线段树想瘦身之开点与合并(雾*4
  • 线段树想持久之主席树(雾*5
  • 线段树想...不,你不想

更新日志

5.11 update:修改部分字词,基本原理30%完成,大纲完成。
5.4 update:基本原理20%完成。

线段树想\(AC\)之基本原理

什么是线段树啊?

首先,你得有的基本知识。

然后。

以下内容摘自百度百科

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。

很懵?没关系,我们继续。

其实,线段树(\(Segment\) \(Tree\))是一种基于分治思想的二叉树结构,(Q1:为什么一定是二叉?)如果你学过树状数组,你会清楚地知道两者的差异性,并且随着学习的深入,你会发现线段树是一种更为通用的数据结构。

最基本的线段树包含以下几个概念:

  1. 线段树每个节点表示一个区间
  2. 线段树的唯一根节点表示整个区间统计范围,如[\(1,N\)]。
  3. 线段树的每个叶节点表示一个长度为\(1\)的元区间,如[\(x,x\)]。
  4. 线段树上的每个节点[\(l,r\)],它的左子节点是[\(l,mid\)],右子节点是[\(mid+1,r\)],其中\(mid=(l+r)/2\)(这是线段树的标准写法,也有其他不同的写法,但作为初学者,还是从标准入手好)。

如图,这就是一棵线段树。我们可以发现,当整个区间统计长度为\(2\)的整数次幂时,整棵线段树一定是一棵完全二叉树(Q2:为什么),那我们就可以用堆的编号方法来给线段树来编号啊(其实图中已经编好了)。

即:

  1. 根节点编号为\(1\)
  2. 编号为\(x\)的节点,它的左儿子编号为\(x*2\),右儿子编号为\(x*2+1\)

这样,我们就可以用一个数组来存所有节点的编号了!
至于正确性,,,既然你都学到线段树了,那就不用我说了吧。。。

诶等等,那万一整个区间长度不是\(2\)的整数次幂呢?

看这张图!

可以惊讶地发现,我们同样可以使用父子二倍标记法。正确性显然,只不过,正是因为这种情况,所以树的最后一层节点编号在数组中的位置可能不是连续的。

如果区间长度为\(N\),在最理想的状况下,即\(N\)\(2\)的整数次幂时,\(N\)个叶节点的满二叉树有\(N+N/2+N/4+...+1=2N-1\)个节点。只要不是这种情况,那就还有一层,所以我们保存线段树节点编号的数组长度要大于等于\(4N\)

于是线段树信息储存如下:

struct SegmentTree {
    int l, r;//每个区间左右端点
    int dat;//区间数据
    //其他一些附加信息
}sak[4*MAX];

当然,线段树的写法多种多样,这是最稳的一种,还有一种是记录左右儿子编号的,后面我们再说,\(zkw\)线段树就不介绍了吧。。。

建树

我们需要从根节点“\(1\)”出发,向下递归建树,并把每个节点所代表的区间赋给它。当到达了根节点,便传值,再向上维护信息。

以维护区间和为例,我们可以这样建树:

inline void build(int p, int l, int r) {
    sak[p].l = l, sak[p].r = r;
    if (l == r) {//叶节点赋值
        sak[p].sum = a[l];
        return;
    }
    int mid = (l + r) / 2;
    build(2*p, l, mid);//递归建左儿子树
    build(2*p + 1, mid + 1, r);//递归建右儿子树
    sak[p].sum = sak[2*p].sum + sak[2*p + 1].sum;//向上传递区间和的信息
}

单点修改

显然,每次操作,我们都需要从根节点开始遍历,递归找到需要修改的叶子节点,然后修改,然后向上传递信息。(Q3:正确性)

inline void change(int p, int x, int val) {
    if (sak[p].l == sak[p].r) { sak[p].sum = val; return; }//找到x位置
    int mid = (l + r) / 2;
    if (x <= mid) change(p*2, x, val)
    else change(p*2+1, x, val);
    sak[p].sum = sak[2*p].sum + sak[2*p + 1].sum;//向上传递区间和的信息   

因为整棵树的深度是\(logN\),所以单次修改的时间复杂度为\(O(logN)\)

区间查询

这里直接给出算法过程,正确性显然。

  1. 若当前节点所表示的区间已经被询问区间所完全覆盖,则立即回溯,并传回该点的信息。
  2. 若当前节点的左儿子所表示的区间已经被询问区间所完全覆盖,就递归访问它的左儿子。
  3. 若当前节点的右儿子所表示的区间已经被询问区间所完全覆盖,就递归访问它的右儿子。

以返回区间和为例:

inline ll ask(int p, int l, int r) {
    if (l <= sak[p].l && r >= sak[p].r) {//对应1操作
        return sak[p].sum;
    }
    pushdown(p);
    ll val = 0;
    int mid = (sak[p].l + sak[p].r) / 2;
    if (l <= mid) val += ask(2*p, l, r);//对应2操作
    if (r > mid) val += ask(2*p + 1, l, r);//对应3操作
    return val; 
} 

【例题】Can you answer on these queries 3

【习题】Interval GCD

Q&A

  • A1:你都看到这里了,就不用我说了吧。
  • A2:图+\(YY\)一下,无需多说。
  • A3:修改的节点只包含在递归时经过的区间中,所以只会对递归时经过的区间产生影响。

线段树想偷懒之懒标记

【引题】A Simple Problem with Intergers

【习题】[LG P3373] 线段树 2

【习题】[雅礼集训2017] 市场

Q&A

线段树想应用之扫描线

【引题】Atlantis

【例题】Stars in Your Window

Q&A

线段树想瘦身之开点与合并

【例题】Promotion Counting

Q&A

线段树想持久之主席树

【例题】K-th Number

Q&A

猜你喜欢

转载自www.cnblogs.com/silentEAG/p/10808978.html
今日推荐