树状数组

主要用途

image

tips:绝对不能使用0作为下标(与二进制的性质有关)

倒着数到最后一个1然后把前面都变成0(其实就是去补的取反加1)

构造数组

image

image

单点修改

image

区间查询

将几个部分加起来就是一段完整的区间和。

那我们要如何对于找到完全无缝衔接的t呢,将最大的那个数组减去对应的lowbit就可以得到我们所要找的那个与其无缝相邻的较小的数组。

区间修改

原本的单点修改是利用树状数组来维护前缀和,区间修改则是利用树状数组来维护差分数组。

image

image

将其分成两个部门一个为t1一个为t2某种角度来看定点修改是区间修改的特殊情况。

【模板】树状数组(单点修改)

题目描述

给定一个大小为nn个数组aa,qq次操作。

每次操作分为下面两种:

"1 k v":给a_k加上v

"2 l r":查询区间[l, r][l,r]的和。

对于每次22操作,输出结果。

输入格式

第一行两个整数n,q

第二行n个整数表示数组a

接下来q行,每行一个操作。

输出格式

对于每次22操作,在一行内输出结果。

样例输入1

5 4
1 2 3 4 5
1 1 1
2 1 2
1 4 2
2 3 4

样例输出1

4
9
#include<bits/stdc++.h>
using namespace std;
#define int long long

const int N = 3e5+9;
int a[N],t[N];
stack<int> stk;
vector<int> X;
int n,q;
int getidx(int x){
    //通过lower_bound 的二分查找第一个大于等于的的元素的迭代器
    //返回值范围为[1,X.size()]
    return lower_bound(X.begin(),X.end(),x)-X.begin()+1;
}
int lowbit(int x){
    return x&-x;
}
void update(int k,int x){
    for(int i =k;i<=n;i+=lowbit(i))t[i]+=x;
}
int getsum(int k){
    int res = 0;
    for(int i = k;i > 0;i-=lowbit(i))res+=t[i];
    return res;
}
void solve(){
    cin>>n>>q;
    for(int i = 1;i<=n;i++) cin>>a[i];
    // 先默认树状数组为全0,然后再进行修改
     for(int i = 1;i<=n;++i)update(i,a[i]);
     while(q--){
        int op;cin>>op;
        if(op == 1){
            int l,r;
            cin>>l>>r;
            update(l,r);
         }
         else{
            int l,r;cin>>l>>r;
            cout<<getsum(r)-getsum(l-1)<<'\n';
         }
     }
}

signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int _ = 1;
    while(_--)solve();
    return 0;
}

[P41] 树状数组(区间修改)

题目描述

给定一个大小为n个数组aq次操作。

每次操作分为下面两种:

"1 l, r v":给区间[l, r][l,r]中的数组加上v

"2 l r":查询区间[l, r][l,r]中数字的和。

对于每次2操作,输出结果。

输入格式

第一行两个整数n,q

第二行n个整数表示数组a

接下来q行,每行一个操作。

输出格式

对于每次2操作,在一行内输出结果。

样例输入1

5 4
1 2 3 4 5
1 1 3 1
2 1 2
1 4 5 2
2 3 4

样例输出1

5
10
#include<bits/stdc++.h>
using namespace std;
#define int long long

const int N = 3e5+9;
int a[N],td[N],tdi[N];
stack<int> stk;
vector<int> X;
int n,q;
int lowbit(int x){
    return x&-x;
}
void update(int k,int x){
    for(int i =k;i<=n;i+=lowbit(i))
    {
        td[i]+=x;
        tdi[i]+=k*x;
    }
}
int getsum(int k){
    int res = 0;
    for(int i = k;i>0;i-=lowbit(i))
        res+=(k+1)*td[i]-tdi[i];
    return res;
}
void solve(){
    cin>>n>>q;
    for(int i = 1;i<=n;i++) cin>>a[i];
    // 先默认树状数组为全0,然后再进行修改
     for(int i = 1;i<=n;++i)
     {
         update(i,a[i]);
         update(i+1,-a[i]);
     }
     while(q--){
        int op;cin>>op;
        if(op == 1){
            int l,r,v;
            cin>>l>>r>>v;
            update(l,v);
            update(r+1,-v);
         }
         else{
            int l,r;cin>>l>>r;
            cout<<getsum(r)-getsum(l-1)<<'\n';
         }
     }
}

signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int _ = 1;
    while(_--)solve();
    return 0;
}

洛谷P1908) 逆序对

题目描述
猫猫TOM和小老鼠JERRY最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。最近,TOM老猫查阅到一个人类称之为“逆序对”的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中ai>aj且i<j的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。
输入格式
第一行,一个数n,表示序列中有n个数。
第二行n个数,表示给定的序列。序列中每个数字不超过10^9
输出格式
给定序列中逆序对的数目。

当然逆序对也可以用归并排序的方法求,但树状数组的空间复杂度更低。其实我们与其说在用树状数组求逆序对,不如说是在求非严格顺序对(顺序对和相等对),然后间接算出逆序对的数量。我们是怎么算出非严格顺序对的个数的呢?

#include<bits/stdc++.h>
using namespace std;
#define int long long

const int N = 3e5+9;
int a[N],t[N],tdi[N];
stack<int> stk;
vector<int> X;
int n,q;
int bin(int x){
    return lower_bound(X.begin(),X.end(),x)-X.begin()+1;
}
int lowbit(int x){
    return x&-x;
}
void update(int k,int x){
    for (int i = k; i <= X.size(); i+= lowbit(i)) {
        t[i]+=x;
    }
}
int getsum(int k){
    int sum = 0;
    for (int i = k; i >0 ; i-= lowbit(i)) {
        sum+=t[i];
    }
    return sum;
}
void solve(){
    cin>>n;
    for(int i = 1;i<=n;i++)cin>>a[i];
    for(int i = 1;i<=n;i++){
        X.push_back(a[i]);
    }
    //排序去重
    sort(X.begin(),X.end());
    X.erase(unique(X.begin(),X.end()),X.end());
    int ans = 0;
    for(int i = 1;i<=n;i++){
        ans+= getsum(X.size())- getsum(bin(a[i]));
        update(bin(a[i]),1);
    }
    cout<<ans<<'\n';
}

signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int _ = 1;
    while(_--)solve();
    return 0;
}

这道题的本质其实就是求一个数读入前有几个数是大于他的。

例如,我们要求5 4 1 3 2的逆序对。用ans记录非严格顺序对的数量。

img

我们按顺序去填充树状数组,第一个数字是5,这时没有数比5小,所以ans保持为0。我们把tree[5]填为1。

img

下一个数字是1,这时也没有数比1小,ans仍为0。我们把tree[1]填为1。

img

下一个数字是3,这时query(3)为1,有一个数比3小了,ans变为1。然后再填tree[3]。

img

下一个数字是2,这时query(2)为1,说明前面有一个数比2小,ans再加1变为2。然后填tree[2]。

img

最后一个数字是4,query(4)为3,说明前面有3个数比4小,ans加3变为5。所以非严格顺序对的总数就是5。那么逆序对共有 C52−5=5 个。

img

每进行上述过程一次,我们就要进行一次求和,相当于将求从当前位置到数组末尾的的前缀和。