《标准线段树》
普通的线段树,其区间一般都是一个序列的数的个数,但还是要根据不同题目来判断
注意:tr[]空间是N*4,N为最大范围
《单点修改,区间查询》
原题:https://www.acwing.com/problem/content/1277/
像这道题,N最大为2*1e5,我们可以事先建立一颗最大范围1~N的线段树,然后不断分裂这个范围,产生子节点,
每一个节点维护的信息是,这个节点代表的区间的最大值,这里范围代表数的个数;
1 #include <iostream> 2 #include <algorithm> 3 #include <cstring> 4 using namespace std; 5 typedef long long LL; 6 const int N = 200010; 7 struct tree 8 { 9 int l, r; 10 //区间[l,r]中的最大值 11 int v; 12 } tr[N * 4]; 13 //由子节点维护的信息计算(或更新)父节点维护的信息 14 void pushup(int u) 15 { 16 //解释一下 u<<1|1这个操作: 17 //对于2的倍数,其二进制下的第一位总是0,根据这个特性|1,相当于+1; 18 tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v); 19 } 20 //将tr[u]这个位置初始化为树 21 void build(int u, int l, int r) 22 { 23 tr[u].l = l, tr[u].r = r; 24 if (l == r) 25 return; 26 //维护信息根据题目的不同,在代码中的写法也不同 27 //这里因为是以逐渐加点(在modify中)来产生信息, 28 //在最开始的建树过程是无维护信息的,所以这里不管tr[u].v; 29 int mid = l + r >> 1; 30 build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); 31 } 32 //单点修改,将tr[u]包含区间中的位置为x的值修改为v; 33 void modify(int u, int x, int v) 34 { 35 if (tr[u].l == x && tr[u].r == x) 36 tr[u].v = v; 37 else 38 { 39 int mid = tr[u].l + tr[u].r >> 1; 40 if (x <= mid) 41 modify(u << 1, x, v); 42 else 43 modify(u << 1 | 1, x, v); 44 //注意这个pushup()操作一定要写到这里,而不是写到外面 45 // pushup操作是更新父节点u的维护信息,其是写在子节点u<<1,u<<1|1被更新时写的 46 //如果写在外面,当 if (tr[u].l == x && tr[u].r == x)tr[u].v = v; 生效时 47 //这个时候u是无子节点的,那就会问:这个u点更新了,那如何更新其父节点呢? 48 //很简单,因为在modify时,我们会传入的u是1,即根节点,此后都是递归去更新 49 pushup(u); 50 } 51 } 52 //查询在点u包含的范围内中[l,r]的维护的信息 53 int query(int u, int l, int r) 54 { 55 if (tr[u].l >= l && tr[u].r <= r) 56 return tr[u].v; 57 //注意这里v是取题中数的最小值 58 int v = 0; 59 int mid = tr[u].l + tr[u].r >> 1; 60 if (l <= mid) 61 v = query(u << 1, l, r); 62 //注意:这里就不是else了 63 if (r > mid) 64 v = max(v, query(u << 1 | 1, l, r)); 65 return v; 66 } 67 int main() 68 { 69 int m, p, a = 0, x, n = 0; 70 cin >> m >> p; 71 char op[2]; 72 build(1, 1, m); 73 while (m--) 74 { 75 scanf("%s%d", op, &x); 76 if (op[0] == 'A') 77 { 78 //注意这里可能爆int; 79 modify(1, n + 1, ((LL)a + x) % p); 80 n++; 81 } 82 else 83 { 84 a = query(1, n - x + 1, n); 85 printf("%d\n", a); 86 } 87 } 88 return 0; 89 }
《时间复杂度》
《区间修改,区间查询》
对于区间修改,tree结构体中要多一个变量lazy,用来记录整个区间的修改,是缓存下放到修改子区间的值
注意:不是缓存修改本区间,本区间的维护的信息是要随时修改的
1 #include <iostream> 2 #include <algorithm> 3 #include <cstring> 4 using namespace std; 5 const int N = 100010; 6 typedef long long LL; 7 int w[N]; 8 struct tree 9 { 10 // sum是区间[l,r]要维护的信息 11 // lazy是在多次修改时,将修改的数先累加起来,即先不下放修改给子区间 12 //到这个区间的lazy不平衡时才下放(如[1,10],先整个区间+10,再[5,10]区间+5,这个时候区间lazy不能平衡,要下放) 13 //当前的sum是可以即时修改的 14 int l, r, lazy; 15 LL sum; 16 } tr[N * 4]; 17 //用子节点更新父节点u的操作 18 void pushup(int u) 19 { 20 tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum; 21 } 22 //将父节点u的lazy标记下放的操作 23 void pushdown(int u) 24 { 25 //如果积累下来的lazy有修改: 26 if (tr[u].lazy) 27 { 28 int sl = u << 1, sr = u << 1 | 1; 29 //注意lazy这里是+= 30 tr[sl].lazy += tr[u].lazy; 31 //注意可能会爆int 32 tr[sl].sum += (LL)(tr[sl].r - tr[sl].l + 1) * tr[u].lazy; 33 34 tr[sr].lazy += tr[u].lazy; 35 tr[sr].sum += (LL)(tr[sr].r - tr[sr].l + 1) * tr[u].lazy; 36 //全部下放成功,清空: 37 tr[u].lazy = 0; 38 } 39 } 40 void build(int u, int l, int r) 41 { 42 tr[u].l = l, tr[u].r = r; 43 44 if (l == r) 45 { 46 tr[u].lazy = 0; 47 tr[u].sum = w[l]; 48 return; 49 } 50 int mid = l + r >> 1; 51 build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); 52 //子节点变化,pushup 53 pushup(u); 54 } 55 //区间修改 56 void modify(int u, int l, int r, int d) 57 { 58 //注意这里与单点修改有着极不一样的地方 59 if (tr[u].l >= l && tr[u].r <= r) 60 { 61 //注意这里tr[u].sum是+= 62 tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * d; 63 tr[u].lazy += d; 64 return; 65 } 66 // u管辖的区间太大,接下来区间分裂,直到出现上面的情况 67 //区间分裂说明,在u管辖的区间只会有一部分区间加上修改,lazy不平衡,要pushdown(); 68 pushdown(u); 69 int mid = tr[u].l + tr[u].r >> 1; 70 if (l <= mid) 71 modify(u << 1, l, r, d); 72 if (r > mid) 73 modify(u << 1 | 1, l, r, d); 74 //上面u的子节点发生的变化,要pushup 75 pushup(u); 76 } 77 //区间询问 78 LL query(int u, int l, int r) 79 { 80 if (tr[u].l >= l && tr[u].r <= r) 81 return tr[u].sum; 82 //当要分裂询问时 83 //当然要懒标记下放,要不然父节点一直拿到修改值而不去修改也没用 84 pushdown(u); 85 int mid = tr[u].l + tr[u].r >> 1; 86 LL res = 0; 87 if (l <= mid) 88 res += query(u << 1, l, r); 89 if (r > mid) 90 res += query(u << 1 | 1, l, r); 91 return res; 92 } 93 int main() 94 { 95 int n, m; 96 cin >> n >> m; 97 for (int i = 1; i <= n; i++) 98 scanf("%d", &w[i]); 99 build(1, 1, n); 100 while (m--) 101 { 102 char op[2]; 103 int l, r, d; 104 scanf("%s%d%d", op, &l, &r); 105 if (op[0] == 'Q') 106 printf("%lld\n", query(1, l, r)); 107 else 108 { 109 scanf("%d", &d); 110 modify(1, l, r, d); 111 } 112 } 113 return 0; 114 } 115 //关于为啥modify哪里要pushdown,再pushup的原因 116 /* 由于modify直接修改了一些区间上面的add与sum,导致递归路径上的父节点全部都需要重新算一遍sum,所以递归结尾需要加一个pushup, 117 118 同时递归路径上如果存在带有懒标记的区间,则区间结尾的pushup会用两个子区间的返回值直接覆盖该区间的sum,但是该区间的懒标记add值还没有加到子区间上面去,也就是说懒标记的附加值就会被直接覆盖掉。 119 这种pushup会直接消灭懒标记,所以modify递归路径上必须全部消灭懒标记,即每一层递归都要先pushdown。 120 121 这里可以看到pushup和pushdown是一个组合,递归中每一层pushup保证了递归树的叶节点的改变对父节点的影响能够及时修正,每一层的pushdown都能保证本次的递归路径上一定不存在懒标记,保证本次递归路径上的sum赋值全部为正确值。 122 123 所以尽管查询操作并没有新增懒标记且保证递归路径上没有懒标记导致我们不需要pushup,但当我们在query的时候在每一层返回sum之前加一次pushup上述的一套组合依然能保证查询结果一定是正确的, 124 125 所以可以从这一套组合的性质出发直接 背下来,在简单运用线段树模板时候直接modify和query都用这一套,就省事很多了。 126 */ 127 //lazy不是对本区间的维护信息延迟,而是对本区间的子区间延迟 128 //本区间维护的信息是要时刻保持最新
《时间复杂度》
《小总结》
区间询问这里是真的要根据题目的不同而变化的,要考虑如何才能正确的维护信息