树状数组模板

树状数组

树状数组

为什么需要树状数组?
很多场景下,需要通过O(n)的复杂度求前缀和,之后通过差分O(1)的复杂度,求指定区间的区间和。
但是,对于已经建立起来的前缀和数组,如果改变某个单点的值,修改的代价是O(n)的。

树状数组的定义

  • 可以通过树状数组实现O(logn)的修改,实现O(logn)的查询
  • 树状数组的大小于员数列大小相同
  • 与前缀和不同的是,树状数组的 i 位置的元素存储的并不是前 i 个元素 而是从 i 开始,包括第 i 个元素的前 ti 个元素的和。
  • 其中 ti 是最大的可以整除 i 的2的幂
  • 例如:s[ i ] 为树状数组,d[ i ] 是原数组
  •  i = 9, ti = 1      s [ 9 ] = d [ 9 ]
    
  •  i = 6 ,   ti = 2      s [ 6 ] = d [ 5 ] + d [ 6 ] 
    
  •  i = 24 , ti = 8      s[ 24 ] = d [17] + d [ 18 ] + d [ 19 ] + d [ 20 ] + d [ 21 ] +...+ d [ 24 ]    
    
  • 如果转为二进制,ti 的 归路更加明显
  • i = 9 (1001) ti = 1
  • i = 6 (0110) ti = 2
  • i = 24 (11000) ti = 8
    ti 为二进制最低位的1,与其后的0组成的2进制

lowbit(i)

求解ti的函数

原数 i : 101011000
取反 : 010100111
取反+1:010101000
和原数相与:000001000

lowbit(i) = i & ((~1) + 1) = i & ( -i )
#define lb(x) (x & (-x))

树状数组的查询

树状数组
要查询的数据是指定位置x的前缀和 [1, x ]的前缀和
对于区间 [ 1, 11 ] 可以使用 是 s[ 11 ] + s [ 10 ] + s [ 8 ] 表示
对于任何一个区间,都能被完整的表示出来
因为s[ i ] 可以表示前 ti 个数的和,
ans += s[ i ]
令 i = i - ti
ans += s[ i ]
。。。直到i = 1
ans += s[ 1 ]
返回 ans

#define lb(x) (x & (-x))
long long  ask(int x){
    long long  res = 0;
    for(int i = x;i >= 1;i -= lb(i))
    res += s[i];
    return res;
}

树状数组的性质

性质1 : 若当前结点为x,且令x + tx 为父节点 tx = lowbit(x) ,则梳妆数组将形成一个树形结构
性质2 : 结点x 记录区间 ( x - tx,x ] 的信息,其子节点记录的区间是( x - tx, x ] 的子集,且不会相互覆盖。
性质3 : 结点x 的记录的区间为结点y 记录的区间的子集,当且仅当y是结点x的祖先节点

树状数组的修改

修改的时候,改变原数组x位置的值,只需要修改 s[x] 和所有祖先节点中的值,复杂度是O(logn)的,相比前缀和,修改的复杂度小
怎么找祖先节点?
树状数组

在 i 位置 加上 lowbit(i),就是祖先的位置
在i 位置加 v,每个祖先节点都要加v

void upd(int x,int v){
    for(int i =x; i <= n;i += lb(i))
    s[i] += v;
}

例题

题目
求区间[ l, r ]的和, ask(r)- ask(l-1)

#include"bits/stdc++.h"
#define lb(x) (x & (-x))
using namespace std;

long long  s[1000001];
int n,q;
long long  ask(int x){
    long long  res = 0;
    for(int i = x;i >= 1;i -= lb(i))
    res += s[i];
    return res;
}
void upd(int x,int v){
    for(int i =x; i <= n;i += lb(i))
    s[i] += v;
}
int main()
{
    
    //freopen("a.in","r",stdin);
    //freopen("a.out","w",stdout);
    memset(s,0,sizeof(s));

    cin>>n>>q;
    for(int i = 1 ;i <= n;i++)
        {
            int t;
            scanf("%d",&t);
            upd(i,t);
        }
    for(int i = 0;i < q;i++)
    {
        int op,num1,num2;

        scanf("%d %d %d",&op,&num1,&num2);
        if(op ==1)
        {
            upd(num1,num2);
        }
        else
        {
            long long ansl = ask(num1-1);
            long long  ansr = ask(num2);
            cout << (ansr - ansl) <<endl;
        }
    }
    return 0;
}

用树状数组解决二维偏序问题

  • 给定一个序列 a1, a2, a3, …, an,如果存在 i < j 且 ai >
    aj,那么我们称之为逆序对,求给定序列中逆序对的数目。其中 1 ≤ ai, n ≤ 10^5。
  • 对于任意一个ai,统计在其之前求大于 ai 的数的数量,即为以 ai 结尾的逆序对的数量
  • 只要枚举a1 ~ an分别计算,再求和,即为所求的答案

例题2 二维偏序问题

给定一个序列 a1, a2, a3, …, an,如果存在 i < j 且 ai > aj,那么我们称
之为逆序对,求给定序列中逆序对的数目。其中 1 ≤ ai, n ≤ 10^5。
思路:开一个数组s[MAXN],对于a1查询 a[MAXN]到a[a1 + 1]所有的和,也就是目前比a1大的元素有多少个,查询的结果加到ans上,更新 a[a1] +1

例子

题目描述
两维坐标,按照某一维i例如运行时间排序,求另一维的逆序对数


#include"bits/stdc++.h"
using namespace std;
#define lb(x) (x & (-x))
int s[1000010],n;
//int cnt[1000010];
int   ask(int x)
{
     int ans = 0;
    for(int i = x ;i >= 1; i -= lb(i))
        ans += s[i];
        return ans;
}

void upd(int x,int v)
{
    for(int i = x; i<=1000009;i += lb(i))
        s[i] += v;
}
int main()
{
    freopen("a.in","r",stdin);
    freopen("a.out","w",stdout);
    cin>>n;
    memset(s,0,sizeof(s));
    memset(cnt,0,sizeof(cnt));
    vector<pair<int,int>> a;
    for(int i= 0;i < n;i++)
    {
        int t,c;
        scanf("%d %d",&t,&c);
        a.push_back(make_pair(t+1,c+1));
    }
    sort(a.begin(),a.end());
    for(int i = 0;i <n;i++)
    {
        
        int  num = ask(1000009) - ask(a[i].second);
        int  score = i - num;
        cnt[score]++;
        upd(a[i].second,1);
    }
    for(int i = 0;i < n;i++)
    printf("%d\n",cnt[i]);
    return 0;
}

例题3 树状数组和动态规划相结合 – 最长上升子序列

题目描述
状态:定义fi 表示以A为结尾的最长上升序列的长度
初始化 f1= 1
转移过程: fi = max{ fj | j < i && Aj < Ai } + 1
输出答案 max{f[i], i = 1…n}
再找最大的fj j < i && Aj < Ai 的过程中,要从1 到 i 遍历一遍才行,复杂度为O(n^2)

使用树状数组进行优化

树状数组保存的内容,从前ti个数据的和, 变为前ti个数的最大值
如果读到一个数 x ,ask(x - 1),得出结尾小于x的所有子序列的最长的长度,
tmp = ask(x - 1) + 1
upd(x , tmp)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值