已更新(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]。

具体步骤(以最小值为例):

  1. 将st[i][0]赋值为arr[i];
  2. 利用动态规划思想,dp出st[i][j] = min(st[i][j - 1], st[i + 2j - 1][j - 1])  (1 ≤ i ≤ n, 1 ≤ j ≤ log2 n);
  3. 查询时,定义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 - 2+ 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);
}

那么问题来了,怎么求这个 2呢?

有一个巧妙的(我自己也没推出来的)算法是:

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表、树状数组与线段树 笔记与思路整理的更多相关文章

  1. bzoj 3110: [Zjoi2013]K大数查询 树状数组套线段树

    3110: [Zjoi2013]K大数查询 Time Limit: 20 Sec  Memory Limit: 512 MBSubmit: 1384  Solved: 629[Submit][Stat ...

  2. [BZOJ 3196] 213平衡树 【线段树套set + 树状数组套线段树】

    题目链接:BZOJ - 3196 题目分析 区间Kth和区间Rank用树状数组套线段树实现,区间前驱后继用线段树套set实现. 为了节省空间,需要离线,先离散化,这样需要的数组大小可以小一些,可以卡过 ...

  3. [BZOJ 1901] Dynamic Rankings 【树状数组套线段树 || 线段树套线段树】

    题目链接:BZOJ - 1901 题目分析 树状数组套线段树或线段树套线段树都可以解决这道题. 第一层是区间,第二层是权值. 空间复杂度和时间复杂度均为 O(n log^2 n). 线段树比树状数组麻 ...

  4. POJ 1195 Mobile phones (二维树状数组或线段树)

    偶然发现这题还没A掉............速速解决了............. 树状数组和线段树比较下,线段树是在是太冗余了,以后能用树状数组还是尽量用......... #include < ...

  5. 【BZOJ3196】二逼平衡树(树状数组,线段树)

    [BZOJ3196]二逼平衡树(树状数组,线段树) 题面 BZOJ题面 题解 如果不存在区间修改操作: 搞一个权值线段树 区间第K大--->直接在线段树上二分 某个数第几大--->查询一下 ...

  6. 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[ ...

  7. P3157 [CQOI2011]动态逆序对(树状数组套线段树)

    P3157 [CQOI2011]动态逆序对 树状数组套线段树 静态逆序对咋做?树状数组(别管归并QWQ) 然鹅动态的咋做? 我们考虑每次删除一个元素. 减去的就是与这个元素有关的逆序对数,介个可以预处 ...

  8. 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 ...

  9. BZOJ 1901 Zju2112 Dynamic Rankings 树状数组套线段树

    题意概述:带修改求区间第k大. 分析: 我们知道不带修改的时候直接上主席树就可以了对吧?两个版本号里面的节点一起走在线段树上二分,复杂度是O((N+M)logN). 然而这里可以修改,主席树显然是凉了 ...

随机推荐

  1. spring源码分析系列4:ApplicationContext研究

    ApplicationContext接口 首先看一下一个最基本的上下文应该是什么样子 ApplicationContext接口的注释里写的很清楚: 一个基本applicationContext应该提供 ...

  2. 《构建之法》项目管理&典型用户和场景

    项目管理   PM的能力要求和任务: 1.观察.理解和快速学习能力 2.分析管理能力 3.一定的专业能力 4.自省的能力 在一个项目中,PM的具体任务: 1.带领团队形成团队的目标/远景,把抽象的目标 ...

  3. SpannableString与SpannableStringBuilder

    一.概述 1.SpannableString.SpannableStringBuilder与String的关系 首先SpannableString.SpannableStringBuilder基本上与 ...

  4. RDD基础-笔记

    RDD编程 基础Spark中的RDD是一个不可变的分布式对象集合.每个RDD都被分为多个分区,这些分区运行在集群中的不同节点上.RDD可以包含Python.java.Scala中任意类型的对象,甚至可 ...

  5. Mac 10.14 安装抓包工具Fiddler

    环境安装 第一步: 首先,Mac下需要使用.Net编译后的程序,需要用到跨平台的方案Mono(现阶段微软已推出跨平台的方案.Net Core,不过暂时只支持控制台程序).安装程序可以从http://w ...

  6. DG常用运维命令及常见问题解决

    DG常见运维命令及常见问题解决方法 l> DG库启动.关闭标准操作Dataguard关闭1).先取消日志应用alter database recover managed standby data ...

  7. 算法学习之剑指offer(二)

    题目1 题目描述 用两个栈来实现一个队列,完成队列的Push和Pop操作. 队列中的元素为int类型. import java.util.Stack; public class Solution { ...

  8. Java基础之集合框架(Collection接口和List接口)

    首先我们说说集合有什么作用. 一.集合的作用 1.在类的内部,对数据进行组织: 2.简单而快速的搜索大数量的条目: 3.有的集合接口,提供一系列排列有序的元素,并且可以在序列中间快速的插入或者删除有关 ...

  9. 使用Line Pos Info 和 Modern C++ 改进打印日志记录

    使用Line Pos Info 和 Modern C++ 改进打印日志记录 使用跟踪值:不管自己是多么的精通,可能仍然使用调试的主要方法之一 printf , TRaCE, outputDebugSt ...

  10. Cocos2d-x 学习笔记(11.3) JumpBy JumpTo

    1. JumpBy JumpTo JumpBy,边跳边平移,不只做垂直向上的抛物动作,同时还在向终点平移.JumpTo是JumpBy的子类. 1.1 成员变量 create方法 JumpBy: Vec ...