【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
中央情报局要研究敌人究竟演习什么战术,所以Tidy要随时向Derek汇报某一段连续的工兵营地一共有多少人,例如Derek问:“Tidy,马上汇报第3个营地到第10个营地共有多少人!”Tidy就要马上开始计算这一段的总人数并汇报。但敌兵营地的人数经常变动,而Derek每次询问的段都不一样,所以Tidy不得不每次都一个一个营地的去数,很快就精疲力尽了,Derek对Tidy的计算速度越来越不满:"你个死肥仔,算得这么慢,我炒你鱿鱼!”Tidy想:“你自己来算算看,这可真是一项累人的工作!我恨不得你炒我鱿鱼呢!”无奈之下,Tidy只好打电话向计算机专家Windbreaker求救,Windbreaker说:“死肥仔,叫你平时做多点acm题和看多点算法书,现在尝到苦果了吧!”Tidy说:"我知错了。。。"但Windbreaker已经挂掉电话了。Tidy很苦恼,这么算他真的会崩溃的,聪明的读者,你能写个程序帮他完成这项工作吗?不过如果你的程序效率不够高的话,Tidy还是会受到Derek的责骂的.
每组数据第一行一个正整数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条命令
对于每个Query询问,输出一个整数并回车,表示询问的段中的总人数,这个数保持在int以内。

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...)

浙公网安备 33010602011771号