比较简单的线段树入门

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了比较简单的线段树入门相关的知识,希望对你有一定的参考价值。

线段树是一种十分方便的数据结构,可以解决多段连续区间的查询问题

对比其他一些数据结构,线段树能够解决的问题是动态的,这也是线段树的特性

线段树的性质还有每个节点保存一个线段,以及左节点保存的线段是父节点保存的线段的左半段,右子节点反之

(即当父节点保存的线段为[1,n],左子节点保存的线段为[1,(1+n)/2],而右子节点保存的线段为[(1+n)/2+1,n])

由于线段树是一颗完全二叉树,所以每个操作的复杂度大概保持在O(log n)

线段树可以做到的操作有如下:

1)构建线段树;

2)区间查询;

3)区间或单点修改;

(单点查询即为左右端点相等的特殊区间查询,单点修改也是同理)

那话不多说(已经不少了),接下来就对以上几种操作举例说明(这里统一以维护区间的和为例)

1)线段树的构建

就是递归构造一个树结构

 1 int a[200010];//a[i]记录数组
 2 struct node{int val;}tree[400010];//tree用于构造线段树
 3 //root 当前父节点
 4 //l 当前节点保存区间的左端点
 5 //r 当前节点保存区间的右端点
 6 void bdt(int root,int l,int r)
 7 {
 8     if(l==r) //当节点保存的区间是一个点时,记录该元素
 9     {tree[root].val=a[l]; return;}
10     //递归构建子树
11     bdt(rt*2,l,(l+r)/2);
12     bdt(rt*2+1,(l+r)/2+1,r);
13     //将左右子节点的值回溯到当前节点上
14     tr[rt].val=tr[rt*2].val+tr[rt*2+1].val;
15 }

 

2)区间查询

区间的查询就是将要查询的区间划分为线段树上节点保存的区间,然后通过节点的合成得到目的区间的值

 1 //root,l,r同上
 2 //f 查询目标区间的左端点
 3 //t 查询目标区间的右端点
 4 int query(int root,int l,int r,int f,int t)
 5 {
 6     //如果查询区间与节点区间没有交集则返回0(0对求和没有影响)
 7     if(t<l || f>r)
 8     return 0;
 9     //如果节点区间包含于查询区间则返回当前区间的求和值
10     if(f<=l && t>=r)
11     return tree[root].val;
12     //左右子树递归将求和保存到父节点
13     return query(root*2,l,(l+r)/2,f,t)+query(root*2+1,(l+r)/2+1,r,f,t);
14 }

 

 可以见得,由于所选的区间尽量少,所以很大程度上节省了时间复杂度,大概节省到了O(log n)

还要记得使用线段树解决问题有一个条件——问题是可以分解解决的

3)单点及区间的修改

首先是单点修改

 1 //root,l,r仍然同上
 2 //k 要修改的点
 3 //add 要增加的值
 4 void update(int root,int l,int r,int k,int add)
 5 {
 6     //当节点是叶子节点执行修改
 7     if(l==r)
 8     {if(k==l) tree[root]+=ad; return;}
 9     //递归左右字数寻找目标节点
10     int mid=(l+r)/2;
11     if(k<=mid)
12     update(root*2,l,mid,k,add);
13     else update(root*2+1,mid+1,r,k,add);
14     //回溯更新父节点(可见回溯在线段树中还是很重要的)
15     tree[root]=tree[root*2]+tree[root*2+1];
16 }

然后是区间修改

区间修改要是也和上面一样那就失去了线段树的意义了(笑

于是就需要一个很有意思的操作——对每个点维护一个延迟标记

这个标记可以记录节点受到了什么样的修改,而这个修改会影响他的子节点

当我们找到一个节点并且判断需要考虑其子节点,就将这个节点的延迟标记向子节点传递,并将这个节点的标记清零

恩...由于这个操作新加入了一个元素,就破坏了以上代码的连续性,这里决定直接上例题(cogs上的一道模板题)

1317. 数列操作c

★★☆   输入文件:shuliec.in   输出文件:shuliec.out   简单对比
时间限制:1 s   内存限制:128 MB

所有答案小于4611686018427387904

【问题描述】

    假设有一列数 {Ai }(1 ≤ i ≤ n),n<=100000 ,支持如下两种操作:

(1)将 A i至A j 的值均增加 D 。( i,j,D 是输入的数)

(2) 输出 A s +A s+1 +…+A t 。( s, t 都是输入的数, S ≤ T )

根据操作要求进行正确操作并输出结果。

【输入格式】

    输入文件第一行一个整数 n ,

第二行为 n 个整数,表示 {A i } 的初始值。

第三行为一个整数 m ,表示操作数

v 下接 m 行,每行描述一个操作,有如下两种情况:

ADD i j d ( 表示将 A i至A j 的值均增加 D , 1<=i,j<=n , d 为整数 )

SUM s t (表示输出 A s +…+A t )

【输出格式】

   对于每一个 SUM 提问,输出结果

【输入输出样例】

输入:

shuliec.in

4

1 4 2 3

3

SUM 1 3

ADD 2 2 50

SUM 2 3

输出:

shuliec.out

7

56

思路:就是一道区间修改区间查询的线段树模板,以下代码

 1 #include<cstdio>
 2 #define LL long long
 3 using namespace std;
 4 int n,m,a[200010];
 5 struct node{LL val,mark;}tree[400010];//这里的mark即为延迟标记
 6 char s[10];
 7 void buildtree(int root,int l,int r)
 8 {
 9     //构建时要先把所有点的延迟标记清零
10     tree[root].mark=0;
11     if(l==r)
12     {tree[root].val=a[l]; return;}
13     buildtree(root*2,l,(l+r)/2);
14     buildtree(root*2+1,(l+r)/2+1,r);
15     tree[root].val=tree[root*2].val+tree[root*2+1].val;
16 }
17 //这就是将延迟标记向下传递的操作
18 void pushdown(int root,int x)
19 {
20     if(tree[root].mark!=0)
21     {
22         //考虑到可能有些节点并不需要继续向下查询,应该是"+="
23         tree[root*2].mark+=tree[root].mark;
24         tree[root*2+1].mark+=tree[root].mark;
25         //这可是个区间啊,每个节点的权值只增加标记值怎么能行
26         tree[root*2].val+=tree[root].mark*(x-(x>>1));
27         tree[root*2+1].val+=tree[root].mark*(x>>1);
28     }tree[root].mark=0;
29 }
30 LL query(int root,int l,int r,int f,int t)
31 {
32     if(t<l || f>r)
33     return 0;
34     if(f<=l && t>=r)
35     return tree[root].val;
36     pushdown(root,r-l+1);
37     return query(root*2,l,(l+r)/2,f,t)+query(root*2+1,(l+r)/2+1,r,f,t);
38 }
39 //备受期待的区间修改,前三个元素仍然是同上
40 //f,t分别是修改区间的左右端点,v则是需要增加的值
41 void add(int root,int l,int r,int f,int t,int v)
42 {
43     int mid=(l+r)/2;
44     //修改区间和节点区间没有交集,自然不考虑
45     if(f>r || t<l) return;
46     //节点区间包含于修改区间,更改区间权值,记录延迟标记
47     if(f<=l && t>=r)
48     {
49         tree[root].mark+=v;
50         tree[root].val+=(LL)v*(r-l+1);
51         return;
52     }
53     //将延迟标记向下传递
54     pushdown(root,(r-l+1));
55     //考虑和左子,右子是否有交集的情况
56     add(root*2,l,mid,f,t,v);
57     add(root*2+1,mid+1,r,f,t,v);
58     //重要的回溯
59     tree[root].val=tree[root*2].val+tree[root*2+1].val;
60 }
61 int main()
62 {
63     int x,y,w,i,j;
64     LL ans;
65     scanf("%d",&n);
66     for(i=1;i<=n;++i)
67     scanf("%d",&a[i]);
68     buildtree(1,1,n);
69     scanf("%d",&m);
70     for(i=1;i<=m;++i)
71     {
72         scanf("%s",s);
73         if(s[0]==S)
74         {
75             scanf("%d%d",&x,&y);
76             ans=query(1,1,n,x,y);
77             printf("%lld\n",ans);
78         }
79         if(s[0]==A)
80         {
81             scanf("%d%d%d",&x,&y,&w);
82             add(1,1,n,x,y,w);
83         }
84     }
85 }

 

以上是关于比较简单的线段树入门的主要内容,如果未能解决你的问题,请参考以下文章

线段树--从入门到精通

线段树--从入门到入土入门篇:没有pushdown的简单建树&查询&修改(单点+区间)

分块简单入门

主席树学习--入门

线段树基础

线段树合并