st表、树状数组与线段树 笔记与思路整理
已更新(2/3):st表、树状数组
st表、树状数组与线段树是三种比较高级的数据结构,大多数操作时间复杂度为O(log n),用来处理一些RMQ问题或类似的数列区间处理问题。
一、ST表(Sparse Table)
st表预处理时间复杂度O(n log n),查询O(1),但不支持在线更改,否则要重新进行预处理。
使用一个二维数组:st[i][j]存储i为起点,长度为2j的一段区间最值,即arr[i, i + 2j - 1]。
具体步骤(以最小值为例):
- 将st[i][0]赋值为arr[i];
- 利用动态规划思想,dp出st[i][j] = min(st[i][j - 1], st[i + 2j - 1][j - 1]) (1 ≤ i ≤ n, 1 ≤ j ≤ log2 n);
- 查询时,定义len为log2(r - l + 1),区间[l, r]的最小值为min(st[l][len],st[r - 2len + 1][len])。
总时间复杂度为O(n log n + q),q为请求数。
代码实现(两个st表分别求最大最小值):
#include <bits/stdc++.h> using namespace std; int stmin[60010][20], stmax[60010][20]; int n, q, arr[60010], minans, maxans; void init(){ for(int j = 1 ; j <= n ; j++)stmax[j][0]=stmin[j][0]=arr[j]; for(int i = 1 ; i <= log2(n) ; i++){ for(int j = 1 ; j <= n ; j++){ stmax[j][i] = stmax[j][i-1]; if(j + (1 << (i-1)) <= n ) stmax[j][i] = max(stmax[j][i], stmax[j+(1<<(i-1))][i-1]); stmin[j][i] = stmin[j][i-1]; if(j + (1 << (i-1)) <= n ) stmin[j][i] = min(stmin[j][i], stmin[j+(1<<(i-1))][i-1]); } } } void query(int l,int r){ int len = log2(r - l + 1); minans = min(stmin[l][len],stmin[r - (1 << len) + 1][len]); maxans = max(stmax[l][len],stmax[r - (1 << len) + 1][len]); } int main(){ scanf("%d %d", &n, &q); for(int i = 1 ; i <= n ; i++) scanf("%d", &arr[i]); init(); int l,r; for(int i = 1 ; i <= q ; i++ ){ scanf("%d %d", &l, &r); query(l, r); printf("%d %d\n", minans, maxans); } return 0; }
2019.9.13 upd:
一点优化:每次计算2n或log2n会比较慢,可以事先用两个数组初始化2n或log2n的值。递推公式:
Bin[0] = 1;
for(int i=1; i<20; i++)
Bin[i] = Bin[i-1] * 2; //Bin[i]表示2的i次方
Log[0] = -1;
for(int i=1; i<=200000; i++)
Log[i] = Log[i/2] + 1; //Log[i]表示以2为底i的对数
2019.9.20 upd:
预处理Bin数组(Bin[i] = 2i)与 1<<i 时间基本一致(但是log2(i)还是比较慢的,最好还是初始化Log数组)
二、树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)
树状数组是一种树状的结构(废话),但是只需要 O(n) 的空间复杂度。区间查询和单一修改复杂度都为 O(log n) ,经过差分修改后区间修改也可以达到 O(log n) ,但此时不能区间查询。通过维护多个数组可以达到 O(log n) 的区间修改与查询。
先来看一棵树(伪)。
一棵二叉树。
(图片均盗自网络QwQ)
如果要在一棵树上存储一个数组并且便于求和,我们可以想到让每个父节点存储其两个子节点的和。(就选择是你啦!线段树!)
为了达到 O(n) 的空间复杂度,删去一些节点(放弃线段树)后如下:
红色的为树状数组的节点,黑色为原始数组。每个树状数组的节点存储以其为根节点的子树上的所有值之和。
设 a[] 为原数组, t[] 为树状数组,则:
t[1] = a[1]; t[2] = a[1] + a[2]; t[3] = a[3]; t[4] = a[1] + a[2] + a[3] + a[4]; t[5] = a[5]; t[6] = a[5] + a[6]; t[7] = a[7]; t[8] = a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7] + a[8];
所以说,这棵树的(我自己没推出来的)规律是:
t[i] = a[i - 2k + 1] + a[i - 2k + 2] + ... + a[i]; //k为i的二进制中从最低位到高位连续零的长度
i的前缀和sum[i] = t[i] + t[i-2k1] + t[(i - 2k1) - 2k2] + ...;
设lowbit(i) = 2k , 则可以递推如下:
void add_node(int pos, int val){ //将节点pos增加val for(int i=pos; i<=n; i+=lowbit(i)){ t[i] += val; } } int ask(int pos){ //求节点pos前缀和 int ans = 0; for(int i=pos; i>0; i-=lowbit(i)){ ans += t[i]; } return ans; } int query_sum(int l, int r){ //利用前缀和求[l, r]总和 return ask(r) - ask(l); }
那么问题来了,怎么求这个 2k 呢?
有一个巧妙的(我自己也没推出来的)算法是:
lowbit(x) = x & (-x);
抄一段证明如下:
这里利用的负数的存储特性,负数是以补码存储的,对于整数运算 x&(-x)有
● 当x为0时,即 0 & 0,结果为0;//因此实际运算的时候如果真的出现了lowbit(0)会卡死,要从1开始存储
●当x为奇数时,最后一个比特位为1,取反加1没有进位,故x和-x除最后一位外前面的位正好相反,按位与结果为0。结果为1。
●当x为偶数,且为2的m次方时,x的二进制表示中只有一位是1(从右往左的第m+1位),其右边有m位0,故x取反加1后,从右到左第有m个0,第m+1位及其左边全是1。这样,x& (-x) 得到的就是x。
●当x为偶数,却不为2的m次方的形式时,可以写作x= y * (2^k)。其中,y的最低位为1。实际上就是把x用一个奇数左移k位来表示。这时,x的二进制表示最右边有k个0,从右往左第k+1位为1。当对x取反时,最右边的k位0变成1,第k+1位变为0;再加1,最右边的k位就又变成了0,第k+1位因为进位的关系变成了1。左边的位因为没有进位,正好和x原来对应的位上的值相反。二者按位与,得到:第k+1位上为1,左边右边都为0。结果为2^k。
总结一下:x&(-x),当x为0时结果为0;x为奇数时,结果为1;x为偶数时,结果为x中2的最大次方的因子。
1、区间查询与单点修改
具体讲解见上。
完整的树状数组单点修改和区间查询实现为:
(针对模板题:Luogu P3374)
#include <bits/stdc++.h> using namespace std; int a[500010], t[500010]; int n, m; int lowbit(int x){ return x & (-x); } void add_node(int pos, int val){ for(int i=pos; i<=n; i+=lowbit(i)){ t[i] += val; } } int query_node(int pos){ int ans = 0; for(int i=pos; i>0; i-=lowbit(i)){ ans += t[i]; } return ans; } int query_range(int l, int r){ return query_node(r) - query_node(l-1); } int main(){ cin >> n >> m; int opt, pos, l, r, num; for(int i=1; i<=n; i++){ scanf("%d", &a[i]); add_node(i, a[i]); } while(m--){ scanf("%d", &opt); if(opt == 1){ scanf("%d%d", &pos, &num); add_node(pos, num); } if(opt == 2){ scanf("%d%d", &l, &r); printf("%d\n", query_range(l, r)); } } return 0; }
2、单点查询与区间修改
那么,如何让线段树支持区间更改与单点查询呢?
设数组 b[i] = a[i] - a[i-1] ,用 t[] 表示 b[] 。
模拟算一次:
a[] = 1, 5, 4, 2, 3, 1, 2, 5
b[] = 1, 4, -1, -2, 1, -2, 1, 3
将区间[2, 5]加上1:
a[] = 1, 6, 5, 3, 4, 2, 2, 5
b[] = 1, 5, -1, -2, 1, -2, 0, 3
可以看到,只有 b[2] 和 b[6] 发生了变化。(即更改区间[l, r]时的节点l与节点r+1)因此,以 b[] 为原数组的 t[] 只需要执行两次 add_node() 即可。但是,在查询 a[i] 的时候就需要查询 b[1...i] 之和,在 log n 时间里只能查询单个节点的值。
完整的区间修改与单点查询代码实现:
(针对模板题:Luogu P3368)
#include <bits/stdc++.h> using namespace std; int a[500010], t[500010]; int n, m; int lowbit(int x){ return x & (-x); } void add_node(int pos, int val){ for(int i=pos; i<=n; i+=lowbit(i)){ t[i] += val; } } void add_range(int l, int r, int val){ add_node(l, val); add_node(r+1, -val); } int query_node(int pos){ int ans = 0; for(int i=pos; i>0; i-=lowbit(i)){ ans += t[i]; } return ans; } int main(){ cin >> n >> m; int opt, pos, l, r, num; for(int i=1; i<=n; i++){ scanf("%d", &a[i]); add_node(i, a[i] - a[i-1]); } while(m--){ scanf("%d", &opt); if(opt == 1){ scanf("%d%d%d", &l, &r, &num); add_range(l, r, num); } if(opt == 2){ scanf("%d", &pos); printf("%d\n", query_node(pos)); } } return 0; }
3、区间查询与区间修改
简单谈一下区间查询与区间修改的操作:
(本段参考了xenny的博客)
∑ni = 1a[i] = ∑ni = 1 ∑ij = 1t[j];
则 a[1] + a[2] + ... + a[n]
= (t[1]) + (t[1] + t[2]) + ... + (t[1] + t[2] + ... + t[n])
= n * t[1] + (n-1) * t[2] + ... + t[n]
= n * (t[1] + t[2] + ... + t[n]) - (0 * t[1] + 1 * t[2] + ... + (n - 1) * t[n])
所以上式可以变为∑ni = 1a[i] = n*∑ni = 1t[i] - ∑ni = 1( t[i] * (i - 1) );
因此,维护两个树状数组,t1[i] = t[i],t2[i] = t[i] * (i - 1);
具体修改及查询公式见完整代码实现:
(针对模板题:POJ 3468)
#include<iostream> #include<cstdio> using namespace std; int n, m, maxn = 1; long long a[500010], t1[500010], t2[500010]; int lowbit(int x){ return x & (-x); } void add_node(int pos, long long val){ for(int i=pos; i<=n; i+=lowbit(i)){ t1[i] += 1ll * val; t2[i] += 1ll * val * (pos-1); } } void add_range(int l, int r, long long val){ add_node(l, val); add_node(r+1, -val); } long long query_node(int pos){ long long ans = 0; for(int i=pos; i>0; i-=lowbit(i)){ ans += 1ll * pos * t1[i] - t2[i]; } return ans; } long long query_range(int l, int r){ return query_node(r) - query_node(l-1); } int main(){ ios::sync_with_stdio(false); cin >> n >> m; char opt; int pos, l, r, num; for(int i=1; i<=n; i++){ cin >> a[i]; add_node(i, a[i] - a[i-1]); } while(m--){ cin >> opt; if(opt == 'C'){ cin >> l >> r >> num; add_range(l, r, num); } if(opt == 'Q'){ cin >> l >> r; cout << query_range(l, r) << endl; } } return 0; }
三、线段树
每次基本操作(插入或删除)O(log n),但是可以在不改变时间复杂度的情况下修改数据。
(正在更新)咕咕咕