c++树状数组(超多例题讲解)适合有相应基础的

树状数组(Fenwick Tree)是一种用于高效计算前缀和的数据结构,具有较小的内存占用和较快的查询、更新操作。它广泛应用于解决一维数组的区间查询问题。

树状数组的原理基于二进制的思想。假设有一个长度为n的数组A,树状数组就是用一个长度为n的辅助数组C来模拟A数组的前缀和。数组C的索引i表示原数组A的前i个元素的和,数组C的值表示A数组对应前缀的和。

树状数组的核心操作有两个:区间和查询和单点更新。

区间和查询:给定一个区间[l, r],要求计算出原数组A[l, r]的和。使用树状数组的查询操作如下:
1. 初始化一个变量sum为0。
2. 从r开始,将r的最低位的1置为0(即r = r - (r & -r)),并将sum加上C[r]的值。
3. 重复步骤2,直到r为0。
4. 从l开始,将l的最低位的1置为0(即l = l - (l & -l)),并将sum减去C[l]的值。
5. 重复步骤4,直到l为0。
6. 返回sum。

单点更新:给定一个索引i和一个增量delta,要求将原数组A[i]的值加上delta。使用树状数组的更新操作如下:
1. 从i开始,将i的最低位的1加上delta(即i = i + (i & -i))。
2. 重复步骤1,直到i大于数组长度n。

通过这两个核心操作,可以高效地实现对原数组的区间查询和单点更新。

需要注意的是,树状数组的索引从1开始,因此在使用时需要对原始数据进行适当的处理。同时,树状数组只能处理非负数据,对于负数的处理需要进行适当的转换或者使用其他数据结构。

例题1:求每个数在数组中的逆序数和总逆序数

给定一个 1∼N 的随机排列,要求一次只能交换相邻两个数,那么最少需要交换多少次才可以使数列按照从小到大排列呢?

请你求出一个待排序序列的最少交换次数和对应的逆序数列

输入格式

第一行一个整数 N。

第二行一个 1∼N的排列。

输出格式

第一行输出逆序数列,数之间用空格隔开。

第二行输出最少交换次数。

数据范围

1≤N≤1000

输入样例:

8
4 8 2 7 5 6 1 3

输出样例:

6 2 5 0 2 2 1 0
18

代码:

#include<bits/stdc++.h>

using namespace std;

typedef long long LL;
const int N = 1e5 + 10;

int n;
int a[N], tr[N];
int f[N];

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

void add(int x, int c)
{
    for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}

int ask(int x)
{
    LL res = 0;
    for(int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int main()
{
    cin >> n;
    
    for(int i = 1; i <= n; i ++) cin >> a[i];
    for(int i = 1; i <= n; i ++)
    {
        int y = a[i];
        f[y] = ask(n) - ask(y);
        add(y, 1);
    }
    
    int res = 0;
    for(int i = 1; i <= n; i ++) 
    {
        cout << f[i] << " ";
        res += f[i];
    }
    cout << endl << res;
    
    return 0;
}

1、f[N]数组存储的是所有在i前面,比a[i]小的数据

2、样例解释:,第一个的逆序对为什么为6,因为求的是就是数值为1的逆序对数量,在1前面有6个数比1大,所以逆序对为6

3、

例题2:逆序对的扩展

在完成了分配任务之后,西部 314 来到了楼兰古城的西部。

相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(V),一个部落崇拜铁锹(),他们分别用 V 和  的形状来代表各自部落的图腾。

西部 314 在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了 n 个点,经测量发现这 n 个点的水平位置和竖直位置是两两不同的。

西部 314314 认为这幅壁画所包含的信息与这 n 个点的相对位置有关,因此不妨设坐标分别为 (1,y1),(2,y2),…,(n,yn),其中 y1∼yn 是 1 到 n 的一个排列。

西部 314 打算研究这幅壁画中包含着多少个图腾。

如果三个点 (i,yi),(j,yj),(k,yk) 满足 1≤i<j<k≤n 且 yi>yj,yj<yk,则称这三个点构成 V 图腾;

如果三个点 (i,yi),(j,yj),(k,yk) 满足 1≤i<j<k≤n 且 yi<yj,yj>yk,则称这三个点构成  图腾;

西部 314 想知道,这 n个点中两个部落图腾的数目。

因此,你需要编写一个程序来求出 V 的个数和  的个数。

输入格式

第一行一个数 n。

第二行是 n个数,分别代表 y1,y2,…,yn。

输出格式

两个数,中间用空格隔开,依次为 V 的个数和  的个数。

数据范围

对于所有数据,n≤200000,且输出答案不会超过 int64。
y1∼yn是 1到 n 的一个排列。

样例输入

5
1 5 3 2 4

输出样例:

3 4

代码

#include<bits/stdc++.h>

using namespace std;

typedef long long LL;
const int N = 2e5 + 10;

int n;
int a[N], tr[N];
int f[N];
int g[N];

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

void add(int x, int c)
{
    for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}

LL ask(int x)
{
    LL res = 0;
    for(int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
    
    for(int i = 1; i <= n; i ++)
    {
        int y = a[i];
        f[i] = ask(y - 1);
        g[i] = ask(n) - ask(y);
        add(y, 1);
    }
    
    memset(tr, 0, sizeof tr);
    
    LL resV = 0, resA = 0;
    for(int i = n; i; i --)
    {
        int y = a[i];
        resV += (LL)g[i] * (ask(n) - ask(y));
        resA += (LL)f[i] * ask(y - 1);
        add(y, 1);
    }
    
    cout << resV << " " << resA << endl;
    return 0;
}

1、求所以V 的个数和  的个数,而且严格y1∼yn是 1到 n 的一个排列,所以可以不用离散化处理,求v的个数,所以求出每一个数,左边比它的大的和右边都比它大,然后相乘在全部相加既可,求 的个数也是如此的思路

2、从左到右扫描一遍,f[i]存储的是当前比a[i]小的集合,g[i]存储的是当前比a[i]大的集合,然后建树

3、重新初始化,然后从从右到左扫描一遍

4、树状数组求逆序对,让我们知道了如何在一个序列中计算每个数后面有多少个数比它小,因此我们可以通过这个性质来做一些事情
‘v’图腾求法
倒序扫描序列a,利用树状数组求出每个a[i]后面有几个数比它大记录为g[i]
正序扫描序列a,利用树状数组求出每个a[i]前面有几个数比它大,记录为r[i]
’^’图腾求法
倒序扫描序列a,利用树状数组求出每个a[i]后面有几个数比它小,记录为g[i]
正序扫描序列a,利用树状数组求出每个a[i]前面有几个数比它小,记录为f[i]

例题三:区间和和单点修改

给定 n个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b]的连续和。

输入格式

第一行包含两个整数 n和 m,分别表示数的个数和操作次数。

第二行包含 n 个整数,表示完整数列。

接下来 m 行,每行包含三个整数 k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a 个数加 b)。

数列从 1 开始计数。

输出格式

输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。

数据范围

1≤n≤100000,
1≤m≤100000,
1≤a≤b≤n,
数据保证在任何时候,数列中所有元素之和均在 int 范围内。

输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8

输出样例:

11
30
35

代码:

#include<bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;

int n, m;
int a[N], tree[N];

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

void add(int x, int c)
{
    for(int i = x; i <= n; i += lowbit(i)) tree[i] +=c;
}

int ask(int x)
{
    int res = 0;
    for(int i = x; i; i-= lowbit(i)) res += tree[i];
    return res;
}

int main()
{
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
    for(int i = 1; i <= n; i ++) add(i, a[i]);
    
    while(m --)
    {
        int k, x, y;
        scanf("%d%d%d", &k, &x, &y);
        
        if(k == 0)
        {
            cout << ask(y) - ask(x - 1) << endl;
        }
        else add(x, y);
    }
    return 0;
}

解析:1、因为是单点修改,所以正常建树,把每个数据压入就行

2、求区间和,从r到l - 1求的就是区间l - r 的和

样例四:区间修改和单点查询

给定长度为 N 的数列 A,然后输入 M 行操作指令。

第一类指令形如 C l r d,表示把数列中第 l∼r 个数都加 d。

第二类指令形如 Q x,表示询问数列中第 x 个数的值。

对于每个询问,输出一个整数表示答案。

输入格式

第一行包含两个整数 N 和 M。

第二行包含 N 个整数 A[i]。

接下来 M 行表示 M 条指令,每条指令的格式如题目描述所示。

输出格式

对于每个询问,输出一个整数表示答案。

每个答案占一行。

数据范围

1≤N,M≤10^5,
|d|≤10000,
|A[i]|≤10^9

输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
Q 4
Q 1
Q 2
C 1 6 3
Q 2

输出样例:

4
1
2
5

代码:

#include<bits/stdc++.h>

using namespace std;

typedef long long LL;
const int N = 1e5 + 10;

int n, m;
int a[N], tr[N];

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

void add(int x, int c)
{
    for(int i = x;i <= n; i += lowbit(i)) tr[i] += c;
}

int ask(int x)
{
    LL res = 0;
    for(int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int main()
{
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
    
    for(int i = 1; i <= n; i ++) add(i, a[i] - a[i - 1]);
    
    while(m --)
    {
        char op[2];
        scanf("%s", op);
        if(*op == 'C')
        {
            int l, r, d;
            scanf("%d%d%d", &l, &r, &d);
            add(l, d), add(r + 1, -d);
        }
        else
        {
            int x;
            cin >> x;
            cout << ask(x) << endl;
        }
    }
    return 0;
}

1、因为是区间修改,所以我们可以使用差分来降低时间复杂度

2、区间修改,所以我们是两个端点修改就可以了

样例五:区间查询和区间修改

既可以树状数组,也可以使用线段树,但是这里就使用树状数组来做

给定一个长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:

  1. C l r d,表示把 A[l],A[l+1],…,A[r] 都加上 d。
  2. Q l r,表示询问数列中第 l∼r个数的和。

对于每个询问,输出一个整数表示答案。

输入格式

第一行两个整数 N,M。

第二行 N 个整数 A[i]。

接下来 M行表示 M 条指令,每条指令的格式如题目描述所示。

输出格式

对于每个询问,输出一个整数表示答案。

每个答案占一行。

数据范围

1≤N,M≤10^5,
|d|≤10000,
|A[i]|≤10^9

输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4

输出样例:

4
55
9
15

代码:

#include<bits/stdc++.h>

using namespace std;

typedef long long LL;
const int N = 1e5 + 10;

int n, m;
int a[N];
LL tr1[N], tr2[N];

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

void add(LL tr[], LL x, LL c)
{
    for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}

LL ask(LL tr[], LL x)
{
    LL res = 0;
    for(int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

LL get_sum(LL x)
{
    return ask(tr1, x) * (x + 1) - ask(tr2, x);
}

int main()
{
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
    
    for(int i = 1; i <= n; i ++)
    {
        LL b = a[i] - a[i - 1];
        add(tr1, i, b);
        add(tr2, i, b * i);
    }
    
    while(m --)
    {
        char op[2];
        scanf("%s", op);
        
        if(*op == 'Q')
        {
            int l, r;
            scanf("%d%d", &l, &r);
            cout << get_sum(r) - get_sum(l - 1) << endl;;
        }
        else
        {
            int l, r, d;
            scanf("%d%d%d", &l, &r, &d);
            add(tr1, l, d), add(tr2, l, l * d);
            add(tr1, r + 1, -d), add(tr2, r + 1, (r + 1) * -d);
        }
    }
    return 0;
}

1、因为是区间修改,所以还是使用差分来做,这里维护两个树状数组,tr1存储的是修改区间的数组,tr2存储的是区间的数值

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值