线段树与树状数组
为了方便,以下规定线段树中每个节点所存储的区间均为 [L, R) 的形式,这样对点操作的时候可以当点树,对线段操作的时候可以当线段树,完美。
点树与线段树的区别:比如 [1, 2] 和 [3, 4] 覆盖了 [1, 4] 中的所有整点,但却没有覆盖 [1, 4] 整条线段。
线段树的核心在于区间分解,涉及到的主要操作有建树(Build)、更新(Update)和询问(Query),下面是用伪代码给出的操作框架。
void Operate(root, L, R) if (root.L == L && root.R == R) Get or set related information return mid = (root.L + root.R) / 2; if (R <= mid) Operate(root * 2, L, R); else if (L >= mid) Operate(root * 2 + 1, L, R); else Operate(root * 2, L, mid); Operate(root * 2 + 1, mid, R);
线段树可以用数组或者指针实现,数组大小建议开为4N。线段树的功能取决于树的节点中的信息以及以上操作对这些信息的处理。
当进行区间更新操作时,强烈安利 lazy-tag 思想,即(当所求区间等于当前节点区间时)标记先做在分支节点上,询问的时候(如果所求区间不等于当前节点区间)再将标记向下传递。
当涉及到的区间非常大或者以浮点数形式出现时,需要先进行一步离散化的操作(类似hash)。此时更能体现出区间设为 [L, R) 形式的好处,例如对于点树,[1, 3], [4, 6] 和 [1, 3], [5, 6] 都会被离散化为 [1, 2], [3, 4],看上去都覆盖了整个 [1, 4],但事实上只有前者覆盖了整个 [1, 6]。但若将区间表示为 [1, 4), [4, 7) 和 [1, 4), [5, 7) 则不会出现这个问题。
离散化的主要过程包括排序、去重与二分查找(节省时间和空间)。离散化也可以采用hash数组的方法,将较大的数映射到较小的数,但不适用于区间有负数或者浮点数的情形。
树状数组和线段树拥有相同的更新和查询复杂度O(logn)。树状数组的优点在于常数、空间复杂度和代码量都比较小,缺点在于应用范围比线段树要窄。
树状数组适用于单点更新区间查询的情形,或者利用差分也可以解决区间更新单点查询的情形。
树状数组的核心在于充分利用低位函数lowbit(x) = x & (-x)。对于给定的数组a,构造树状数组:c[n] = sum[n] - sum[n - lowbit(n)]。其中sum是数组a的前缀和(因而数组a不一定需要存起来)。树状数组的主要操作有更新(Update)和询问(Query),下面是操作框架。
int Query(int x) { // Top-down int sum = 0; for (; x > 0; x -= x & (-x)) sum += c[x]; return sum; } void Update(int x, int val) { // Bottom-up for (; x <= MAXX; x += x & (-x)) c[x] += val; }
注意:树状数组的有效下标从1开始,在Query函数中循环终止条件必须为 x>0 而不能是 x>=0。
Problems
题目大意:无更新操作,询问区间最大值与最小值的差(N<=50,000, Q<=200,000)。
线段树模板题,不必多说。
1 // Problem: poj3264 - Balanced Lineup 2 // Category: Segment Tree 3 // Author: Niwatori 4 // Date: 2016/07/21 5 6 #include <stdio.h> 7 #define MAX(a,b) ((a)>(b)?(a):(b)) 8 #define MIN(a,b) ((a)<(b)?(a):(b)) 9 #define INF 2000000000 10 11 struct Node{ 12 int L, R, maxV, minV; 13 } tree[200005]; 14 15 int h[50005]; 16 17 void Build(int root, int L, int R) 18 { 19 tree[root].L = L; 20 tree[root].R = R; 21 if (L + 1 == R) 22 { 23 tree[root].maxV = tree[root].minV = h[L]; 24 return; 25 } 26 27 int mid = (L + R) / 2; 28 Build(root * 2, L, mid); 29 Build(root * 2 + 1, mid, R); 30 31 tree[root].maxV = MAX(tree[root * 2].maxV, tree[root * 2 + 1].maxV); 32 tree[root].minV = MIN(tree[root * 2].minV, tree[root * 2 + 1].minV); 33 } 34 35 void Query(int root, int L, int R, int & ansmax, int & ansmin) 36 { 37 if (tree[root].L == L && tree[root].R == R) 38 { 39 ansmax = MAX(ansmax, tree[root].maxV); 40 ansmin = MIN(ansmin, tree[root].minV); 41 return; 42 } 43 44 int mid = (tree[root].L + tree[root].R) / 2; 45 if (R <= mid) 46 Query(root * 2, L, R, ansmax, ansmin); 47 else if (L >= mid) 48 Query(root * 2 + 1, L, R, ansmax, ansmin); 49 else { 50 Query(root * 2, L, mid, ansmax, ansmin); 51 Query(root * 2 + 1, mid, R, ansmax, ansmin); 52 } 53 } 54 55 int main() 56 { 57 int n, q; 58 scanf("%d%d", &n, &q); 59 for (int i = 1; i <= n; ++i) 60 scanf("%d", &h[i]); 61 Build(1, 1, n + 1); 62 while (q--) 63 { 64 int l, r, ansmax = -INF, ansmin = INF; 65 scanf("%d%d", &l, &r); 66 Query(1, l, r + 1, ansmax, ansmin); 67 printf("%d\n", ansmax - ansmin); 68 } 69 return 0; 70 }
POJ3468 - A Simple Problem with Integers
题目大意:区间更新,查询区间和(N, Q<=100,000)。
线段树模板题,lazy-tag思想经典练习,注意查询结果要开long long。
1 // Problem: poj3468 - A Simple Problem with Integers 2 // Category: Segment Tree 3 // Author: Niwatori 4 // Date: 2016/07/21 5 6 #include <stdio.h> 7 8 struct Node{ 9 int L, R; 10 long long sum, inc; 11 } tree[400005]; 12 13 long long a[100005]; 14 15 void Build(int root, int L, int R) 16 { 17 tree[root].L = L; 18 tree[root].R = R; 19 if (L + 1 == R) 20 { 21 tree[root].sum = a[L]; 22 tree[root].inc = 0; 23 return; 24 } 25 26 int mid = (L + R) / 2; 27 Build(root * 2, L, mid); 28 Build(root * 2 + 1, mid, R); 29 30 tree[root].sum = tree[root * 2].sum + tree[root * 2 + 1].sum; 31 } 32 33 void Update(int root, int L, int R, int val) 34 { 35 if (tree[root].L == L && tree[root].R == R) 36 { 37 tree[root].inc += val; 38 return; 39 } 40 41 tree[root].sum += val * (R - L); 42 int mid = (tree[root].L + tree[root].R) / 2; 43 if (R <= mid) 44 Update(root * 2, L, R, val); 45 else if (L >= mid) 46 Update(root * 2 + 1, L, R, val); 47 else { 48 Update(root * 2, L, mid, val); 49 Update(root * 2 + 1, mid, R, val); 50 } 51 } 52 53 long long Query(int root, int L, int R) 54 { 55 if (tree[root].L == L && tree[root].R == R) 56 return tree[root].sum + tree[root].inc * (R - L); 57 58 tree[root].sum += tree[root].inc * (tree[root].R - tree[root].L); 59 tree[2 * root].inc += tree[root].inc; 60 tree[2 * root + 1].inc += tree[root].inc; 61 tree[root].inc = 0; 62 63 int mid = (tree[root].L + tree[root].R) / 2; 64 if (R <= mid) 65 return Query(root * 2, L, R); 66 else if (L >= mid) 67 return Query(root * 2 + 1, L, R); 68 else 69 return Query(root * 2, L, mid) + Query(root * 2 + 1, mid, R); 70 } 71 72 73 int main() 74 { 75 int n, q; 76 scanf("%d%d", &n, &q); 77 for (int i = 1; i <= n; ++i) 78 scanf("%lld", &a[i]); 79 Build(1, 1, n + 1); 80 while (q--) 81 { 82 char flag; int l, r, s; 83 scanf("\n%c%d%d", &flag, &l, &r); 84 if (flag == 'Q') 85 printf("%lld\n", Query(1, l, r + 1)); 86 else 87 { 88 scanf("%d", &s); 89 Update(1, l, r + 1, s); 90 } 91 } 92 return 0; 93 }
题目大意:按照顺序在长度为L的墙上贴N张等高的海报(海报可视为连续区间)(N<=10,000, L<=10,000,000),求最后有几张海报没有被其他海报完全遮住。
本题是对瓷砖编号,海报覆盖范围又以瓷砖为单位,所以本质上还是点树。
为了求没被完全遮住的海报数,考虑按照从后往前的顺序贴海报,如果贴第 i 张海报时,发现其区间已经被前面的海报完全盖住,那么第 i 张海报就看不见了。于是变成一个区间更新区间查询的问题,即每次对海报的区间查询是否已被完全覆盖,然后将该海报插入线段树,节点中只需存储一个covered变量以表示该节点的区间是否被完全覆盖即可。由于区间范围比较大,所以要先进行一步离散化操作。细节:(1)如果用数组hash[10000000]来离散化,需要把hash设为全局数组;(2)Query函数中不可以写
tmp = Query(root * 2, L, mid) && Query(root * 2 + 1, mid, R);
否则可能由于逻辑优化导致后半部分递归不被调用。
1 // Problem: poj2528 - Mayor's posters 2 // Category: Segment Tree with Discretization 3 // Author: Niwatori 4 // Date: 2016/07/22 5 6 #include <stdio.h> 7 #include <algorithm> 8 #define MAXN 10010 9 #define MAXX 10000010 10 11 struct Poster{ 12 int l, r; 13 } poster[MAXN]; 14 15 struct Node{ 16 int L, R; 17 bool covered; 18 } tree[MAXN * 8]; 19 20 void Build(int root, int L, int R) 21 { 22 tree[root].L = L; 23 tree[root].R = R; 24 tree[root].covered = 0; 25 if (L + 1 == R) return; 26 27 int mid = (L + R) / 2; 28 Build(root * 2, L, mid); 29 Build(root * 2 + 1, mid, R); 30 } 31 32 bool Query(int root, int L, int R) // Update while query 33 { 34 if (tree[root].L == L && tree[root].R == R) 35 { 36 int tmp = tree[root].covered; 37 tree[root].covered = 1; 38 return tmp; 39 } 40 41 if (tree[root].covered) // Push-down 42 tree[2 * root].covered = tree[2 * root + 1].covered = 1; 43 int mid = (tree[root].L + tree[root].R) / 2, tmp; 44 if (R <= mid) 45 tmp = Query(root * 2, L, R); 46 else if (L >= mid) 47 tmp = Query(root * 2 + 1, L, R); 48 else { 49 int tmp1 = Query(root * 2, L, mid); 50 int tmp2 = Query(root * 2 + 1, mid, R); 51 tmp = tmp1 && tmp2; // Careful!!! 52 } 53 54 tree[root].covered = tree[2 * root].covered && tree[2 * root + 1].covered; 55 return tmp; 56 } 57 58 int find(int x[], int val, int cnt) 59 { 60 int l = 0, r = cnt; 61 while (l < r) // Search val in [l, r) 62 { 63 int mid = (l + r) / 2; 64 if (x[mid] == val) return mid; 65 if (x[mid] < val) l = mid + 1; else r = mid; 66 } 67 return -1; 68 } 69 70 int main() 71 { 72 int t; scanf("%d", &t); 73 while (t--) 74 { 75 int n, cnt = 0, ans = 0, x[MAXN * 2]; 76 scanf("%d", &n); 77 for (int i = 0; i < n; ++i) 78 { 79 scanf("%d%d", &poster[i].l, &poster[i].r); 80 x[cnt++] = poster[i].l; // Discretization process: 81 x[cnt++] = poster[i].r + 1; 82 } 83 84 std::sort(x, x + cnt); // 1. Sort 85 cnt = std::unique(x, x + cnt) - x; // 2. Unique 86 Build(1, 0, cnt); 87 88 for (int i = n - 1; i >= 0; --i) 89 { 90 int newl = find(x, poster[i].l, cnt); // 3. Binary search 91 int newr = find(x, poster[i].r + 1, cnt); 92 ans += !Query(1, newl, newr); 93 } 94 printf("%d\n", ans); 95 } 96 return 0; 97 }
题目大意:按照从下到上从左到右的顺序给出平面上N个点的坐标 (x, y)(N<=15000, 0<=x, y<=32000),根据每个点左下方点的个数对这些点进行分类。
由于题目已经按照纵坐标的顺序排好了,所以先给出的点一定在后给出的点的下方,于是只需要统计比给定点横坐标要小的点的个数,对各横坐标处的点的数量建树,化为一个单点更新区间查询的问题,下面利用树状数组来解决。细节:(1)起始的a数组为0,所以c数组即初始化为0;(2)点的横坐标可能为0,所以写树状数组的时候需要对点进行向右平移处理。
1 // Problem: poj2352 - Stars 2 // Category: Binary Indexed Tree 3 // Author: Niwatori 4 // Date: 2016/07/21 5 6 #include <stdio.h> 7 #include <string.h> 8 #define MAXN 15005 9 #define MAXX 32005 10 11 int c[MAXX], lev[MAXN] = {0}; 12 13 int Query(int x) 14 { 15 int sum = 0; 16 for (; x > 0; x -= x & (-x)) 17 sum += c[x]; 18 return sum; 19 } 20 21 void Update(int x) 22 { 23 for (; x <= MAXX; x += x & (-x)) 24 c[x]++; 25 } 26 27 int main() 28 { 29 int n; scanf("%d", &n); 30 memset(c, 0, sizeof(c)); 31 for (int i = 0; i < n; ++i) 32 { 33 int x, y; 34 scanf("%d%d", &x, &y); 35 lev[Query(x + 1)]++; 36 Update(x + 1); 37 } 38 for (int i = 0; i < n; ++i) 39 printf("%d\n", lev[i]); 40 return 0; 41 }