ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

一维树状数组的基本操作及简单应用

2021-03-04 17:01:25  阅读:121  来源: 互联网

标签:return 树状 int LL 个数 add 一维 基本操作 include


树状数组

基本操作

一、单修区查

洛谷P3374
题目描述
如题,已知一个数列,你需要进行下面两种操作:

将某一个数加上 xx

求出某区间每一个数的和

输入格式
第一行包含两个正整数 n,m,分别表示该数列数字的个数和操作的总个数。

第二行包含 n个用空格分隔的整数,其中第 i个数字表示数列第 i项的初始值。

接下来 m 行每行包含 3个整数,表示一个操作,具体如下:

1 x k 含义:将第 x个数加上 k

2 x y 含义:输出区间 [x,y] 内每个数的和

输出格式
输出包含若干行整数,即为所有操作 2的结果。
对于 100% 的数据,1 <= n, m <= 5 * 105

输入 #1
5 5
1 5 4 2 3
1 1 3
2 2 5
1 3 -1
1 4 2
2 1 4
输出 #1
14
16

代码

//单修区查 
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define lowbit(x) x&(-x) //返回非负整数x在二进制表示下最低位1及其后面的0构成的数值
const int N = 5e5+10;
int n, m;
LL a[N];
LL tr[N];

//将序列中第x个数加上k
void add(int x, int k)
{
    for(int i = x; i <= n; i += lowbit(i)) tr[i] += k;
}

//查询序列前x个数的和
LL ask(int x)
{
    LL ans=0;
    for(int i = x; i ; i -= lowbit(i)) ans += tr[i];
    return ans;
}

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

二、区修单查

洛谷P3368
题目描述
如题,已知一个数列,你需要进行下面两种操作:

将某区间每一个数数加上 x

求出某一个数的值

输入格式
第一行包含两个整数 N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含 N个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。

接下来 M 行每行包含 2 或 4个整数,表示一个操作,具体如下:

操作 1: 格式:1 x y k 含义:将区间 [x,y] 内每个数加上 k;

操作 2: 格式:2 x 含义:输出第 x个数的值。

输出格式
输出包含若干行整数,即为所有操作 2的结果。
对于 100% 的数据,1 <= n, m <= 5 * 105
输入输出样例
输入 #1
5 5
1 5 4 2 3
1 2 4 2
2 3
1 1 5 -1
1 3 5 7
2 4
输出 #1
6
10

代码

//区修单查 
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 5e5+10;
typedef long long LL;
int n, m;
int a[N];  //原数组
LL tr[N];  //差分数组 

int lowbit(int x)
{
    return x & -x;
}
void add(int x, int k)
{
    for(int i = x; i <= n; i += lowbit(i)) tr[i] += k;
}
LL ask(int x)
{
    LL res = 0;
    for(int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}
int main()
{
    scanf("%d%d",&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--)
    {
        int op;
        int l, r, k;
        scanf("%d%d",&op, &l);
        if(op == 1)
        {
            scanf("%d%d",&r, &k);
            add(l, k);
            add(r+1, -k); //前缀和思想
        }
        else
        {
            printf("%lld\n",ask(l));
        }
    }
    return 0;
}

三、区修区查

AcWing 243.一个简单的整数问题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 ≤ 105,
|d| ≤ 10000,
|A[i]| ≤ 1000000000
输入样例:
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.这是一道最常见的树状数组模板题,用于实现区间的修改与求和,在时间与空间上都优于带lazy标记的线段树。
2.具体分析,请看大佬题解

代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

typedef long long LL;
const int N = 100010;
int n, m;
int a[N];
LL tr1[N];  // 维护b[i]的前缀和
LL tr2[N];  // 维护b[i] * i的前缀和

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

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

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

LL prefix_sum(int x)  //a[]的前缀和
{
    return sum(tr1, x) * (x + 1) - sum(tr2, x);
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
    for(int i = 1; i <= n; i ++)
    {
        int b = a[i] - a[i - 1];  //差分数组
        add(tr1, i, b);
        add(tr2, i, (LL)b * i);
    }

    while(m --)
    {
        char op[2];
        int l, r, d;
        scanf("%s%d%d", op, &l, &r);
        if(*op == 'Q')
        {
            printf("%lld\n", prefix_sum(r) - prefix_sum(l - 1));
        }
        else
        {
            scanf("%d", &d);
            // a[l] += d
            add(tr1, l, d), add(tr2, l, l * d);
            // a[r + 1] -= d
            add(tr1, r + 1, -d), add(tr2, r + 1, (r + 1) * -d);
        }
    }

    return 0;
}

简单应用

一、谜一样的牛

AcWing 244
题目描述
有n头奶牛,已知它们的身高为 1~n 且各不相同,但不知道每头奶牛的具体身高。

现在这n头奶牛站成一列,已知第i头牛前面有Ai头牛比它低,求每头奶牛的身高。

输入格式
第1行:输入整数n。

第2…n行:每行输入一个整数Ai,第i行表示第i头牛前面有Ai头牛比它低。
(注意:因为第1头牛前面没有牛,所以并没有将它列出)

输出格式
输出包含n行,每行输出一个整数表示牛的身高。

第i行输出第i头牛的身高。

数据范围
1 ≤ n ≤ 105
输入样例:
5
1
2
1
0
输出样例:
2
4
5
3
1

代码

#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 100010;
int n;
int h[N];
int ans[N];
int tr[N];   //树状数组维护h[1]~h[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 sum(int x)    
{
    int res = 0;
    for(int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int main()
{
    scanf("%d", &n);
    for(int i = 2; i <= n; i ++) scanf("%d", &h[i]);  //i从2开始,因为h[1]默认为0
    for(int i = 1; i <= n; i ++) tr[i] = lowbit(i);  //初始化,每一个区间的总和=区间长度(优化版)
    // add(i,1)也可,每个数都是1
    for(int i = n; i; i --)  //从后往前推
    {
        int k = h[i] + 1; //第i头牛前面有h[i]头牛比它矮,说明ans[i]在剩下的牛中排名h[i]+1
        int l = 1, r = n;
        while(l < r)
        {
            int mid = l + r >> 1;
            if (sum(mid) >= k) r = mid;
            else l = mid + 1;
        }
        ans[i] = r;
        add(r, -1);   //删除某个数
    }
    for(int i = 1; i <= n; i ++) printf("%d\n", ans[i]);

    return 0;
}

二、楼兰图腾

AcWing 241
题目描述
在完成了分配任务之后,西部314来到了楼兰古城的西部。

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

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

西部314认为这幅壁画所包含的信息与这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 <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 2000010;
typedef long long LL;
int n;
int a[N];
LL t[N]; //t[i]表示树状数组i结点覆盖的范围和
//Lower[i]表示左边比第i个位置小的数的个数
//Greater[i]表示左边比第i个位置大的数的个数
LL Lower[N], Greater[N];

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

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

LL ask(int x)
{
    LL 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;
}

标签:return,树状,int,LL,个数,add,一维,基本操作,include
来源: https://blog.csdn.net/jessy_lemon/article/details/114362716

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有