树状数组引入
- 由于普通数组结构导致的部分缺陷,所以我们引入树状数组,保证区间求和操作和查询操作的同时高效性;
存储非前缀和数组 | 存储前缀和数组 |
---|---|
修改单个数据时间复杂度为O(1) | 修改单个数据时间复杂度为O(n) |
询问区间和时间复杂度为O(n) | 询问区间和时间复杂度为O(1) |
普通数组操作特点如上
- 树状数组并不是一棵树,而是根据数字的二进制表示来对数组中的元素进行逻辑上的分层存储;树状数组是一个数组,与普通数组不同之处在于它的某些元素维护的是一段区间的信息,以区间和为例,若i为奇数,则第i个元素就是源数据的第i个元素,若i为偶数,则第i个元素维护的是[i-2^k+1, i]这段区间的和,k代表i的二进制末尾0的个数;则[1,14]分别存的是:[1,1], [1,2], [3,3], [1,4], [5,5], [5,6], [7,7], [1,8], [9,9], [9,10], [11,11], [9,12], [13,13], [13,14]
- [1,14] 分层步骤如下:
分层数以及原理 | 所得区间 |
---|---|
第一层,按照20,21,22,23分 | [1,1], [1,2], [1,4], [1,8] |
第二层,选择剩下数字按20,21,22,23分 | [3,3], [5,5], [5,6], [9,9], [9,10], [9,12] |
第三层,选择剩下数字按20,21,22,23分 | [7,7], [11,11], [13,13], [13,14] |
还是记模板吧。。暂时真的无法理解这玩意
//代码实现:
int lowbit(int x)//找到x在二进制表示下1的最低位
{
return x & (-x);
}
void Update(int pos, int val)//更新,在第pos个位置加上val
{
for (int i = pos; i <= n; i += lowbit(i))
{
c[i] += val;
}
}
int Query(int pos)//查询,查询区间[1,pos]的和
{
int ans = 0;
for (int i = n; i > 0; i -= lowbit(i))
{
ans += c[i];
}
return ans;
}
- 可见树状数组能维护部分区间信息,但是无法满足求最值等操作,所以我们引入线段树。
线段树引入
参考:线段树介绍文章
- 线段树其实是二叉搜索树,通常采用顺序存储结构;
- 线段树与树状数组之间的差别/关系:树状数组能做的事情其实是线段树的一个子集;
- 树状数组本质仍是数组,而线段树是树型结构;
- 构建操作:
const int maxn = 1e4;
int num[maxn]; ///各个位置上的数值
///线段树节点类型
typedef struct Node {
int left;
int right;
int Max;
}node[maxn<<2]; ///范围扩大四倍
///构建线段树
void build(int root,int left,int right) {
node[root].left = left;
node[root].right = right;
if(left == right) {
node[root].Max = num[left];
return;
}
int mid = (left+right)>>1;
build(root<<1,left,mid);
build(root<<1|1,mid+1,right);
push_up(root); ///更新当前节点的函数。
}
///第一类:根节点维护区间最大值
void push_up(int root) {
node[root].Max = max(node[root<<1].Max,node[root<<1|1].Max);
}
//第二类:根节点维护区间的和值
void push_up(root) {
node[root].sum = node[root<<1].sum + node[root<<1|1].sum;
}
- 单点更新和区间查询操作:
// 根节点存和的 单点更新,区间查询操作
void add(int i,int dis,int k)//从t[1]开始,为所有与dis有关的节点都 +k
{
t[i].num+=k;//进入即有关,则实行+k操作
if(t[i].left==t[i].right) return ;//为叶子节点了
if(dis<=t[i<<1].right)// =包含了dis节点;说明dis在t[i]左子树,搜左子树
add(i<<1,dis,k);
if(dis>=t[i<<1|1].left)//在t[i]右子树下,搜右子树,并实行添加操作
add(i<<1|1,dis,k);
}
void search(int i,int l,int r)//求[l,r]内每个数字的和 加至ans 从1开始寻找
{
//寻找区间的部分全部在此区间内,直接 +num
if(t[i].left>=l&&t[i].right<=r)
{
ans+=t[i].num;
return ;
}
if(t[i<<1].right>=l) search(i<<1,l,r);
//此节点左子树的区间的最大值>=l,搜此节点左子树
if(t[i<<1|1].left<=r) search(i<<1|1,l,r);
//此节点右子树的区间的最小值<=r,则[l,r]中有数字在此节点右子树,进行搜索
//如寻找5在[5,6],即[t[i<<1|1].left,t[i<<1|1].right]中,搜索 i<<1|1 查找5
}
- 添加lazy标记的区间更新/查询操作
const int maxn = 1e5;
typedef struct Node {
int left; //节点所维护区间的左边界
int right; //节点所维护区间的右边界
int data; //节点的数据
int lazy; //节点的懒惰标记
}node[maxn<<2];
//根节点存和的区间更新
//更新当前节点
void push_up(int root) {
node[root].sum = node[root<<1].sum + node[root<<1|1].sum;
}
//下推标记
void push_down(int root) {
//说明该节点有标记
if(node[root].lazy>0) {
//求左区间的长度
int leftLen = node[root<<1].right - node[root<<1].left + 1;
//求右区间的长度
int rightLen = node[root<<1|1].right - node[root<<1|1].left + 1;
//更新左区间和值
node[root<<1].sum += leftLen*node[root].lazy;
//更新右区间和值
node[root<<1|1].sum += rightLen*node[root].lazy;
//下推标记到左区间
node[root<<1].lazy += node[root].lazy;
//下推标记到右区间
node[root<<1|1].lazy += node[root].lazy;
//当前节点标记下推完毕,恢复成无标记状态
node[root].lazy = 0;
}
}
//将区[L,R]中的数字都加上add
void update(int L,int R,int add,int root) {
//到达子区间,更新该区间的值,并留下标记
if(L<=node[root],left && node[root].right<=R) {
node[root].sum += (node[root].right-node[root].left+1)*add;
node[root].lazy += add;
return;
}
push_down(root); //下推之前残留的标记
int mid = (node[root].left+node[root].right)/2;
if(L<=mid) update(L,R,add,root<<1); //更新左区间
if(R>mid) update(L,R,add,root<<1|1); //更新右区间
push_up(root); //更新当前节点
}
//区间查询
int query(int L,int R,int root) {
if(L<=node[root].left && node[root].right<=R) {
return node[root].sum;
}
push_down(root); //下推残留标记
int mid = (node[root].left+node[root].right)/2;
int ans = 0;
if(L<=mid) ans += query(L,R,root<<1);
if(R>mid) ans += query(L,R,root<<1|1);
return ans;
}