树状数组浅谈

news/2024/11/15 4:17:26/文章来源:https://www.cnblogs.com/jrzhr/p/18425506

什么是树状数组

树状数组是一种码量小,常数小,支持单点修改区间查询的数据结构。

树状数组维护的信息和运算需要满足结合律并且可差分

注意gcd和max操作虽然满足结合律,但不可差分,因此不能使用树状数组维护

其实,树状数组能做的,线段树都能做,线段树能做的,树状数组不一定能做,但线段树码量大,常数大,不像树状数组,码量小,常数小, 适合我这种懒人使用。因此还是要学的,毕竟谁都不想再考场上手打一个线段树。

进过一些操作后树状数组也可以支持区间查询和单点修改,这个我们待会再说。

如何实现

引入

让我们先来看一个问题:

给定一个序列,满足以下操作

  1. 将某一个数加上一个值
  2. 查询 \(l\)\(r\) 的和

假设你还没学过线段树和树状数组,你会怎么做这道题。

显然有两种方法

方法1:直接模拟,修改 \(O(1)\) ,查询 \(O(n)\)

方法2:前缀和,修改 \(O(n)\),查询 \(O(1)\)

然而,如果操作次数很多序列又很长的话上面的做法就显得有些无力了,这时我们就需要使用树状数组来解决这个问题。

树状数组计算 \(a_1+a_2+a_3+a_4+...+a_n\) 是把它们拆成不超过 \(\log n\) 段进行计算,这样我们只需要合并 \(\log n\) 段区间就能够计算出结果,相比于暴力效率有很大的提高。下面是一张简图,生动的展示了树状数组的工作原理。

114514

最下面的八个方块表示原始数组,上面的方块就是c数组,用来存放某一段区间的和, 这些信息是已知的,我们可以把前缀和拆成这些小区间来计算。

从图中可以看出,\(c[i]\) 存的是以 \(i\) 为右端点的一段区间的和,让我们先不管左端点,来感受一下树状数组是如何查询的。

例如我们要查询 \([1,7]\) 的区间和。就会从\(c[7]\) 开始跳,先将答案加上 \(c[7]\) ,发现 \(c[7]\) 计算的是 \([7, 7]\) 的和,于是我们跳到 \(c[6]\),发现 \(c[6]\) 算的是 \([5,6]\) 的和,所以我们跳到 \(c[4]\),并将答案加上 \(c[6]\)\(c[4]\) 算的是 \([1,4]\) 的和,我们将答案加上 \(c[4]\),但此时前面已经没有可跳了,所以过程结束,返回答案。

如果我们要查询 \([4, 7]\) 的和怎么办呢,我们可以利用前缀和的思想,用 \([1,7]\) 的和减去 \([1,3]\) 的和就好了。

区间大小

现在我们来考虑左端点在哪里。

在树状数组中,我们规定 \(c_x\) 的存的是 \([x-\operatorname{lowbit}(x)+1, x]\) 的和。\(\operatorname{lowbit}(x)\) 指的是 \(x\) 这个数在二进制下最低位的1的位权。

我们要如何计算 \(\operatorname{lowbit}(x)\) 呢。根据位运算知识得,\(\operatorname{lowbit}(x) = x\ \& -x\),有人肯定要问,为什么? 下面是一段摘自oiwiki的解释,可能可以帮助理解。

将 x 的二进制所有位全部取反,再加 1,就可以得到 -x 的二进制编码。例如,6 的二进制编码是 110,全部取反后得到 001,加 1 得到 010。

设原先 x 的二进制编码是 (...)10...00,全部取反后得到 [...]01...11,加 1 后得到 [...]10...00,也就是 -x 的二进制编码了。这里 x 二进制表示中第一个 1 是 x 最低位的 1。

(...) 和 [...] 中省略号的每一位分别相反,所以 x & -x = (...)10...00 & [...]10...00 = 10...00,得到的结果就是 \(\operatorname{lowbit}\)

代码实现:

int lowbit(int x){return x & -x;
}

区间查询

回顾一下刚才举的例子,不难想出查询前缀和的方法,我们写出计算 \([1, x]\) 的和的过程。

  1. \(ans\) 加上 \(c_x\)
  2. \(x\) 减去 \(\operatorname{lowbit}(x)\)
  3. 判断 \(x\) 是否为 \(0\) ,不是就回到第1步,否则返回 \(ans\)

代码实现:

int sum(int x){int res = 0;while(x){res += c[x];x -= lowbit(x);}return res;
}

那么我们可以写出查询 \([l, r]\) 区间和的代码

int query(int l, int r){return sum(r) - sum(l-1);
}

区间查询部分完结。

单点修改

修改一个值其实只需要把管辖了 \(a_x\)\(c_y\) 找到并修改即可,那我们怎么找到管辖了 \(a_x\)\(c_y\) 呢?

显而易见的,\(c_x\) 管辖了 \(a_x\)。在之前的那副简图中可以看出 \(c[x]\)\(c[x+\operatorname{lowbit}(x)]\) 包含。

于是我们就有了一种找的方法,假设要把 \(a_x\) 加上 \(y\), 下面给出过程。

  1. \(c_x\) 加上 \(a_x\)
  2. \(x\) 加上 \(\operatorname{lowbit}(x)\)
  3. 如果 \(x\) 大于 \(n\) 那么退出,否则回到第1步。

代码如下:

void modify(int x, int y){while(x <= n){c[x] += y;x += lowbit(x);}
}

建树

可以直接转换为 \(n\) 次单点修改。

代码如下:

for(int i = 1; i <= n; i++){int x;scanf("%d", &x);modify(i, x);
}

完整代码

#include<bits/stdc++.h>
using namespace std;
int n, m, c[1000010];
int lowbit(int x){return x & (-x);
}
void modify(int x, int y){while(x <= n){c[x] += y;x += lowbit(x);}
}
int sum(int x){int res = 0;while(x >= 1){res += c[x];x -= lowbit(x);}return res;
}
int query(int l, int r){return sum(r) - sum(l-1);
}
int main(){scanf("%d%d", &n, &m);for(int i = 1; i <= n; i++){int x;scanf("%d", &x);modify(i, x);}for(int i = 1; i <= m; i++){int opt, x, y;scanf("%d%d%d", &opt, &x, &y);if(opt == 1){modify(x, y);}else{printf("%d\n", query(x, y));}}return 0;
}

好了,你已经学会了基础的树状数组,接着我们来学习如何让树状数组支持区间修改单点查询

区间修改单点查询

题目:【模板】树状数组 2

我们发现如果树状数组维护的是原本的数组则无法实现,考虑维护差分数组。

我们知道在如果要将 \([l, r]\) 的所有数加上 \(x\) 的话,只需要在差分数组的第 \(l\) 位加上 \(x\),再在第 \(r+1\) 位减去 \(x\) 即可。

而查询第 \(x\) 个数是多少其实就是计算差分数组前 \(x\) 个数的和。这两个操作树状数组都可以实现,于是我们就做完了。

代码如下:

#include<bits/stdc++.h>
using namespace std;
int n, m, a[1000010], b[1000010], c[1000010];
int lowbit(int x){return x & (-x);
}
void modify(int x, int y){for(int i = x; i <= n; i += lowbit(i)){c[i] += y;}return ;
}
int query(int x){int res = 0;for(int i = x; i; i -= lowbit(i)){res += c[i];}return res;
}
int main(){scanf("%d%d", &n, &m);for(int i = 1; i <= n; i++){scanf("%d", &a[i]);b[i] = a[i] - a[i-1];modify(i, b[i]);}while(m--){int opt;scanf("%d", &opt);if(opt == 1){int l, r, x;scanf("%d%d%d", &l, &r, &x);modify(l, x);modify(r+1, -x);}else if(opt == 2){int p;scanf("%d", &p);printf("%d\n", query(p));}}return 0;
}

好了,现在你已经学会了基本的树状数组,来做几道例题吧

例题

例题1:[CQOI2006] 简单题

题目链接

这是一道树状数组板子题,只需要把区间修改单点查询的加法改为异或就好了。

代码:

#include<bits/stdc++.h>
using namespace std;
int n, m, c[100001];
int lowbit(int x){return x & (-x);
}
void add(int x){for(int i = x; i <= n; i += lowbit(i)){c[i] ^= 1;}
}
int sum(int x){int res = 0;for(int i = x; i; i -= lowbit(i)){res ^= c[i];}return res;
}
int main(){scanf("%d%d", &n, &m);while(m--){int opt, l, r;scanf("%d%d", &opt, &l);if(opt == 1){scanf("%d", &r);add(l);add(r+1);}else{printf("%d\n", sum(l));}}return 0;
}

例题2:HH的项链

题目链接

首先,如果一个贝壳反复出现,那我们只要关注它出现的某一个位置而忽略其他位置,显而易见的,我们可以关注最右边的位置,因为好更新。

既然关注右边的贝壳,那么我们就要从左到右的处理数组,注意到这题不强制在线,于是我们可以离线后按照右端点排序,这样保证了右边的数是按顺序更新的。

查询的时候利用前缀和思想即可。

下面引入一个数组 \(vis\)\(vis_i\) 表示第 \(i\) 种贝壳在目前询问的区间内最后出现的位置。还有一个名叫 \(last\) 的变量,意思是还没修改区间的左端点,每次更新从它开始。

我们写出修改部分的代码:

    int last = 1;for(int i = 1; i <= m; i++){for(int j = last; j <= q[i].r; j++){if(vis[a[j]]) modify(vis[a[j]], -1);//如果之前有这个数就先把它删掉modify(j, 1);//用新的位置代替vis[a[j]] = j;//修改vis数组}last = q[i].r+1;//更新lastans[q[i].id] = query(q[i].r) - query(q[i].l-1);}

总代码,可以配合理解:

#include<bits/stdc++.h>
using namespace std;
namespace qwq{inline int read(){int n = 0;  int f = 1;char c = getchar();while(c < '0' || c > '9'){if(c == '-') f = -1;c = getchar();}while(c >= '0' && c <= '9'){n = (n << 3) + (n << 1) + (c ^ 48);c = getchar();}return n * f;}inline void Write(int x){if(x < 0){putchar('-');x = -x;}if(x > 9) Write(x / 10);putchar((x % 10) ^ 48);return;}inline void write(int x, char c){Write(x);putchar(c);}
}
using namespace qwq;
struct node{int l, r, id;bool operator < (const node &A){return r < A.r;}
} q[1000010];
int tree[1000010], n, m, a[1000010], vis[1000010], ans[1000010];
int lowbit(int x){return x & (-x);
}
void modify(int x, int y){while(x <= n){tree[x] += y;x += lowbit(x);}
}
int query(int x){int res = 0;while(x){res += tree[x];x -= lowbit(x);}return res;
}
int main(){n = read();for(int i = 1; i <= n; i++){a[i] = read();}m = read();for(int i = 1; i <= m; i++){q[i].l = read(), q[i].r = read(), q[i].id = i;}sort(q+1, q+m+1);int last = 1;for(int i = 1; i <= m; i++){for(int j = last; j <= q[i].r; j++){if(vis[a[j]]) modify(vis[a[j]], -1);modify(j, 1);vis[a[j]] = j;}last = q[i].r+1;ans[q[i].id] = query(q[i].r) - query(q[i].l-1);}for(int i = 1; i <= m; i++){write(ans[i], '\n');}return 0;
}

这道题还有一个莫队做法,感兴趣的读者可自行了解。

结语

上面就是我的总结和笔记,这里其实还有很多技巧没有讲到,例如扫描线等,之后可能会补充。还有树状数组虽然代码短,但适用范围小,线段树还是要学的。让我们一起,向着神犇的路进发。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/801565.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Filebeat

Filebeat 简介 Filebeat用于转发和集中日志数据的轻量级传送程序。作为服务器上的代理安装,Filebeat监视指定的位置文件或位置,收集日志事件,并将他们转发到Elasticsearch 或Logstash进行索引。 架构图安装Filebeat 下载并安装 wget https://artifacts.elastic.co/download…

Tarjan算法及其应用 总结+详细讲解+详细代码注释

\(\text{Tarjan}\) 1.引入概念 1.强连通分量 1.定义 在有向图 \(G\) 中,强连通分量就是满足以下条件的 \(G\) 的子图:从任意一点出发,都有到达另一个点的路径。 不存在一个或者几个点,使得这个子图加入这些点后,这个子图还满足上一个性质。为什么要限定”在有向图中“而不…

三级数据库技术笔记

数据库应用系统开发方法 数据库应用系统生命周期 软件工程与软件开发方法 瀑布模型 快速原型模型 螺旋模型 DBAS生命周期 DBAS生命周期:项目规划、需求分析、系统设计、实现与部署、运行与维护 规划与分析 可行性分析:经济可行性、技术可行性、操作可行性、开发方案选择 需求…

洛谷P5683 [CSP-J2019 江西] 道路拆除

立下flag:今天一定AC这道题! 题目意思: 思路: 然而并没有分。。 输出-1,祈求CCF的施舍( 30% 的数据,有 \(s_1 = s_2\) 求 1 号点到 \(s_1\) 最短路,再计算不需要的路径。 SPFA,启动! #include<bits/stdc++.h>using namespace std;const int maxn=3010; const i…

golang 项目引入私有仓库包

场景: 当你多个项目,都需要使用一个或者多个方法,那么可以将公共方法,抽成一个包,进行管理(类似Log模块等)。这时候可以将你的包上传到私有的仓库,其他项目引入该包即可。下面来介绍下,如何引用私有仓库的包。 1. 创建一个新的 Git 标签 假设你已经在你的私有 GitLab …

设计模式之——装饰者模式

前言: 装饰者模式是结构性设计模式之一,其在不必改变类文件及不适用继承的情况下,动态的扩展一个对象的功能。它通过创建一个包装对象(即装饰)来包裹真实的对象。 一.定义 动态的给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更加灵活。 装饰者模式的…

解决vsc中文乱码

关于vs code使用code runner运行python代码出现中文乱码的解决办法_code runner 运行乱码-CSDN博客 Code Runner插件设置 "set PYTHONIOENCODING=utf8 && python -u"

CSP-S 2024 提高组初赛第一轮初赛试题及答案解析

CSP-S 2024 提高组初赛第一轮初赛试题及答案解析 一、 单项选择题(共15题,每题2分,共计30分:每题有且仅有一个正确选项) 1 在 Linux 系统中,如果你想显示当前工作目录的路径,应该使用哪个命令?( ) A pwd B cd C ls D echo 答案 A 解析 A pwd:这个命令是“prin…

结对项目:四则运算题目生成

这个作业属于哪个课程 22级计科1班这个作业要求在哪里 作业要求这个作业的目标 自动生成小学四则运算题目github项目链接 链接姓名&学号姓名 学号万凯毅 3122004788周彦安 3122004804PSPPSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)Plannin…

帝国CMS搬家后无法发文章或者上传图片无法显示?

如果你在搬家后遇到无法发文章或上传图片的问题,通常是因为权限设置不当导致的。以下是一些详细的步骤来解决这些问题: 1. 检查目录权限 无法发文章检查栏目目录及子文件夹权限确保栏目目录及其子文件夹具有适当的权限。 通常需要将这些目录设置为 777 权限。shchmod -R 777 …

WordPress数据库连接失败是最常见的错误情况

WordPress数据库连接失败常见原因及解决方法 官方提示的三种情况数据库名、用户名或密码错误解决方法:检查并确认数据库名称、用户名和密码是否正确无误。数据库主机地址错误解决方法:通常填写localhost即可;如果是远程数据库,请根据主机提供商提供的地址进行填写。数据库未…

打靶记录 SickOS 1.1

https://www.vulnhub.com/entry/sickos-11,132/主机发现端口扫描探测存活主机,136是靶机,因为靶机是我最后添加的 nmap -sP 192.168.75.0/24 // Starting Nmap 7.93 ( https://nmap.org ) at 2024-09-22 11:36 CST Nmap scan report for 192.168.75.1 Host is up (0.00038s l…