ST TableST 表 (Sparse Table, 稀疏表) 基于 倍增 思想,用于解决 可重复贡献问题† ^\dagger † ,支持在Θ ( 1 ) \Theta(1) Θ ( 1 ) 的时间内 区间查询 ,不支持在线修改。
预处理时间复杂度Θ ( n log n ) \Theta(n\log n) Θ ( n log n ) ,查询时间复杂度Θ ( 1 ) \Theta(1) Θ ( 1 ) 。
† : ^\dagger: † : 区间询问对应的运算符∗ * ∗ 满足x ∗ x = x x*x=x x ∗ x = x 和结合律( x ∗ y ) ∗ z = x ∗ ( y ∗ z ) (x*y)*z=x*(y*z) ( x ∗ y ) ∗ z = x ∗ ( y ∗ z ) ,如max , min , ⊕ , ∣ , & , gcd \max,\ \min,\ \oplus,\ |\ ,\ \&,\ \gcd max , min , ⊕ , ∣ , & , g cd 等,包括 RMQ (Range Maximum/Minimum Query) 问题和区间 GCD。
以查询区间的最大值、最小值为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 int arr[N];int Log2[N], hi[33 ][N], lo[33 ][N];void beforeT () { for (int i = 2 ; i < N; ++i) Log2[i] = Log2[i / 2 ] + 1 ; } void init (int n) { for (int i = 1 ; i <= n; ++i) hi[0 ][i] = lo[0 ][i] = arr[i]; for (int i = 1 ; (1ll << i) < n; ++i) { for (int j = 1 ; j + (1ll << i) - 1 <= n; ++j) { hi[i][j] = max (hi[i-1 ][j], hi[i-1 ][j + (1ll << i - 1 )]); lo[i][j] = min (lo[i-1 ][j], lo[i-1 ][j + (1ll << i - 1 )]); } } } int querymx (int l, int r) { int d = Log2[r - l + 1 ]; return max (hi[d][l], hi[d][r - (1ll << d) + 1 ]); } int querymn (int l, int r) { int d = Log2[r - l + 1 ]; return min (lo[d][l], lo[d][r - (1ll << d) + 1 ]); } void eachT () { int n = read (); for (int i = 1 ; i <= n; ++i) arr[i] = read (); init (n); for (int q = read (); q--; ) { int l = read (), r = read (); printf ("%d\n" , querymx (l, r) - querymn (l, r)); } }
例题 洛谷P2880
BIT树状数组 (bisrch Index Tree, 二进制下标树) 用于维护满足 结合律 和 可差分 (即具有逆运算,如和、积、异或)的区间信息,支持在Θ ( n ) \Theta(n) Θ ( n ) 的空间和Θ ( log n ) \Theta(\log n) Θ ( log n ) 的时间内 单点修改 和 区间查询 。
树上节点p p p 存储区间( p − lowbit p , p ] (p-\operatorname{lowbit}p,\ p] ( p − l o w b i t p , p ] 的某个性质,其中lowbit p \operatorname{lowbit}p l o w b i t p 表示p p p 的二进制中 1 的最低位。前缀查询 [ 1 , p ] [1,p] [ 1 , p ] 可按二进制拆成对Θ ( log p ) \Theta(\log p) Θ ( log p ) 个子区间求和。
以维护区间的和(单点修改和区间查询)为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int n;ll tree[N]; inline int lowbit (int _n) { return _n & (-_n); }inline void update (int i, int num) { for (int p = i; p < N; p += lowbit (p)) tree[p] += num; } inline ll query (int i) { ll ans = 0 ; for (int p = i; p; p -= lowbit (p)) ans += tree[p]; return ans; } inline ll query (int l, int r) { return query (r) - query (l - 1 ); } void eachT () { memset (tree, 0 , sizeof tree); n = read (); for (int i = 1 ; i <= n; ++i) update (i, read ()); for (int q = read (); q--; ) { int op = read (); if (op) update (i, num); else print (query (l, r)); } }
例题 洛谷P3374
BIT.1 权值树状数组将序列从后向前倒着插入 BIT,插入a i a_i a i 前,统计已插入的比a i a_i a i 小的数字的数量 query(a[i])
,即是序列中满足j > i , a j < a i j>i,\ a_j<a_i j > i , a j < a i 的数量,也即a i a_i a i 对应的逆序对的数量。考虑到给出的数字较大,且存在负数,又注意到逆序数只与数字的相对大小有关,先 离散化 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int a[N];std::pair<int , int > b[N]; void eachT () { memset (tree, 0 , sizeof tree); n = read (); for (int i = 1 ; i <= n; ++i) b[i] = { read (),i }; std::sort (b + 1 , b + n + 1 ); for (int i = 1 ; i <= n; ++i) a[b[i].second] = i; ll sum = 0 ; for (int i = n; i >= 1 ; --i) { sum += query (a[i]); update (a[i], 1 ); } printf ("%lld\n" , sum); }
注 更标准的离散化写法,即复制、排序、去重、查找,时间复杂度Θ ( n log n ) \Theta(n\log n) Θ ( n log n ) :
1 2 3 4 5 6 7 8 int arr[N], val[N];void eachT () { memcpy (val, arr, sizeof arr); std::sort (val, val + n); int val_size = std::unique (val, val + val) - val; for (int i = 0 ; i < n; ++i) arr[i] = std::lower_bound (val, val + val_size, arr[i]) - val + 1 ; }
例题 HDU1394 Minimum Inversion Number
BIT.2 高阶前缀和 BIT.2.1 区间修改和单点查询利用差分,将此问题转化为单点修改和区间查询。具体地,记b i = a i − a i − 1 b_i = a_i - a_{i-1} b i = a i − a i − 1 ,则a i = ∑ b j a_i = \sum b_j a i = ∑ b j ,用 BIT 维护序列{ b i } \lbrace b_i\rbrace { b i } 的前缀和,修改[ l , r ] [l,r] [ l , r ] 即b l : = b l + x , b r + 1 : = b r + 1 − x b_l:=b_l+x,\ b_{r+1}:=b_{r+1}-x b l : = b l + x , b r + 1 : = b r + 1 − x 。
1 2 3 4 5 6 7 8 9 10 11 12 13 void eachT () { memset (tree, 0 , sizeof tree); n = read (); for (int i = 1 ; i <= n; ++i) { a[i] = read (); update (i, a[i] - a[i - 1 ]); } for (int q = read (); q--; ) { int op = read (); if (op) update (l, num), update (r + 1 , -num); else print (query (x)); } }
例题 洛谷P3368
BIT.2.2 区间修改和区间查询即线段树模板题。
类似地,利用差分,
s k = ∑ i = 1 k a i = ∑ i = 1 k ∑ j = 1 i b j = ∑ i = 1 k ∑ j = i k b i = ( k + 1 ) ∑ i = 1 k b i − ∑ i = 1 k i b i s_k = \sum_{i=1}^{k} a_i = \sum_{i=1}^{k} \sum_{j=1}^{i} b_j = \sum_{i=1}^{k} \sum_{j=i}^{k} b_i = (k+1) \sum_{i=1}^{k} b_i - \sum\limits_{i=1}^{k} ib_i s k = i = 1 ∑ k a i = i = 1 ∑ k j = 1 ∑ i b j = i = 1 ∑ k j = i ∑ k b i = ( k + 1 ) i = 1 ∑ k b i − i = 1 ∑ k i b i
用两个 BIT 分别维护序列{ b i } \lbrace b_i\rbrace { b i } 、{ i b i } \lbrace ib_i\rbrace { i b i } 的前缀和。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 int n;ll tree[N], tree2[N]; inline int lowbit (int _n) { return _n & (-_n); }inline void update (int i, int num) { for (int p = i; p < N; p += lowbit (p)) { tree[p] += num, tree2[p] += num * i; } } inline ll query (int i) { ll ans = 0 ; for (int p = i; p; p -= lowbit (p)) { ans += (i + 1 ) * tree[p] - tree2[p]; } return ans; } inline ll query (int l, int r) { return query (r) - query (l - 1 ); } void eachT () { memset (tree, 0 , sizeof tree); n = read (); for (int i = 1 ; i <= n; ++i) { a[i] = read (); update (i, a[i] - a[i-1 ]); } for (int q = read (); q--; ) { int op = read (); if (op) update (l, num), update (r + 1 , -num); else print (query (l, r)); } }
例题 洛谷P3372
BIT2.3 高阶前缀和一般地,对于m m m 阶前缀和,计算C k − x + m − 1 m − 1 C_{k-x+m-1}^{m-1} C k − x + m − 1 m − 1 ,得到关于x x x 的多项式a 0 x 0 + a 1 x 1 + a 2 x 2 + ⋯ a_0x^0 + a_1x^1 + a_2x^2 + \cdots a 0 x 0 + a 1 x 1 + a 2 x 2 + ⋯ ,则
s k = a 0 ∑ i = 1 k b i + a 1 ∑ i = 1 k i b i + a 2 ∑ i = 1 k i 2 b i + ⋯ s_k = a_0\sum\limits_{i=1}^{k} b_i + a_1 \sum\limits_{i=1}^{k} ib_i + a_2 \sum\limits_{i=1}^{k} i^2b_i + \cdots s k = a 0 i = 1 ∑ k b i + a 1 i = 1 ∑ k i b i + a 2 i = 1 ∑ k i 2 b i + ⋯
例如,当m = 3 m=3 m = 3 时,s k = ( k 2 + 3 k + 2 ) ∑ i = 1 k b i + ( − 2 k − 3 ) ∑ i = 1 k i b i + ∑ i = 1 k i 2 b i s_k = \left(k^2+3k+2\right)\sum\limits_{i=1}^{k} b_i + \left(-2k-3\right) \sum\limits_{i=1}^{k} ib_i + \sum\limits_{i=1}^{k} i^2b_i s k = ( k 2 + 3 k + 2 ) i = 1 ∑ k b i + ( − 2 k − 3 ) i = 1 ∑ k i b i + i = 1 ∑ k i 2 b i 。
BIT.2 多维 BIT二维数组的区间修改和区间查询:构造差分数组b i j b_{ij} b i j ,则
s u v = ∑ i = 1 u ∑ j = 1 v a i j = ∑ i = 1 u ∑ j = 1 v ∑ k = 1 i ∑ l = 1 j b k l = ∑ i = 1 u ∑ j = 1 v ∑ k = i u ∑ l = j v b i j = ∑ i = 1 u ∑ j = 1 v ( u − i + 1 ) ( v − j + 1 ) b i j = ( u + 1 ) ( v + 1 ) ∑ i = 1 u ∑ j = 1 v b i j − u ∑ i = 1 u ∑ j = 1 v j b i j − v ∑ i = 1 u ∑ j = 1 v i b i j + ∑ i = 1 u ∑ j = 1 v i j b i j \begin{aligned} s_{uv} = \sum\limits_{i=1}^{u}\sum\limits_{j=1}^{v} a_{ij} &= \sum\limits_{i=1}^{u}\sum\limits_{j=1}^{v} \sum\limits_{k=1}^{i}\sum\limits_{l=1}^{j} b_{kl} \\ &= \sum\limits_{i=1}^{u}\sum\limits_{j=1}^{v} \sum\limits_{k=i}^{u}\sum\limits_{l=j}^{v} b_{ij} \\ &= \sum\limits_{i=1}^{u}\sum\limits_{j=1}^{v} (u-i+1)(v-j+1)b_{ij} \\ &= (u+1)(v+1)\sum\limits_{i=1}^{u}\sum\limits_{j=1}^{v}b_{ij} -u\sum\limits_{i=1}^{u}\sum\limits_{j=1}^{v}jb_{ij} \\ &\qquad\ -v\sum\limits_{i=1}^{u}\sum\limits_{j=1}^{v}ib_{ij} +\sum\limits_{i=1}^{u}\sum\limits_{j=1}^{v}ijb_{ij} \end{aligned} s u v = i = 1 ∑ u j = 1 ∑ v a i j = i = 1 ∑ u j = 1 ∑ v k = 1 ∑ i l = 1 ∑ j b k l = i = 1 ∑ u j = 1 ∑ v k = i ∑ u l = j ∑ v b i j = i = 1 ∑ u j = 1 ∑ v ( u − i + 1 ) ( v − j + 1 ) b i j = ( u + 1 ) ( v + 1 ) i = 1 ∑ u j = 1 ∑ v b i j − u i = 1 ∑ u j = 1 ∑ v j b i j − v i = 1 ∑ u j = 1 ∑ v i b i j + i = 1 ∑ u j = 1 ∑ v i j b i j
用四个 BIT 分别维护序列{ b i j } \lbrace b_{ij}\rbrace { b i j } 、{ i b i j } \lbrace ib_{ij}\rbrace { i b i j } 、{ j b i j } \lbrace jb_{ij}\rbrace { j b i j } 、{ i j b i j } \lbrace ijb_{ij}\rbrace { i j b i j } 的前缀和。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 ll t1[N][N], t2[N][N], t3[N][N], t4[N][N]; inline int lowbit (int _n) { return _n & (-_n); }inline void update (int i, int j, int num) { for (int p = i; p < N; p += lowbit (p)) { for (int q = j; q < N; q += lowbit (q)) { t1[p][q] += num; t2[p][q] += j * num; t3[p][q] += i * num; t4[p][q] += i * j * num; } } } inline ll query (int i, int j) { ll ans = 0 ; for (int p = i; p; p -= lowbit (p)) { for (int q = j; q; q -= lowbit (q)) { ans += (i + 1 ) * (j + 1 ) * t1[p][q] - (i + 1 ) * t2[p][q] - (j + 1 ) * t3[p][q] + t4[p][q]; } } return ans; } void eachT () { for (int q = read (); q--; ) { int op = read (); if (op) { int x1 = read (), y1 = read (), x2 = read (), y2 = read (), num = read (); update (x1, y1, num); update (x2 + 1 , y1, -num); update (x1, y2 + 1 , -num); update (x2 + 1 , y2 + 1 , num); } else { int x1 = read (), y1 = read (), x2 = read (), y2 = read (); printf ("%lld\n" , query (x2, y2) - query (x1 - 1 , y2) - query (x2, y1 - 1 ) + query (x1 - 1 , y1 - 1 )); } } }
例题 洛谷P4514上帝造题的七分钟
BIT.3 后缀 BIT维护后缀和。交换 update
和 query
的跳转方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int n;ll tree[N]; inline int lowbit (int _n) { return _n & (-_n); }inline void update (int i, int num) { for (int p = i; p; p -= lowbit (p)) tree[p] += num; } inline ll query (int i) { ll ans = 0 ; for (int p = i; p < N; p += lowbit (p)) ans += tree[p]; return ans; }
对于可减信息,用前缀后缀 BIT 维护是一样的。但在当信息不可减且可转化为询问后缀时,能够不翻转序列地维护修改和查询。
Segment Tree线段树 (Segment Tree) 用于维护满足 结合律 的区间信息,支持在Θ ( log n ) \Theta(\log n) Θ ( log n ) 的时间内 区间修改 和 区间查询 。
线段树是一棵 平衡二叉树 ,树上每个 节点 p \pmb{p} p p 都存储一条 线段(区间) [ l , r ] \pmb{[l,r]} [ l , r ] [ l , r ] 的某个性质,节点p p p 的左右子节点的编号分别为 2 p 2p 2 p 和 2 p + 1 2p+1 2 p + 1 ,分别储存 [ l , m i d ] [l, \mathrm{mid}] [ l , m i d ] 和 [ m i d + 1 , r ] [\mathrm{mid}+1, r] [ m i d + 1 , r ] 。
修改时,使用 懒标记 m a r k p \pmb{\mathrm{mark}_p} m a r k p m a r k p ,表示该节点p p p 对应的区间上每一个点都要加上某个数,用到它的 子区间 的时候,再向下 传递 修改。
预处理时间复杂度Θ ( n ) \Theta(n) Θ ( n ) ,查询或修改时间复杂度Θ ( log n ) \Theta(\log n) Θ ( log n ) 。
例:维护区间的和,修改操作是区间增加某数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 struct SEGTREE { ll val; ll mrk; } tree[N << 2 ]; int n, arr[N];#define ls p<<1 #define rs p<<1|1 #define mid (cl+cr>>1) inline void push_up (SEGTREE& p, SEGTREE l, SEGTREE r) { p.val = l.val + r.val; } inline void push_down (SEGTREE& p, int mrk, int len) { p.val += mrk * len; p.mrk += mrk; } inline void push_up (int p) { push_up (tree[p], tree[ls], tree[rs]); }inline void push_down (int p, int len) { if (len <= 1 ) return ; push_down (tree[ls], tree[p].mrk, len - len / 2 ); push_down (tree[rs], tree[p].mrk, len / 2 ); tree[p].mrk = 0 ; } void build (int p = 1 , int cl = 1 , int cr = n) { tree[p].mrk = 0 ; if (cl == cr) return tree[p].val = arr[cl], void (); build (ls, cl, mid); build (rs, mid + 1 , cr); push_up (p); } SEGTREE query (int l, int r, int p = 1 , int cl = 1 , int cr = n) { if (cl >= l && cr <= r) return tree[p]; push_down (p, cr - cl + 1 ); if (mid >= r) return query (l, r, ls, cl, mid); if (mid < l) return query (l, r, rs, mid + 1 , cr); SEGTREE ans; push_up (ans, query (l, r, ls, cl, mid), query (l, r, rs, mid + 1 , cr)); return ans; } void update (int l, int r, ll d, int p = 1 , int cl = 1 , int cr = n) { if (cl >= l && cr <= r) return push_down (tree[p], d, cr - cl + 1 ); push_down (p, cr - cl + 1 ); if (mid >= l) update (l, r, d, ls, cl, mid); if (mid < r) update (l, r, d, rs, mid + 1 , cr); push_up (p); } void eachT () { n = read (); for (int i = 1 ; i <= n; ++i) arr[i] = read (); build (); for (int q = read (); q--; ) op = read () ? update (l, r, d) : print (query (l, r).val); }
例题 洛谷P3372 ,洛谷P3373 (利于理解懒标记)
SegTree.1 动态开点当值域为v v v 时,普通的线段树的空间复杂度是Θ ( 4 v ) \Theta(4v) Θ ( 4 v ) 。如果v v v 巨大,但查询次数q q q 较小时,边修改边建树,这样空间的复杂度是Θ ( q log v ) \Theta(q\log v) Θ ( q log v ) 。
递归的起点是区间[ L , R ] [L,R] [ L , R ] ,依数据范围修改,允许是负数。注意,这里的 >>1
不能写为 /2
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 struct SEGTREE { ll val, mrk; int ls, rs; } tree[N]; int L, R, pcnt; #define mid (cl+cr>>1) inline void push_up (SEGTREE& p, SEGTREE l, SEGTREE r) { p.val = l.val + r.val; } inline void push_down (int & p, int mrk, int len) { if (!p) p = ++pcnt; tree[p].val += mrk * len; tree[p].mrk += mrk; } inline void push_up (int p) { push_up (tree[p], tree[tree[p].ls], tree[tree[p].rs]); }inline void push_down (int p, int len) { if (!tree[p].mrk) return ; push_down (tree[p].ls, tree[p].mrk, len - len / 2 ); push_down (tree[p].rs, tree[p].mrk, len / 2 ); tree[p].mrk = 0 ; } SEGTREE query (int l, int r, int p = 1 , int cl = L, int cr = R) { if (cl >= l && cr <= r) return tree[p]; push_down (p, cr - cl + 1 ); if (mid >= r) return query (l, r, tree[p].ls, cl, mid); if (mid < l) return query (l, r, tree[p].rs, mid + 1 , cr); SEGTREE ans; push_up (ans, query (l, r, tree[p].ls, cl, mid), query (l, r, tree[p].rs, mid + 1 , cr)); return ans; } void update (int l, int r, ll d, int p = 1 , int cl = L, int cr = R) { if (cl >= l && cr <= r) return push_down (p, d, cr - cl + 1 ); push_down (p, cr - cl + 1 ); if (mid >= l) update (l, r, d, tree[p].ls, cl, mid); if (mid < r) update (l, r, d, tree[p].rs, mid + 1 , cr); push_up (p); } void eachT () { int n = read (); L = 1 , R = n, pcnt = 1 ; for (int q = read (); q--; ) op = read () ? update (l, r, d) : print (query (l, r).val); }
SegTree.2 线段树二分具有单调性才能二分!
例:非负序列,单点修改,全局查询前缀和大于S S S 的第一个位置p p p ,n ⩽ 5 × 1 0 5 , q ⩽ 5 × 1 0 6 n \leqslant 5\times 10^{5},\ q \leqslant 5\times 10^{6} n ⩽ 5 × 1 0 5 , q ⩽ 5 × 1 0 6 。
线段树维护区间和。对于朴素二分,每次 check
需查询单点前缀和,一次询问复杂度Θ ( log 2 n ) \Theta(\log^{2}n) Θ ( log 2 n ) ;而线段树的分治结构 合并二分和查询的过程 ,实现Θ ( log n ) \Theta(\log n) Θ ( log n ) 的复杂度。
考察二分过程,第一次查询[ 1 , n ] [1, n] [ 1 , n ] 中点m m m 处的前缀和s m s_m s m ,它等于线段树根节点左儿子的权值。若s m > S s_m > S s m > S ,说明答案在[ 1 , m ] [1, m] [ 1 , m ] ,否则答案在[ m + 1 , n ] [m + 1, n] [ m + 1 , n ] 。对于前者,第二次查询[ 1 , m ] [1, m] [ 1 , m ] 中点m l m_{l} m l 处的前缀和s m l s_{m_{l}} s m l ,它等于根节点左儿子的左儿子的权值;对于后者,第二次查询[ m + 1 , r ] [m + 1, r] [ m + 1 , r ] 中点m r m_{r} m r 处的前缀和,它等于s m s_m s m 加上根节点右儿子的左儿子的权值……不断递归,直至进入叶子节点。
以cur \text{cur} cur 记录已查询到的累计前缀和。递归至节点p p p 时,查询左儿子的权值val p l \text{val}_{p_{l}} val p l ,比较cur + val p l \text{cur}+\text{val}_{p_{l}} cur + val p l 与S S S 的大小,如果cur + val p l > S \text{cur}+\text{val}_{p_{l}}>S cur + val p l > S ,向左递归,cur \text{cur} cur 不变;否则向右递归,并cur : = cur + val p l \text{cur}:=\text{cur}+\text{val}_{p_{l}} cur : = cur + val p l 。
1 2 3 4 5 6 7 8 9 int bisrch (ll& cur, ll lim, int p = 1 , int cl = 1 , int cr = n) { if (cur + tree[p].val <= lim) return cur += tree[p].val, -1 ; if (cl == cr) return cl; push_down (p); int res = bisrch (cur, lim, ls, cl, mid); if (res != -1 ) return res; return bisrch (cur, lim, rs, mid + 1 , cr); }
在区间[ l , r ] [l, r] [ l , r ] 查询前缀和大于S S S 的第一个位置p p p ,大同小异,直接给出代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int bisrch (int l, int r, ll& cur, ll lim, int p = 1 , int cl = 1 , int cr = n) { if (cl >= l && cr <= r) { if (cur + tree[p].val <= lim) return cur += tree[p].val, -1 ; if (cl == cr) return cl; } push_down (p); if (mid >= l) { int res = bisrch (l, r, cur, lim, ls, cl, mid); if (res != -1 ) return res; } if (mid < r) return bisrch (l, r, cur, lim, rs, mid + 1 , cr); return -1 ; }
线段树二分灵活性很强,没有绝对固定的模板。
SegTree.3 权值线段树用线段树维护 桶 ,在Θ ( log v ) \Theta(\log v) Θ ( log v ) 的时间内查询某个范围内的数出现的总次数、第k k k 大数、前驱后继等。空间复杂度Θ ( q log v ) \Theta(q\log v) Θ ( q log v ) 。
权值线段树无需懒标记 mrk
,由于是单点更新,也无需 push_down
和 push_up
函数,而是递归到底、区间相加,代码相对于普通线段树更短,这也是较 平衡树 的优势,但是当值域较大、询问较多时空间占用较大。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 struct SEGTREE { int val, ls, rs; } tree[N]; int L, R, rt, pcnt; #define mid (cl+cr>>1) void update (int x, int d, int & p = rt, int cl = L, int cr = R) { if (!p) p = ++pcnt; if (cl == cr) return tree[p].val += d, void (); if (x <= mid) update (x, d, tree[p].ls, cl, mid); if (x > mid) update (x, d, tree[p].rs, mid + 1 , cr); tree[p].val = tree[tree[p].ls].val + tree[tree[p].rs].val; } int query (int l, int r, int p = 1 , int cl = L, int cr = R) { if (cl >= l && cr <= r) return tree[p].val; if (mid >= r) return query (l, r, tree[p].ls, cl, mid); if (mid < l) return query (l, r, tree[p].rs, mid + 1 , cr); return query (l, r, tree[p].rs, mid + 1 , cr) + query (l, r, tree[p].ls, cl, mid); } inline void insert (int x) { update (x, 1 ); } inline void remove (int x) { update (x, -1 ); } inline int countl (int x) { return query (L, x - 1 ); }inline int countg (int x) { return query (x + 1 , R); }inline int rankof (int x) { return countl (x) + 1 ; } int kthnum (int x, int p = rt, int cl = L, int cr = R) { if (cl == cr) return cl; if (tree[tree[p].ls].val >= x) return kthnum (x, tree[p].ls, cl, mid); else return kthnum (x - tree[tree[p].ls].val, tree[p].rs, mid + 1 , cr); } inline int prenum (int x) { return kthnum (countl (x)); } inline int sucnum (int x) { return kthnum (tree[rt].val - countg (x) + 1 ); } void eachT () { L = -1e8 , R = 1e8 , rt = 0 , pcnt = 0 ; for (int q = read (); q--; ) { insert (x); remove (x); print (rankof (x), kthnum (x), prenum (x), sucnum (x)); } }
例题 洛谷P3369 (模板),洛谷P4309 最长上升子序列 (从前向后将i i i 插入到位置p i p_i p i ,如果倒着看插入过程(即把最终序列b b b 逐个删除),每次删除的数i i i 在剩余序列的第a i a_{i} a i 位。模拟这个删数过程,用权值线段树维护第k k k 小,就能得到序列b b b )
SegTree.4 线段树合并维护 从若干子结构合并成大结构 的合并过程中所有出现过的结构的完整信息,如 并查集 、树上每个节点的 子树信息 等。时空复杂度与开的总点数相同,为线性对数。
动态开点,rt[p]
表示节点p p p 所在树的根节点 unix。递归地将两棵线段树的节点信息对应合并:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int merge (int x, int y, int cl = L, int cr = R) { if (!x || !y) return x | y; int z = ++pcnt; if (cl == cr) return , z; push_down (x), push_down (y); tree[z].ls = merge (tree[x].ls, tree[y].ls, cl, mid); tree[z].rs = merge (tree[x].rs, tree[y].rs, mid + 1 , cr); push_up (z); return z; } void eachT () { L = 1 , R = n, pcnt = 0 ; for (int i = 1 ; i <= n; ++i) { rt[i] = ++pcnt; update (num, 1 , rt[i]); } for (int q = read (); q--; ) { rt[x] = rt[y] = merge (rt[x], rt[y]); } }
上面的写法保留原来两棵线段树的信息,创建了一棵新树,因此需要额外的一倍空间。如果不要求保留原来的信息,优先选择直接把第二棵树的信息加到第一棵树上,这不需要新建节点,但会毁掉原先的两棵树:
1 2 3 4 5 6 7 8 9 10 int merge (int x, int y, int cl = L, int cr = R) { if (!x || !y) return x | y; if (cl == cr) return , x; push_down (x), push_down (y); tree[x].ls = merge (tree[x].ls, tree[y].ls, cl, mid); tree[x].rs = merge (tree[x].rs, tree[y].rs, mid + 1 , cr); push_up (x); return x; }
不可以把自己和自己合并!
SegTree.5.1 线段树上树回忆 树的 DFS 序 :每个节点在 DFS 中的进出栈的时间序列。每棵子树的 DFS 序都是连续的,且子树上根节点 DFS 序最小。因此,u u u 的子树上所有节点的编号介于 dfsn[u]
和 dfsnR[u]
之间,其中 dfsnR[u]
是结束遍历u u u 的子树的最后一个节点的 DFS 序。
1 2 3 4 5 6 7 8 9 std::vector<int > E[N]; int unix, dfsn[N], dfsnR[N]; void dfs (int u, int fau = 0 ) { dfsn[u] = ++unix; for (auto v : E[u]) { if (v != fau) dfs (v, u); } dfsnR[u] = unix; }
这个良好性质 将树转化为 (广义的) 区间 ,对u u u 的 子树 的操作即是对 区间 dfsn[u]
∼ \sim ∼ dfsnR[u]
的操作。
这支持在Θ ( log n ) \Theta(\log n) Θ ( log n ) 的时间内实现单点修改查询、子树修改查询、路径修改查询,在树根固定的问题中应用广泛。
具体代码实现:Segment Tree on Tree
SegTree.5.2 线段树优化建图有n n n 个点,q q q 个询问,每次询问给出一个操作,在这些操作后求1 1 1 到n n n 的最短路。n ⩽ 1 × 1 0 5 , q ⩽ 1 × 1 0 5 n \leqslant 1\times 10^{5},\ q \leqslant 1\times 10^{5} n ⩽ 1 × 1 0 5 , q ⩽ 1 × 1 0 5 。
1 u v w
:连一条u → v u\to v u → v 的有向边,权值为w w w ;2 u l r w
:对于所有i ∈ [ l , r ] i\in[l,r] i ∈ [ l , r ] ,连一条u → i u\to i u → i 的有向边,权值为w w w ;3 u l r w
:对于所有i ∈ [ l , r ] i\in[l,r] i ∈ [ l , r ] ,连一条i → u i\to u i → u 的有向边,权值为w w w ;4 l1 r1 l2 r2 w
:对于所有i ∈ [ l 1 , r 1 ] , j ∈ [ l 2 , r 2 ] i\in[l_{1},r_{1}],j\in[l_{2},r_{2}] i ∈ [ l 1 , r 1 ] , j ∈ [ l 2 , r 2 ] ,连一条i → j i\to j i → j 的有向边,权值为w w w ;无论何种最短路方法,从建图就卡住了。注意到,题给建图是区间,联想到线段树,可以利用线段树减少连边数量,从而降低复杂度。