【First Summer】线段树第一炮--模板及入门(建树+单点更新+区间求和的姿势)

    和all the companions 相约在博客园煞有介事地写一写。一是为了好玩_(:ω ν ∠ )_, 二也确实是督促自己及时总结。如果能帮助到像我一样基础不好的coding beginnnnners,也算给自己积点德。

    最近的训练一直以数据结构为主,线段树的题目颇多。做了一些线段树的题目,有些感悟,也看了网上很多神牛们的线段树教程,学到了很多姿♂势♂。这里也和大家分享。

    先上一道例题(被无数人拿来当线段树入门的例题---真心经典)(HDU 1166 --- 敌兵布阵):

        Submit传送楼:http://acm.hdu.edu.cn/submit.php?pid=1166

    

敌兵布阵

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 57934    Accepted Submission(s): 24483

Problem Description
C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了。A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些工兵营地的活动情况。由于采取了某种先进的监测手段,所以每个工兵营地的人数C国都掌握的一清二楚,每个工兵营地的人数都有可能发生变动,可能增加或减少若干人手,但这些都逃不过C国的监视。
中央情报局要研究敌人究竟演习什么战术,所以Tidy要随时向Derek汇报某一段连续的工兵营地一共有多少人,例如Derek问:“Tidy,马上汇报第3个营地到第10个营地共有多少人!”Tidy就要马上开始计算这一段的总人数并汇报。但敌兵营地的人数经常变动,而Derek每次询问的段都不一样,所以Tidy不得不每次都一个一个营地的去数,很快就精疲力尽了,Derek对Tidy的计算速度越来越不满:"你个死肥仔,算得这么慢,我炒你鱿鱼!”Tidy想:“你自己来算算看,这可真是一项累人的工作!我恨不得你炒我鱿鱼呢!”无奈之下,Tidy只好打电话向计算机专家Windbreaker求救,Windbreaker说:“死肥仔,叫你平时做多点acm题和看多点算法书,现在尝到苦果了吧!”Tidy说:"我知错了。。。"但Windbreaker已经挂掉电话了。Tidy很苦恼,这么算他真的会崩溃的,聪明的读者,你能写个程序帮他完成这项工作吗?不过如果你的程序效率不够高的话,Tidy还是会受到Derek的责骂的.
Input
第一行一个整数T,表示有T组数据。
每组数据第一行一个正整数N(N<=50000),表示敌人有N个工兵营地,接下来有N个正整数,第i个正整数ai代表第i个工兵营地里开始时有ai个人(1<=ai<=50)。
接下来每行有一条命令,命令有4种形式:
(1) Add i j,i和j为正整数,表示第i个营地增加j个人(j不超过30)
(2)Sub i j ,i和j为正整数,表示第i个营地减少j个人(j不超过30);
(3)Query i j ,i和j为正整数,i<=j,表示询问第i到第j个营地的总人数;
(4)End 表示结束,这条命令在每组数据最后出现;
每组数据最多有40000条命令
Output
对第i组数据,首先输出“Case i:”和回车,
对于每个Query询问,输出一个整数并回车,表示询问的段中的总人数,这个数保持在int以内。
Sample Input
1
10
1 2 3 4 5 6 7 8 9 10
Query 1 3
Add 3 6
Query 2 7
Sub 10 2
Add 6 3
Query 3 10
End
Sample Output
Case 1: 6 33 59
Author
Windbreaker
 
    现在看到类似这样查询的题目都有条件反射_(:ω ν ∠ )_,一般来说这种涉及到多种operation的操作线段树居多【也很可能是其他的知识点= =,这只是个表象】。然而再观察,题目是对于某个位置的修改和对某个区间和的查询,这多半可以确定是线段树的题了。再看数据规模,粗略估计最坏情况,普通暴力算法一定会T【学长区域赛亲测1000ms运算次数在4*107】。因此考虑线段树。
    什么是线段树?线段树就是一种利用二分思想进行数据维护和查询的数据结构,树形结构使得父节点和子节点之间存在着从属关系,这种从属关系最经典的应用就是区间求和。如将1~10十个数存到线段树当中,每个节点存储的是区间的和,那么就可以构造出如下的线段树。
 
    那么,这样一棵树是怎么建出来的?上代码,边看边说。
    
 1 void build(int l, int r, int rt)
 2 {
 3     if(l == r)
 4     {
 5         scanf("%d", &sum[rt]);
 6         return;
 7     }
 8     int m = (l+r) >> 1;
 9     build(l, m, rt<<1);
10     build(m+1, r, rt<<1|1);
11     pushUp(rt);
12 }

    建树函数通常都有三个参数,区间左端点l,区间右端点r和当前结点rt。至于函数内部,很明显是用递归实现的层层建树,直到l=r,即到达叶节点时才会返回。

    具体过程窝就不详细推了,如果看不太清可以自己拿build(1, 10, 1)自己手跑一遍_(:ω ν ∠ )_。

    这个建树函数需要特别提到的一点是,因为建树的操作由于递归的性质是存在顺序的,叶节点一定是从左至右依次访问,因此,在建树的过程中如果需要的话,就可以顺序读入题目给出的值辣!

    这个函数里面有一个非常扎眼的东西,就是pushUp(rt);

    这个东西,把他叫做上传函数。因为我们虽然是自上而下建树,但是读入节点的值却是从叶节点开始的。因此为了更新每一个子节点的父节点的值,就需要这样的一个上传函数,将子节点的信息上传到父节点。由于本题节点存储的是区间的和,因此pushUp函数应该是下面这个样子:

1 void pushUp(int rt)
2 {
3     sum[rt] = sum[rt<<1] + sum[rt<<1|1];
4 }

    这样我们就完成了线段树的空间分配辣!对了,很重要的是,为了防止你的程序会出现ACCESS VIOLATION,sum数组要开得足够大,窝习惯开到4倍节点数,也就是maxn<<2。当然,一般二倍就够用了。

    但是只完成了建树操作并没有什么卵用,或者说只完成了第一步,什么操作都还没有。下面就要写线段树的重头戏辣!更新(也叫维护)和查询。

    先说更新函数update。这道题运用到的update是单点的维护。也就是说,每个数据的更新针对某一个点而不针对某个区间内的每一个点【单点更新和区间更新的姿势不太一样,今天太晚了又是第一炮就先写单点更新了_(:ω ν ∠ )_】。

    还是先贴代码:

 1 void update(int i, int j, int l, int r, int rt)
 2 {
 3     if(l == r)
 4     {
 5         sum[rt] += j;
 6         return;
 7     }
 8     int m = (l+r) >> 1;
 9     if(i <= m)
10         update(i, j, l, m, rt<<1);
11     else
12         update(i, j, m+1, r, rt<<1|1);
13     pushUp(rt);
14 }

    根据题目的要求,维护操作的格式是Add i j和Sub i j,分别代表给第i个兵营加上和减去j人。将这二者统一其实很简单辣,初中老师就在不停的说弱化减法和除法,减法就是加上一个数的相反数,除法就是乘一个数的倒数。用到这里来Sub i j就可以先考虑成Add i -j。

    线段树也是树,因此要对它进行操作时,找点往往从树根开始二分二分二分直到找到我想要的点。这里是一样的,为了找到位置为i的点,我们从整个区间开始找起,找到l=

r也就到了区间当中只有一个点的情况,只要没手残一定就是我们要找的点了。所以我们可以看到,整体的框架还是递归,当l=r时进行操作并返回,还没到达叶节点的时候就二分进行查找,若在左子树就去update左子树;反之则update右子树。注意函数参数的变化。

    当l=r时我们会发现到达了叶节点, 这时候我们应该进行更新操作,由于我们需要做的操作就是对当前点的存储的值加上j,因此就sum[rt] += j即可。

    同样,由于我们的update操作是从叶子节点做起,那么就需要逐层上传,一直更新到根节点,利用的是同一个pushUp函数。

    下面,再看查询函数query:

1 int query(int _l, int _r, int l, int r, int rt)
2 {
3     if(_l <= l && r <= _r) return sum[rt];
4     int m = (l+r) >> 1;
5     int ret = 0;
6     if(_l <= m) ret += query(_l, _r, l, m, rt<<1);
7     if(_r > m) ret += query(_l, _r, m+1, r, rt<<1|1);
8     return ret;
9 }

    一看,嘛,query函数这么麻烦。其实也没多麻烦,因为是区间上的操作,必然会比单点的操作要(看起来)麻烦那么一丝。

    第一个判断条件是最关键的,当然也很好理解。当你要查询的区间[_l, _r]包含了当前区间[l, r]时,就一定要返回当前节点的sum值辣。

    ---那剩下的呢?!超出l和超出r的怎么办!

    ---别着急,用子树的思想去递归不就行辣!

    定义一个临时变量ret并初始化为0,如果_l比当前区间中点要靠左,那么区间值就要加上左子树的值;同理,_r比当前区间中点靠右,就要加上右子树的值。

    最后return ret就行辣。

    说着清楚不清楚的,反正单点更新+区间和查询的操作窝们都完成了。下面直接处理一下格式化输入输出就可以尝试第一次submit了,下面给个ac代码:

 1 #include <cstdio>
 2 #include <cstring>
 3 
 4 #define mem(a)  memset(a, 0, sizeof(a))
 5 
 6 using namespace std;
 7 
 8 const int max_n = 50005;
 9 int sum[max_n<<2], a[max_n];
10 
11 void pushUp(int rt)
12 {
13     sum[rt] = sum[rt<<1] + sum[rt<<1|1];
14 }
15 
16 void build(int l, int r, int rt)
17 {
18     if(l == r)
19     {
20         scanf("%d", &sum[rt]);
21         return;
22     }
23     int m = (l+r) >> 1;
24     build(l, m, rt<<1);
25     build(m+1, r, rt<<1|1);
26     pushUp(rt);
27 }
28 
29 void update(int i, int j, int l, int r, int rt)
30 {
31     if(l == r)
32     {
33         sum[rt] += j;
34         return;
35     }
36     int m = (l+r) >> 1;
37     if(i <= m)
38         update(i, j, l, m, rt<<1);
39     else
40         update(i, j, m+1, r, rt<<1|1);
41     pushUp(rt);
42 }
43 
44 int query(int _l, int _r, int l, int r, int rt)
45 {
46     if(_l <= l && r <= _r) return sum[rt];
47     int m = (l+r) >> 1;
48     int ret = 0;
49     if(_l <= m) ret += query(_l, _r, l, m, rt<<1);
50     if(_r > m) ret += query(_l, _r, m+1, r, rt<<1|1);
51     return ret;
52 }
53 
54 int main()
55 {
56     char op[15];
57     int t, c, d, n;
58     scanf("%d", &t);
59     for(int i = 1; i <= t; i++)
60     {
61         printf("Case %d:\n", i);
62         mem(sum);
63         mem(a);
64         scanf("%d", &n);
65         build(1, n, 1);
66         while(scanf("%s", op))
67         {
68             if(op[0] == 'E') break;
69             if(op[0] == 'Q')
70             {
71                 scanf("%d%d", &c, &d);
72                 printf("%d\n", query(c, d, 1, n, 1));
73             }
74             else if(op[0] == 'A')
75             {
76                 scanf("%d%d", &c, &d);
77                 update(c, d, 1, n, 1);
78             }
79             else if(op[0] == 'S')
80             {
81                 scanf("%d%d", &c, &d);
82                 update(c, -d, 1, n, 1);
83             }
84         }
85     }
86 }

    再来一次,HDU 1166 Submit传送门:http://acm.hdu.edu.cn/submit.php?pid=1166

    根据上面讲的下面给大家窝自己常用的板子以供参考:

 1 void pushUp(int rt)
 2 {
 3     sum[rt] = sum[rt<<1] + sum[rt<<1|1];
 4 }
 5 
 6 void build(int l, int r, int rt)
 7 {
 8     if(l == r)
 9     {
10         需要做的操作,可以为空;
11         return;
12     }
13     int m = (l+r) >> 1;
14     build(l, m, rt<<1);
15     build(m+1, r, rt<<1|1);
16     pushUp(rt);
17 }
18 
19 void update(int i, int j, int l, int r, int rt)
20 {
21     if(l == r)
22     {
23         单点更新的维护操作;
24         return;
25     }
26     int m = (l+r) >> 1;
27     if(i <= m)
28         update(i, j, l, m, rt<<1);
29     else
30         update(i, j, m+1, r, rt<<1|1);
31     pushUp(rt);
32 }
33 
34 int query(int _l, int _r, int l, int r, int rt)
35 {
36     if(_l <= l && r <= _r) return sum[rt];
37     int m = (l+r) >> 1;
38     int ret = 0;
39     if(_l <= m) ret += query(_l, _r, l, m, rt<<1);
40     if(_r > m) ret += query(_l, _r, m+1, r, rt<<1|1);
41     return ret;
42 }

    先这样先这样,闲下来再写后续(To Be Continued...)

posted @ 2015-08-04 01:33  R.Wang  阅读(226)  评论(0)    收藏  举报