【模板篇】树状数组们(三)

本文介绍了一种使用树状数组实现区间加和区间查询的方法,通过维护两个数组c1和c2来高效处理区间操作,相比线段树具有更优的空间和时间复杂度。

ok,以上两期稍稍讲了一下树状数组的基本功能。。

当然,把树状数组拉出来不能只有这两个功能对不对。。。(不然网上都有怎么把你们忽悠来看嘛)
树状数组还是有两把刷子的

(非战斗人员退散)
今天,我们要讲的是:

区间加,区间查

什么???
许多学过线段树的人该诧异了吧。。
树状数组还能干这事?
答案是可以的。。
(○| ̄|_在此%拜一下发明这种做法的神犇orz)

还记得上次我们的c变化成了差分数组吗?
树状数组就是擅长求前缀和和维护差分信息~
当然,这次的目标中有两个区间字样,所以要开两个数组(什么逻辑嘛……)

其实原因并不是这样。

我们用一个差分数组来保存相邻两个数据的差,这个数组命名为c1
此时,无论我们在树状数组上怎么乱搞,原数据的第i个点最后的值(记为a[i]吧)就是求一下c1[i]的总和
(嗯我们上一期的单点查)
对于区间修改,我们采取让c1[l]加和让c1[r+1]减的方式(还是差分)
对于区间查询,我们有ans=sum[r]-sum[l-1](How old are you,差分?)
于是很明显,最后原数组中第i个点乱搞一波后的值是sigma(c1[j]) (j=1..i)

你们理清楚没有。。

理清楚之后,
求1~i的和的时候,仔细看下面:

sum(i)=a[1]+a[2]+...+a[i]
      =c1[1]+(c1[1]+c1[2])+...+(c1[1]+c1[2]+...+c1[i])
      =i*c1[1]+(i-1)*c1[2]+...+1*c1[i]
      =i*(c1[1]+c1[2]+...+c1[i])-(0*c1[1]+1*c1[2]+...+(i-1)*c1[i])
      =i*sigma(c1[j])-sigma(c1[j]*(j-1)) (j=1..i)

所以,我们只要同时 维护一下sigma(c1[j])和sigma(c1[j]*(j-1))就行了。。
还记得c1的差分性质么
所以我们再用一个数组c2搞出sigma(c1[j]*(j-1)),在维护c1的时候顺手维护一下即可。。
这样复杂度也不会被改变!!!非常好而且奇妙的性质。。。
sum[i]就照着上面的式子搞就行。。

下面,终于到了代码,我有一件事情要说:其实只看代码就好,上面讲的没啥用233
代码的例子是用的luogu3372的【模板】线段树 1 (哈哈哈哈,用线段树的人们!)
题目传送门:
https://www.luogu.org/problem/show?pid=3372
里面的数据是要用long long的。。
你们自己看情况改就好了233
而且听说codevs1082的线段树练习3也可以用这种方法水过。。
这题的传送门:http://www.codevs.cn/problem/1082/
而且码长空间时间都要优于线段树哦。。

不过main函数我就不写了_ (:з」∠) _
而且这两个题都要开long long而代码里是没有开的

#include <cstdio>

#define gc getchar

int getnum() {
    int a = 0; char c = gc(); bool f = 0;
    for (; (c<'0' || c>'9') && c != '-'; c = gc());
    if (c == '-') f = 1, c = gc();
    for (; c >= '0'&&c <= '9'; c = gc()) a = (a << 1) + (a << 3) + c - '0';
    return f ? -a : a;
}

class Binary_Tree3 {

private:
    static const int MAXN = 200002;
    int c1[MAXN], c2[MAXN], n, a[MAXN];

    inline int lb(int x) {
        return x&-x;
    }

    int getnum() {
        int a = 0; char c = gc(); bool f = 0;
        for (; (c<'0' || c>'9') && c != '-'; c = gc());
        if (c == '-') f = 1, c = gc();
        for (; c >= '0'&&c <= '9'; c = gc()) a = (a << 1) + (a << 3) + c - '0';
        return f ? -a : a;
    }
public:

    void build(int sum) {
        n = sum;
        for (int i = 1; i <= sum; i++) {
            a[i] = getnum();
            add(c1, i, a[i] - a[i - 1]);
            add(c2, i, (i - 1) * (a[i] - a[i - 1]));
        }
    }

    void add(int *r, int x, int i) {
        for (; x <= n; x += lb(x))
            r[x] += i;
    }

    int ask(int *r, int x) {
        int s = 0;
        for (; x; x -= lb(x))
            s += r[x];
        return s;
    }

    void adda(int l, int r, int i) {
        add(c1, l, i); add(c1, r + 1, -i);
        add(c2, l, i * (l - 1)); add(c2, r + 1, -i * r);
    }

    int query(int l, int r) {
        return r * ask(c1, r) - ask(c2, r) - (l - 1) * ask(c1, l - 1) + ask(c2, l - 1);
    }

};

大概就是这个样子了。。
- 区间加的话就调用adda(l,r,i)就是区间[l,r]加i
- 区间查的话就输出query(l,r)就是区间[l,r]的区间和了。。
对就是这样。

<think>我们正在讨论树状数组的倍增技术。树状数组本身是基于二进制的索引结构,而倍增是一种利用树状数组特性进行高效查询(如前缀和)的技术。倍增在树状数组中的一个典型应用是:在单调序列(如前缀和数组是单调递增的)上进行二分查找。例如,我们可以使用倍增来查找前缀和达到某个值的最小位置(类似于lower_bound)。根据引用[2]中提到的“树状数组二分”,它实际上是一种倍增方法。其基本思想是从大到小枚举幂次(即从高位到低位),判断当前指针加上该幂次后是否满足条件,如果满足则移动指针,并更新累加值。下面是一个树状数组上倍增的模板,用于查找最小的位置pos,使得1到pos的前缀和大于等于某个给定值s(即求前缀和的lower_bound):注意:树状数组的索引从1开始。实现步骤:1.初始化一个指针pos=0,一个累加器sum=0。2.枚举幂次(从大到小,即从log(n)到0),每一步尝试将指针移动2的幂次(即step=1<<i),如果移动后不超过数组长度n,且累加当前树状数组的值后仍小于s,则移动指针并累加。3.最后,如果我们需要的是前缀和大于等于s的最小位置,那么返回pos+1(因为循环结束时,sum是小于s的最大前缀和,所以下一个位置就是所求位置)。注意:树状数组的每个节点存储的是特定区间的和,倍增的过程就是利用树状数组的层次结构来快速逼近目标位置。代码实现如下(假设树状数组已经建好,数组名为c,大小为n+1):```cpp#include<iostream>#include<cmath>usingnamespacestd;constintmaxn=100000;intn;//数组大小intc[maxn];//树状数组//树状数组基本操作intlowbit(intx){returnx&-x;}voidadd(inti,intx){while(i<=n){c[i]+=x;i+=lowbit(i);}}intquery(inti){//查询前缀和[1,i]intsum=0;while(i){sum+=c[i];i-=lowbit(i);}returnsum;}//树状数组上倍增:查找最小的位置pos,使得前缀和>=sintfind(ints){intpos=0;intsum=0;//计算最大的幂次,2^k<=nintk=0;while((1<<(k+1))<=n)k++;//求出最大的k使得2^k<=nfor(inti=k;i>=0;i--){intnext_pos=pos+(1<<i);if(next_pos<=n&&sum+c[next_pos]<s){//如果加上这个区间后仍然小于s,则移动指针并累加pos=next_pos;sum+=c[next_pos];}}//此时,pos位置的前缀和是小于s的最大前缀和,所以pos+1位置的前缀和>=s(如果存在的话)returnpos+1;}intmain(){//初始化树状数组等操作//...return0;}```注意:上面的倍增函数find中,我们直接使用了树状数组的c数组。这是因为树状数组的每个节点c[i]存储的是区间[i-lowbit(i)+1,i]的和。在倍增过程中,我们直接使用c数组的值,而不需要调用query函数(因为query函数是O(logn)的,而倍增过程本身也是O(logn),如果调用query就会变成O(log^2n))。所以倍增过程中直接使用c数组的值是关键。但是,上面的代码中,我们在倍增过程中直接使用了c数组,这要求我们在树状数组更新时,已经维护好了c数组。因此,在更新时我们使用add函数,它会更新所有相关的c数组元素。另外,注意倍增过程中,我们枚举的步长是2的幂次,从大到小枚举。每次尝试移动步长,如果移动后位置不超过n且累加后仍然小于目标值s,则移动并累加。这样,最后我们得到的pos是满足前缀和小于s的最大位置,因此pos+1就是满足条件的最小位置。如果整个数组的前缀和都小于s,那么函数将返回n+1。引用[2]中提到的“树状数组二分”正是这种倍增方法。它比线段树二分更简洁,代码更短。相关问题:1.树状数组倍增的应用场景有哪些?2.树状数组倍增和线段树二分有什么区别?3.如何理解树状数组倍增的时间复杂度?</think>### 树状数组倍增模板及实现 树状数组的倍增是一种高效查询技术,利用二进制位运算快速定位前缀和的分界点。核心思想是通过**高位到低位的二进制枚举**(类似倍增)实现 $O(\log n)$ 复杂度的单次查询,常用于解决前缀和单调性问题(如求第 $k$ 小元素)。 #### 倍增查询模板代码 ```cpp #include <iostream> using namespace std; const int MAXN = 100010; int tree[MAXN], n; // tree: 树状数组, n: 数据规模 int lowbit(int x) { return x & -x; // 获取最低位的1 } void add(int pos, int val) { // 单点更新 for (; pos <= n; pos += lowbit(pos)) tree[pos] += val; } int find_kth(int k) { // 倍增查询:找到前缀和≥k的最小位置 int pos = 0, sum = 0; for (int i = 20; i >= 0; i--) { // 枚举二进制位 (2^20 > 1e6) int next_pos = pos + (1 << i); // 满足条件:不越界、不超k、区间和有效 if (next_pos <= n && sum + tree[next_pos] < k) { sum += tree[next_pos]; pos = next_pos; } } return pos + 1; // 返回目标位置 } int main() { cin >> n; // 初始化树状数组 (需先调用add初始化) // 查询示例: cout << find_kth(5); // 找前缀和≥5的首位置 } ``` #### 关键逻辑说明 1. **二进制枚举** 从高位向低位枚举幂次(`i=20` 到 `0`),相当于按 $2^{20}, 2^{19}, ..., 1$ 的步长试探移动。 2. **安全移动条件** `next_pos = pos + (1 << i)` 需满足: - 位置不越界:`next_pos <= n` - 累加和仍小于目标:`sum + tree[next_pos] < k` - 此时移动指针并累加区间和 3. **定位原理** 循环结束时: - `sum` 是小于 `k` 的最大前缀和 - `pos+1` 是首个满足前缀和 $≥k$ 的位置 #### 应用场景 1. 动态求解第 $k$ 小元素(配合桶计数) 2. 有序序列的快速二分查找 3. 离线查询的优化处理(如区间统计问题) > **复杂度优势**:相比线段树二分节省约 50% 常数时间,代码更简洁[^2]。 --- ### 相关问题 1. 树状数组倍增和普通二分的性能差异具体体现在哪些场景? 2. 如何利用倍增技术动态维护区间第 $k$ 大元素? 3. 倍增过程中如果遇到非单调前缀和会有什么问题?如何解决? [^1]: 树状数组的基础结构参考自一维树状数组模板 [^2]: 倍增技术实现参考树状数组二分模板
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值