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[][], stmax[][];
int n, q, arr[], minans, maxans;
void init(){
for(int j = ; j <= n ; j++)stmax[j][]=stmin[j][]=arr[j];
for(int i = ; i <= log2(n) ; i++){
for(int j = ; j <= n ; j++){
stmax[j][i] = stmax[j][i-];
if(j + ( << (i-)) <= n ) stmax[j][i] = max(stmax[j][i], stmax[j+(<<(i-))][i-]);
stmin[j][i] = stmin[j][i-];
if(j + ( << (i-)) <= n ) stmin[j][i] = min(stmin[j][i], stmin[j+(<<(i-))][i-]);
}
}
}
void query(int l,int r){
int len = log2(r - l + );
minans = min(stmin[l][len],stmin[r - ( << len) + ][len]);
maxans = max(stmax[l][len],stmax[r - ( << len) + ][len]);
}
int main(){
scanf("%d %d", &n, &q);
for(int i = ; i <= n ; i++)
scanf("%d", &arr[i]);
init();
int l,r;
for(int i = ; i <= q ; i++ ){
scanf("%d %d", &l, &r);
query(l, r);
printf("%d %d\n", minans, maxans);
}
return ;
}
2019.9.13 upd:
一点优化:每次计算2n或log2n会比较慢,可以事先用两个数组初始化2n或log2n的值。递推公式:
Bin[] = ;
for(int i=; i<; i++)
Bin[i] = Bin[i-] * ; //Bin[i]表示2的i次方
Log[] = -;
for(int i=; i<=; i++)
Log[i] = Log[i/] + ; //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[] = a[];
t[] = a[] + a[];
t[] = a[];
t[] = a[] + a[] + a[] + a[];
t[] = a[];
t[] = a[] + a[];
t[] = a[];
t[] = a[] + a[] + a[] + a[] + a[] + a[] + a[] + a[];
所以说,这棵树的(我自己没推出来的)规律是:
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 = ;
for(int i=pos; i>; 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[], t[];
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 = ;
for(int i=pos; i>; i-=lowbit(i)){
ans += t[i];
}
return ans;
}
int query_range(int l, int r){
return query_node(r) - query_node(l-);
}
int main(){
cin >> n >> m;
int opt, pos, l, r, num;
for(int i=; i<=n; i++){
scanf("%d", &a[i]);
add_node(i, a[i]);
}
while(m--){
scanf("%d", &opt);
if(opt == ){
scanf("%d%d", &pos, &num);
add_node(pos, num);
}
if(opt == ){
scanf("%d%d", &l, &r);
printf("%d\n", query_range(l, r));
}
}
return ;
}
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[], t[];
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+, -val);
}
int query_node(int pos){
int ans = ;
for(int i=pos; i>; i-=lowbit(i)){
ans += t[i];
}
return ans;
}
int main(){
cin >> n >> m;
int opt, pos, l, r, num;
for(int i=; i<=n; i++){
scanf("%d", &a[i]);
add_node(i, a[i] - a[i-]);
}
while(m--){
scanf("%d", &opt);
if(opt == ){
scanf("%d%d%d", &l, &r, &num);
add_range(l, r, num);
}
if(opt == ){
scanf("%d", &pos);
printf("%d\n", query_node(pos));
}
}
return ;
}
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 = ;
long long a[], t1[], t2[];
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-);
}
}
void add_range(int l, int r, long long val){
add_node(l, val);
add_node(r+, -val);
}
long long query_node(int pos){
long long ans = ;
for(int i=pos; i>; 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-);
}
int main(){
ios::sync_with_stdio(false);
cin >> n >> m;
char opt;
int pos, l, r, num;
for(int i=; i<=n; i++){
cin >> a[i];
add_node(i, a[i] - a[i-]);
} 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 ;
}
三、线段树
每次基本操作(插入或删除)O(log n),但是可以在不改变时间复杂度的情况下修改数据。
(正在更新)咕咕咕
st表、树状数组与线段树 笔记与思路整理的更多相关文章
- bzoj 3110: [Zjoi2013]K大数查询 树状数组套线段树
3110: [Zjoi2013]K大数查询 Time Limit: 20 Sec Memory Limit: 512 MBSubmit: 1384 Solved: 629[Submit][Stat ...
- [BZOJ 3196] 213平衡树 【线段树套set + 树状数组套线段树】
题目链接:BZOJ - 3196 题目分析 区间Kth和区间Rank用树状数组套线段树实现,区间前驱后继用线段树套set实现. 为了节省空间,需要离线,先离散化,这样需要的数组大小可以小一些,可以卡过 ...
- [BZOJ 1901] Dynamic Rankings 【树状数组套线段树 || 线段树套线段树】
题目链接:BZOJ - 1901 题目分析 树状数组套线段树或线段树套线段树都可以解决这道题. 第一层是区间,第二层是权值. 空间复杂度和时间复杂度均为 O(n log^2 n). 线段树比树状数组麻 ...
- POJ 1195 Mobile phones (二维树状数组或线段树)
偶然发现这题还没A掉............速速解决了............. 树状数组和线段树比较下,线段树是在是太冗余了,以后能用树状数组还是尽量用......... #include < ...
- 【BZOJ3196】二逼平衡树(树状数组,线段树)
[BZOJ3196]二逼平衡树(树状数组,线段树) 题面 BZOJ题面 题解 如果不存在区间修改操作: 搞一个权值线段树 区间第K大--->直接在线段树上二分 某个数第几大--->查询一下 ...
- BZOJ.4553.[HEOI2016&TJOI2016]序列(DP 树状数组套线段树/二维线段树(MLE) 动态开点)
题目链接:BZOJ 洛谷 \(O(n^2)\)DP很好写,对于当前的i从之前满足条件的j中选一个最大值,\(dp[i]=d[j]+1\) for(int j=1; j<i; ++j) if(a[ ...
- P3157 [CQOI2011]动态逆序对(树状数组套线段树)
P3157 [CQOI2011]动态逆序对 树状数组套线段树 静态逆序对咋做?树状数组(别管归并QWQ) 然鹅动态的咋做? 我们考虑每次删除一个元素. 减去的就是与这个元素有关的逆序对数,介个可以预处 ...
- HDU 5618 Jam's problem again(三维偏序,CDQ分治,树状数组,线段树)
Jam's problem again Time Limit: 5000/2500 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Othe ...
- BZOJ 1901 Zju2112 Dynamic Rankings 树状数组套线段树
题意概述:带修改求区间第k大. 分析: 我们知道不带修改的时候直接上主席树就可以了对吧?两个版本号里面的节点一起走在线段树上二分,复杂度是O((N+M)logN). 然而这里可以修改,主席树显然是凉了 ...
随机推荐
- 怎样实现给DEDE5.7的栏目增加栏目图片
前两天用DEDE做二次开发的时候,遇到一个问题,领导让给每个栏目增加一个栏目图片的功能,网上找了些东西,结合自己实际做的时候的方法,下面详细描述下具体的实现方式(只测试了V5.7版本,对低版本是否适用 ...
- 多线程基础(主要内容转载于https://segmentfault.com/a/1190000014428190)
进程作为资源分配的基本单位 线程作为资源调度的基本单位,是程序的执行单元,执行路径(单线程:一条执行路径,多线程:多条执行路径).是程序使用CPU的最基本单位. 线程有3个基本状态: 执行.就绪.阻塞 ...
- 项目二:企业级java电商网站开发(服务端)
声明:项目源于网络,支持正版教程,学习使用,仅记录在此 项目介绍 企业级java电商网站开发(服务端),模块划分:用户管理,商品管理,商品品类管理,订单管理,订单详情管理,购物车管理,收货地址管理,支 ...
- 02-28 scikit-learn库之线朴素贝叶斯
目录 scikit-learn库之朴素贝叶斯 一.MultinomialNB 1.1 使用场景 1.2 代码 1.3 参数详解 1.4 属性 1.5 方法 二.GaussianNB 三.Bernoul ...
- python编程基础之十七
字符串:str1 = '123' str2 ="123" str3 = """123""" str4 = '''123' ...
- BZOJ [Scoi2015]情报传递
Description 奈特公司是一个巨大的情报公司,它有着庞大的情报网络.情报网络中共有n名情报员.每名情报员口J-能有 若T名(可能没有)下线,除1名大头日外其余n-1名情报员有且仅有1名上线.奈 ...
- MySQL生僻函数
0X01 字符串函数 STRCMP STRCMP(expr1,expr2) 若所有的字符串均相同,则返回STRCMP(),若根据当前分类次序,第一个参数小于第二个,则返回 -1,其它情况返回 1 . ...
- Unity - Raycast 射线检测
本文简要分析了Unity中射线检测的基本原理及用法,包括: Ray 射线 RaycastHit 光线投射碰撞信息 Raycast 光线投射 SphereCast 球体投射 OverlapSphere ...
- 【MySQL】事务隔离级别及ACID
注:begin或start transaction并不是一个事务的起点,而是在执行它们之后的第一个操作InnoDB表的语句,事务才真正开始.start transaction with consist ...
- 2.Linux Bash认识
虚拟机快照操作 1.什么是Bash shell? 它就是命令解释器,将用户输入的指令翻译给内核程序,内核处理完成之后将结果返回给Bash 2.Bash shell的用途? 几乎能完成所有的操作: 文件 ...