今天来讲一下经典的线段树。

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

简单的说,线段树是一种基于分治思想的数据结构,用来维护序列的区间特殊值,相对于树状数组,线段树可以做到更加通用,解决更多的区间问题。

  • 1.线段树的每一个节点都代表了一个区间
  • 2.线段树是一棵二叉树,具有唯一的根节点,其中,根节点代表的是整个区间\([1,n]\)
  • 3.线段树的每一个叶节点代表的是长度为\(1\)的元区间\([x,x]\)
  • 4.对于每一个节点\([l,r]\),它的左儿子被定义为\([l,mid]\),右儿子被定义为\([mid+1,r]\)

如图,这就是一棵维护了区间\([1,10]\)的线段树。

20180819213931568.png

我们还可以发现,线段树层数为\(log_2n\)层,除去最后一层,线段树是一棵完全二叉树。

我们来考虑一下如何储存并建立一棵线段树。

由于线段树是二叉树,所以我们可以直接用数组存储结点的编号,即对于节点\(x\)储存在\(a[p]\)处,我们令\(x\)的左儿子储存在\(a[p*2]\)处,右儿子储存在\(a[p*2+1]\)处,这样就可以快速地找到节点之间的父子关系。

理想状态下,\(n\)个叶节点的满二叉树有\((\sum_{i=0}^{2^i=n}2^i)=2n-1\)个节点,但由于最后一层至多还可能有\(2n\)个节点,所以数组空间要开到\(4n\)大小。

我们先来看一个维护区间最大值的例子。

对于线段树的每一个节点,我们可以额外的设置一个变量\(Max\)代表该节点所代表区间中的最大值,显然有:\(Max(p)=\max(Max(p*2),Max(p*2+1))\),那么我们可以用如下方法建树。

\(Code:\)

  1. struct SegmentTree
  2. {
  3. int p,l,r,Max;
  4. #define l(x) tree[x].l
  5. #define r(x) tree[x].r
  6. #define p(x) tree[x].p
  7. #define Max(x) tree[x].Max
  8. }tree[N*4];
  9. inline void build(int p,int l,int r)//对于节点p,代表的区间为[l,r]
  10. {
  11. l(p)=l,r(p)=r;//左右边界赋值
  12. if(l==r){Max(p)=0;return;}//如果为叶节点,直接赋值为权值
  13. int mid=(l+r)/2;
  14. //递归构建子树
  15. build(p*2,l,mid);
  16. build(p*2+1,mid+1,r);
  17. Max(p)=max(Max(p*2),Max(p*2+1));//回溯更新最大值
  18. }

线段树支持节点的动态修改。

对于如 “将节点\(x\)修改权值为\(v\)” 的指令,线段树可以以自下向上的方式修改。具体地,可以从根节点作为入口进入,递归向下找到需要修改的节点,再在回溯过程中更新沿路祖先节点的最值信息。时间复杂度\(O(log_2n)\)

\(Code:\)

  1. inline void modify(int p,int x,int v)
  2. {
  3. if(l(p)==r(p))
  4. {
  5. Max(p)=v;
  6. return;
  7. }
  8. int mid=(l(p)+r(p))/2;
  9. if(x<=mid)modify(p*2,x,v);
  10. if(x>mid)modify(p*2+1,x,v);
  11. Max(p)=max(Max(p*2),Max(p*2+1));
  12. }

线段树还需要能够解决区间最值查询问题。

对于如 “查询区间\([l,r]\)的最大值” 的指令,线段树可以递归查找得到最大值。具体地,从根节点开始,递归执行以下过程:

  • 1.若\([l,r]\)完全覆盖了当前结点所代表的区间,返回当前结点区间中的最大值作为备选答案
  • 2.若左子节点与\([l,r]\)有重合部分,递归访问左子节点
  • 3.若右子节点与\([l,r]\)有重合部分,递归访问右子节点

可以证明,区间查询的时间复杂度至多为\(O(2log_2n)\)

\(Code:\)

  1. inline int query(int p,int l,int r)
  2. {
  3. if(l<=l(p)&&r>=r(p))return Max(p);
  4. int mid=(l(p)+r(p))/2;
  5. int res=-INF;
  6. if(l<=mid)res=max(res,query(p*2,l,r));
  7. if(r>mid)res=max(res,query(p*2+1,l,r));
  8. return res;
  9. }

至此,线段树的基本模型已经构成,我们通过一道模板题展示一下代码。

给定一个包含n个数的序列,初值全为0,现对这个序列有两种操作:
操作1:把 给定 第k1 个数改为k2;
操作2:查询 从第k1个数到第k2个数得最大值。(k1<=k2<=n)
所有的数都 <=100000

第一行给定一个整数n,表示有n个操作。
以下接着n行,每行三个整数,表示一个操作。
第一个树表示操作序号,第二个数为k1,第三个数为k2

若干行,查询一次,输出一次。

  1. 3
  2. 1 2 2
  3. 1 3 3
  4. 2 2 3
  1. 3

\(Code:\)

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int N=100000+200,INF=0x3f3f3f3f;
  4. int n;
  5. struct SegmentTree
  6. {
  7. int p,l,r,Max;
  8. #define l(x) tree[x].l
  9. #define r(x) tree[x].r
  10. #define p(x) tree[x].p
  11. #define Max(x) tree[x].Max
  12. }tree[N*4];
  13. inline void build(int p,int l,int r)
  14. {
  15. l(p)=l,r(p)=r;
  16. if(l==r){Max(p)=0;return;}
  17. int mid=(l+r)/2;
  18. build(p*2,l,mid);
  19. build(p*2+1,mid+1,r);
  20. Max(p)=max(Max(p*2),Max(p*2+1));
  21. }
  22. inline void modify(int p,int x,int v)
  23. {
  24. if(l(p)==r(p))
  25. {
  26. Max(p)=v;
  27. return;
  28. }
  29. int mid=(l(p)+r(p))/2;
  30. if(x<=mid)modify(p*2,x,v);
  31. if(x>mid)modify(p*2+1,x,v);
  32. Max(p)=max(Max(p*2),Max(p*2+1));
  33. }
  34. inline int query(int p,int l,int r)
  35. {
  36. if(l<=l(p)&&r>=r(p))return Max(p);
  37. int mid=(l(p)+r(p))/2;
  38. int res=-INF;
  39. if(l<=mid)res=max(res,query(p*2,l,r));
  40. if(r>mid)res=max(res,query(p*2+1,l,r));
  41. return res;
  42. }
  43. inline void input(void)
  44. {
  45. scanf("%d",&n);
  46. build(1,1,n);
  47. }
  48. inline void solve(void)
  49. {
  50. for(int i=1;i<=n;i++)
  51. {
  52. int index,k1,k2;
  53. scanf("%d%d%d",&index,&k1,&k2);
  54. if(index==1)modify(1,k1,k2);
  55. else printf("%d\n",query(1,k1,k2));
  56. }
  57. }
  58. int main(void)
  59. {
  60. input();
  61. solve();
  62. return 0;
  63. }


版权声明:本文为Parsnip原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/Parsnip/p/10467629.html