线段树入门 洛谷【P3372】【模板】线段树1

  我的第一篇真正意义上的博客呢。。。有点紧张>_<

【概述】

  线段树实际上是一棵完全二叉树,主要用于高效解决连续区间的动态查询问题(通过懒标记 lazy tag)。由于它二叉的结构,使得它的效率非常高(O(logn))。

  线段树的每一个节点都表示一个区间,它的左儿子和右儿子分别表示它的左右半区间。例如父节点代表[a,b],设c=(a+b)/2,则左儿子代表[a,c],右儿子代表[c+1,b]。下面我们通过一道例题来体会一下线段树。

 洛谷【P3372】【模板】线段树1

【题目大意】

第一行输入两个数n,m分别代表数字个数和操作个数(n<=100000, m<=100000)

第二行输入n个数,编号从1到n,分别代表这n个数的值

接下来m行有m个操作,格式如下:

  1 x y k 将区间[x,y]内每个数加上k

  2 x y 询问区间[x,y]内每个数的和

(保证在int64/long long数据范围内)

输入样例

5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4

输出样例

11
8
20

【解题步骤】

1.建树

  网上基本都是静态开点建线段树,需要的空间为4n。这里介绍一种动态开点的方法,只需要2n的空间。

 1 #define N 200100 //开两倍空间 
 2 struct node {
 3     int l, r, lc, rc; //左端点,右端点,左儿子,右儿子
 4     long long v, tag; //区间和,懒标记 
 5 } tr[N];
 6 void pushup(int lc, int rc, int rt) { //区间和上浮 
 7     tr[rt].v = tr[lc].v + tr[rc].v;
 8 }
 9 void build(int l, int r, int & rt) {
10     rt = ++tot;
11     tr[rt].l = l, tr[rt].r = r; //记录该节点区间端点 
12     if (l == r) { //叶子结点 
13         scanf("%lld", &tr[rt].v); //读入大数据用scanf更快 
14         return;
15     }
16     int mid = (l + r) >> 1; //位运算,相当于(l + r) / 2 
17     build(l, mid, tr[rt].lc); //建左子树 
18     build(mid + 1, r, tr[rt].rc); //建右子树 
19     pushup(tr[rt].lc, tr[rt].rc, rt); //上浮 
20 }

这里有一个上浮操作,是为了之后查询时只要找到覆盖的区间返回区间和就可以了,而不用一直找到叶子结点(详细代码参看 3.查询)。

2.修改

 1 void pushdown(int lc, int rc, int rt) { //懒标记下沉 
 2     tr[lc].tag += tr[rt].tag; //注意是加上父节点的懒标记 
 3     tr[rc].tag += tr[rt].tag;
 4     tr[lc].v += (tr[lc].r - tr[lc].l + 1) * tr[rt].tag; //修改区间和 
 5     tr[rc].v += (tr[rc].r - tr[rc].l + 1) * tr[rt].tag;
 6     tr[rt].tag = 0; //清零,防止干扰下次操作 
 7 }
 8 void update(int l, int r, int rt) {
 9     if (x <= l && y >= r) { //区间覆盖,[x,y]表示修改区间 
10         tr[rt].tag += k; //修改懒标记 
11         tr[rt].v += (r - l + 1) * k; //区间和加上区间长度与k的乘积 
12         return;
13     }
14     pushdown(tr[rt].lc, tr[rt].rc, rt); //下沉 
15     int mid = (l + r) >> 1;
16     if (x <= mid)
17         update(l, mid, tr[rt].lc); //修改左子树 
18     if (y > mid)
19         update(mid + 1, r, tr[rt].rc); //修改右子树 
20     pushup(tr[rt].lc, tr[rt].rc, rt); //上浮 
21 }

懒标记表示的是这整段区间都加上的值,因此在修改某段区间的一部分时懒标记要下沉(因为不再是同一个值了)。

3.查询

 1 long long getans(int l, int r, int rt) {
 2     if (x <= l && y >= r) //[x,y]表示查询区间 
 3         return tr[rt].v; //返回区间和 
 4     pushdown(tr[rt].lc, tr[rt].rc, rt); //下沉 
 5     long long ans = 0;
 6     int mid = (l + r) >> 1;
 7     if (x <= mid)
 8         ans += getans(l, mid, tr[rt].lc); 
 9     if (y > mid)
10         ans += getans(mid + 1, r, tr[rt].rc);
11     return ans;
12 }

接下来上完整代码辣^w^

 1 #include <cstdio>
 2 using namespace std;
 3 #define N 200100 //开两倍空间 
 4 int n, m, root, tot, p, x, y;
 5 long long k;
 6 struct node {
 7     int l, r, lc, rc; //左端点,右端点,左儿子,右儿子
 8     long long v, tag; //区间和,懒标记 
 9 } tr[N];
10 void pushup(int lc, int rc, int rt) { //区间和上浮 
11     tr[rt].v = tr[lc].v + tr[rc].v;
12 }
13 void pushdown(int lc, int rc, int rt) { //懒标记下沉 
14     tr[lc].tag += tr[rt].tag; //注意是加上父节点的懒标记 
15     tr[rc].tag += tr[rt].tag;
16     tr[lc].v += (tr[lc].r - tr[lc].l + 1) * tr[rt].tag; //修改区间和 
17     tr[rc].v += (tr[rc].r - tr[rc].l + 1) * tr[rt].tag;
18     tr[rt].tag = 0; //清零,防止干扰下次操作 
19 }
20 void build(int l, int r, int & rt) {
21     rt = ++tot;
22     tr[rt].l = l, tr[rt].r = r; //记录该节点区间端点 
23     if (l == r) { //叶子结点 
24         scanf("%lld", &tr[rt].v); //读入大数据用scanf更快 
25         return;
26     }
27     int mid = (l + r) >> 1; //位运算,相当于(l + r) / 2 
28     build(l, mid, tr[rt].lc); //建左子树 
29     build(mid + 1, r, tr[rt].rc); //建右子树 
30     pushup(tr[rt].lc, tr[rt].rc, rt); //上浮 
31 }
32 void update(int l, int r, int rt) {
33     if (x <= l && y >= r) { //区间覆盖,[x,y]表示修改区间 
34         tr[rt].tag += k; //修改懒标记 
35         tr[rt].v += (r - l + 1) * k; //区间和加上区间长度与k的乘积 
36         return;
37     }
38     pushdown(tr[rt].lc, tr[rt].rc, rt); //下沉 
39     int mid = (l + r) >> 1;
40     if (x <= mid)
41         update(l, mid, tr[rt].lc); //修改左子树 
42     if (y > mid)
43         update(mid + 1, r, tr[rt].rc); //修改右子树 
44     pushup(tr[rt].lc, tr[rt].rc, rt); //上浮 
45 }
46 long long getans(int l, int r, int rt) {
47     if (x <= l && y >= r) //[x,y]表示查询区间 
48         return tr[rt].v; //返回区间和 
49     pushdown(tr[rt].lc, tr[rt].rc, rt); //下沉 
50     long long ans = 0;
51     int mid = (l + r) >> 1;
52     if (x <= mid)
53         ans += getans(l, mid, tr[rt].lc); 
54     if (y > mid)
55         ans += getans(mid + 1, r, tr[rt].rc);
56     return ans;
57 }
58 int main() {
59     scanf("%d %d", &n, &m);
60     build(1, n, root);
61     while (m--) {
62         scanf("%d %d %d", &p, &x, &y);
63         if (p == 1) {
64             scanf("%lld", &k);
65             update(1, n, 1);
66         } else
67             printf("%lld\n", getans(1, n, 1));
68     }
69     return 0;
70 }

以上就是线段树的入门了(然而还有一些神奇的操作没讲QAQ)。因为是第一篇博客,所以肯定有许多不足,望各位神犇勿喷。谢谢!QwQ

posted @ 2018-06-01 23:47  rp++  阅读(962)  评论(0编辑  收藏  举报