树型结构——树状数组

目录

1.树状数组的引入

2.基本操作:主要包括 插入操作,查询操作.

3.具体实现:

例题:1.楼兰图腾

241. 楼兰图腾

2.一个简单的整数问题

3.一个简单的整数问题2


1.树状数组的引入

树状数组是用来解决区间修改-单点查询以及单点修改-区间查询和区间修改-区间查询问题的一种优化时间的算法。

这里推荐B站视频歪瑞古德:〔manim | 算法 | 数据结构〕 完全理解并深入应用树状数组 | 支持多种动态维护区间操作_哔哩哔哩_bilibiliicon-default.png?t=M276https://www.bilibili.com/video/BV1pE41197Qj?from=search&seid=13866619238299900297&spm_id_from=333.337.0.0

简单来说,如果一个问题能转变成上述三个问题,就可以使用树状数组,将修改和查询操作的时间复杂度从O(n)优化到O(log n),树状数组是一种简单的数据结构。

2.基本操作:主要包括 插入操作,查询操作.

3.具体实现:

这里以区间求和问题作为例子。

也就是区间修改,区间求和

插入操作:

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

查询操作:

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

例题:1.楼兰图腾

如何把一个问题转换成区间修改,区间查询,是解决树状数组问题的关键,另外能用树状数组解决的问题也可以用线段树解决。

241. 楼兰图腾

  •    题目icon-default.png?t=M276https://www.acwing.com/problem/content/description/243/
  •    提交记录
  •    讨论
  •    题解
  •    视频讲解

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

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

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

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

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

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

如果三个点 (i,yi),(j,yj),(k,yk)满足 1≤i∧ 图腾;

西部 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 
#include 
#include 

using namespace std;

const int N = 2000010;

typedef long long LL;

int n;
//t[i]表示树状数组i结点覆盖的范围和
int a[N], t[N];
//Lower[i]表示左边比第i个位置小的数的个数
//Greater[i]表示左边比第i个位置大的数的个数
int Lower[N], Greater[N];

//返回非负整数x在二进制表示下最低位1及其后面的0构成的数值
int lowbit(int x)
{
    return x & -x;
}

//将序列中第x个数加上k。
void add(int x, int k)
{
    for(int i = x; i <= n; i += lowbit(i)) t[i] += k;
}
//查询序列前x个数的和
int ask(int x)
{
    int sum = 0;
    for(int i = x; i; i -= lowbit(i)) sum += t[i];
    return sum;
}

int main()
{

    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);

    //从左向右,依次统计每个位置左边比第i个数y小的数的个数、以及大的数的个数
    for(int i = 1; i <= n; i++)
    {
        int y = a[i]; //第i个数

        //在前面已加入树状数组的所有数中统计在区间[1, y - 1]的数字的出现次数
        Lower[i] = ask(y - 1); 

        //在前面已加入树状数组的所有数中统计在区间[y + 1, n]的数字的出现次数
        Greater[i] = ask(n) - ask(y);

        //将y加入树状数组,即数字y出现1次
        add(y, 1);
    }

    //清空树状数组,从右往左统计每个位置右边比第i个数y小的数的个数、以及大的数的个数
    memset(t, 0, sizeof t);

    LL resA = 0, resV = 0;
    //从右往左统计
    for(int i = n; i >= 1; i--)
    {
        int y = a[i];
        resA += (LL)Lower[i] * ask(y - 1);
        resV += (LL)Greater[i] * (ask(n) - ask(y));

        //将y加入树状数组,即数字y出现1次
        add(y, 1);
    }

    printf("%lld %lld\n", resV, resA);

    return 0;
}

2.一个简单的整数问题:

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

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

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

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

输入格式

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

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

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

输出格式

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

每个答案占一行。

数据范围

1≤N,M≤1e5
|d|≤10000
|A[i]|≤1e9

输入样例:

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
using namespace std;
#define lowbit(x) x&-x
#define int long long 
const int N=2e5+10;
int n,m;
int a[N],tr[N];

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

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n;i++) add(i,a[i]-a[i-1]);
	while(m--){
		char op;
		cin>>op;
		if(op=='Q'){
			int x;
			cin>>x;
			cout<>l>>r>>c;
			add(l,c),add(r+1,-c);
		}
	}
	
	
	return 0;
}

3.一个简单的整数问题2:

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

  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≤1e5,
|d|≤10000,
|A[i]|≤1e9

输入样例:

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

基本思路就是推公式,思路确实精巧。

树型结构——树状数组_第1张图片

#include 
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 1e5 + 10;
int n, m;
int a[N];
LL tr[N], tri[N];
//tr[]数组是原始数组的差分数组d[i]的树状数组
//tri[]数组是原始数组的差分数组乘以i即i*d[i]的树状数组

int lowbit(int x)
{
    return x & -x;
}
void add(LL c[], int x, int v)
{
    for (int i = x; i <= n; i += lowbit(i))
        c[i] += v;
}
LL query(LL c[], int x)
{
    LL res = 0;
    for (int i = x; i; i -= lowbit(i))
        res += c[i];
    return res;
}
//对应最后一步推导的公式
LL get_sum(int x)
{
    return query(tr, x) * (x + 1) - query(tri, x);
}
int main()
{
    scanf("%d%d", &n, &m);
    //输入数组a[i]
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    //先构造两个数组 d[i] 和 i*d[i]
    for (int i = 1; i <= n; ++i) 
        tr[i] = a[i] - a[i - 1], tri[i] = tr[i] * i;
    //原地 O(n) 建树状数组
    for (int x = 1; x <= n; ++x)
        for (int i = x - 1; i >= x - lowbit(x) + 1; i -= lowbit(i))
            tr[x] += tr[i], tri[x] += tri[i];
    //读入查询
    while (m--)
    {
        char op[2];
        int l, r, c;
        scanf("%s", op);
        if (op[0] == 'Q')
        {
            scanf("%d%d", &l, &r);
            printf("%lld\n", get_sum(r) - get_sum(l - 1));
        }
        else
        {
            scanf("%d%d%d", &l, &r, &c);
            add(tr, l, c), add(tr, r + 1, -c);
            add(tri, l, l * c), add(tri, r + 1, (r + 1) * -c);
        }
    }
    return 0;
}

你可能感兴趣的:(数据结构,算法)