我的第一篇真正意义上的博客呢。。。有点紧张>_<
【概述】
线段树实际上是一棵完全二叉树,主要用于高效解决连续区间的动态查询问题(通过懒标记 lazy tag)。由于它二叉的结构,使得它的效率非常高(O(logn))。
线段树的每一个节点都表示一个区间,它的左儿子和右儿子分别表示它的左右半区间。例如父节点代表[a,b],设c=(a+b)/2,则左儿子代表[a,c],右儿子代表[c+1,b]。下面我们通过一道例题来体会一下线段树。
洛谷【P1531】I Hate It
【题目大意】
第一行输入两个数n,m分别代表数字个数和操作个数
第二行输入n个数,编号从1到n,分别代表这n个数的值
接下来m行有m个操作,格式如下:
Q A B 询问编号从A到B中(包括A,B)的最大值
U A B 若编号为A的数的值小于B,则将A的值改为B,否则不变
输入样例
5 6
1 2 3 4 5
Q 1 5
U 3 6
Q 3 4
Q 4 5
U 2 9
Q 1 5
输出样例
5
6
5
9
【解题步骤】
1.建树
网上基本都是静态开点建线段树,需要的空间为4n。这里介绍一种动态开点的方法,只需要2n的空间。
1 #define N 400100 2 struct node { 3 int l, r, lc, rc, max; //左端点,右端点,左儿子,右儿子,区间最大值(懒标记) 4 } tr[N]; 5 void pushup(int l, int r, int rt) { 6 tr[rt].max = max(tr[l].max, tr[r].max); //懒标记上浮 7 } 8 void build(int l, int r, int & rt) { 9 rt = ++tot; 10 tr[rt].l = l, tr[rt].r = r; //此题并不需要这行代码,但有的线段树会用到 11 if (l == r) { //找到叶子结点 12 scanf("%d", &tr[rt].max); 13 return; 14 } 15 int mid = (l + r) >> 1; //位运算,相当于 (l + r) / 2 16 build(l, mid, tr[rt].lc); //建左子树 17 build(mid + 1, r, tr[rt].rc); //建右子树 18 pushup(tr[rt].lc, tr[rt].rc, rt); 19 }
2.查询
1 int getans(int l, int r, int rt) { 2 if (a <= l && b >= r) //查询区间覆盖当前区间 3 return tr[rt].max; 4 int ans = -99999999, mid = (l + r) >> 1; 5 if (a <= mid) //二分查找左儿子 6 ans = max(ans, getans(l, mid, tr[rt].lc)); 7 if (b > mid) //查找右儿子 8 ans = max(ans, getans(mid + 1, r, tr[rt].rc)); 9 return ans; 10 }
3.修改
1 void update(int l, int r, int rt) { 2 if (l == r) { //找到要修改的叶子结点 3 if (tr[rt].max < b) 4 tr[rt].max = b; 5 return; 6 } 7 int mid = (l + r) >> 1; 8 if (a <= mid) 9 update(l, mid, tr[rt].lc); 10 else //不在左边就在右边 11 update(mid + 1, r, tr[rt].rc); 12 pushup(tr[rt].lc, tr[rt].rc, rt); //别忘了上浮 13 }
接下来上完整代码辣^w^
1 #include <cstdio> 2 #include <iostream> 3 using namespace std; 4 #define N 400100 5 int n, m, root, tot, a, b; 6 char c; 7 struct node { 8 int l, r, lc, rc, max; //左端点,右端点,左儿子,右儿子,区间最大值(懒标记) 9 } tr[N]; 10 void pushup(int l, int r, int rt) { 11 tr[rt].max = max(tr[l].max, tr[r].max); //懒标记上浮 12 } 13 void build(int l, int r, int & rt) { 14 rt = ++tot; 15 tr[rt].l = l, tr[rt].r = r; //此题并不需要这行代码,但有的线段树会用到 16 if (l == r) { //找到叶子结点 17 scanf("%d", &tr[rt].max); 18 return; 19 } 20 int mid = (l + r) >> 1; //位运算,相当于 (l + r) / 2 21 build(l, mid, tr[rt].lc); //建左子树 22 build(mid + 1, r, tr[rt].rc); //建右子树 23 pushup(tr[rt].lc, tr[rt].rc, rt); 24 } 25 int getans(int l, int r, int rt) { 26 if (a <= l && b >= r) //查询区间覆盖当前区间 27 return tr[rt].max; 28 int ans = -99999999, mid = (l + r) >> 1; 29 if (a <= mid) //二分查找左儿子 30 ans = max(ans, getans(l, mid, tr[rt].lc)); 31 if (b > mid) //查找右儿子 32 ans = max(ans, getans(mid + 1, r, tr[rt].rc)); 33 return ans; 34 } 35 void update(int l, int r, int rt) { 36 if (l == r && l == a) { //找到要修改的叶子结点 37 if (tr[rt].max < b) 38 tr[rt].max = b; 39 return; 40 } 41 int mid = (l + r) >> 1; 42 if (a <= mid) 43 update(l, mid, tr[rt].lc); 44 else //不在左边就在右边 45 update(mid + 1, r, tr[rt].rc); 46 pushup(tr[rt].lc, tr[rt].rc, rt); //别忘了上浮 47 } 48 int main() { 49 scanf("%d %d", &n, &m); //大数据用scanf效率更高 50 build(1, n, root); 51 while (m--) { 52 scanf("\n%c %d %d", &c, &a, &b); //换行符别忘了读 53 if (c == 'Q') 54 printf("%d\n", getans(1, n, 1)); 55 else 56 update(1, n, 1); 57 } 58 return 0; 59 }
以上就是线段树的入门了(然而还有一些神奇的操作没讲QAQ)。因为是第一篇博客,所以肯定有许多不足,望各位神犇勿喷。谢谢!QWQ