I am a teacher!

导航

树状数组及应用

1.树状数组的原理

        在程序设计时,我们需要维护一个一维数组A的前缀和S,设S[i]=A[1]+A[2]+…+A[i]。

        如果我们修改了任意一个元素A[i]的值,则相关的前缀和S[i]、S[i+1]、…、S[n]都会发生变化。若采用传统的线性顺序扫描方法求连续数组元素的和,则每次修改A[i]后,调整前缀和S的时间复杂度为O(n)。若修改数组A中元素的频度为m,则调整前缀和S的时间复杂度为O(m*n)。

        为提高维护前缀和的效率,可引入“树状数组”。树状数组通过将线性结构转换成伪树状结构(线性结构只能逐个扫描元素,而树状结构可以实现跳跃式扫描),使得修改数组元素值后,维护前缀和的时间复杂度为O(log2n),从而大大提高整体效率。

       设有数组A[n],相应的树状数组为C[n],当n=9时,其树形结构如图1所示。

图1  树状数组结构示意图

        在图1中,设黑色数组元素A[1]~A[8]代表原来的数组A,红色数组元素C[1]~C[8]就代表了树状数组C。若图中每个结点元素存放其子结点元素值的和,则有

C[1] = A[1];

C[2] = A[1] + A[2];

C[3] = A[3];

C[4] = A[1] + A[2] + A[3] + A[4];

C[5] = A[5];

C[6] = A[5] + A[6];

C[7] = A[7];

C[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8];

可以发现,树状数组各元素的值是有规律的

C[i] = A[i - 2k+1] + A[i - 2k+2] + ... + A[i];   //  k为i的二进制中最右端1的位号

例如i =6时,其二进制数为110,最右端1的位置为1,故k =1,C[6]=A[6-2+1]+A[6];

i = 8时,其二进制数为1000,最右端1的位置为3,故k = 3,C[8]=A[8-8+1]+…+A[8]。

(1)求前缀和。

        我们先来看如何利用树状数组C,求A数组中前i项的和。

例如,i=5时,

S[5]=A[1]+A[2]+A[3]+A[4]+A[5] ; 

由图1知,C[4]=A[1]+A[2]+A[3]+A[4];   C[5]=A[5];

故:   S[5]=C[4]+C[5];

序号写为二进制可得:S[(101)]=C[(100)]+C[(101)]。

再如,i=7时,

S[7]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7] ; 

由图1知,C[4]=A[1]+A[2]+A[3]+A[4];   C[6]=A[5]+A[6];   C[7]=A[7];

故:   S[7]=C[4]+C[6]+C[7];

序号写为二进制可得:S[(111)]=C[(100)]+C[(110)]+C[(111)];

观察上面的两个示例,树状数组其根本实质就是二进制的应用。

由此,可将求前缀和的操作写成如下函数:

int getsum(int i)

{

    int res = 0;

    while(i > 0)

    {

        res += c[i];

        i -= lowbit(i);

    }

    return res;

}

其中,函数lowbit(i)的返回值为2k,k为i的二进制中最右端1的位号。

    下面以i=7为例模拟函数getsum(7)的执行过程。

    i=7(二进制数为111),res+=C[7]=C[7], lowbit(7)=001(二进制表示,对应1),i=7-1=6;

    i=6(二进制数为110),res+=C[6], lowbit(6)=010(二进制表示,对应2),i=6-2=4;

i=4(二进制数为100),res+=C[4], lowbit(4)=100(二进制表示,对应4),i=4-4=0;

i=0,结束循环,返回res(=C[7]+C[6]+C[4])。

       这样,利用树状数组能快速求任意区间[a,b]的和:A[a] + A[a+1] +…+ A[b],直接调用getsum(b)-getsum(a-1)即可。

(2)单点更新。

下面我们再讨论树状数组如何生成并维护。

当修改数组A中的某一个元素A[i]的值时  应当如何更新数组树状C呢?

        当修改A[i]的值时,可以从C[i]往根结点一路上溯,调整这条路线上的所有C[k]即可。

        例如,修改A[1]时,需要向上更新C[1] 、C[2]、C[4]、C[8],表示成二进制对应为:

         C[(001)]、C[(010)]、C[(100)]、C[(1000)]

        再如,修改A[5]时,需要向上更新C[5] 、C[6]、C[8],表示成二进制对应为:

       C[(101)]、C[(110)]、C[(1000)]

        观察上面的两个示例,单点更新其实与求前缀和互为逆运算。

        由此,可将单点更新的操作写成如下函数:

void update(int i,int value)   // 修改A[i]值使其增加value后,更新树状数组

{

    while (i<=n)

    {

        c[i]+=value;

        i+=lowbit(i);

    }

}

        下面以修改A[5]为例,模拟函数update(5,val)的执行过程。

        i=5(二进制数为101),C[5]+=val,lowbit(5)=001(二进制表示,对应1),i=5+1=6;

        i=6(二进制数为110),C[6]+=val,lowbit(6)=010(二进制表示,对应2),i=6+2=8;

        i=8(二进制数为1000),C[8]+=val,lowbit(8)=10000(二进制表示,对应16),i=8+16=24;

        i=24>n,结束循环。循环期间,C[5]、C[6]、C[8]进行了更新。

      有了单点更新函数,就很容易根据给定数组A构造对应的树状数组C了。初始时,令数组C的全部元素值为0,依次用数组A的元素值A[1]~A[n],调用函数update(I,A[i]),即可构造出对应的树状数组C。

     (3)lowbit函数的实现。

       函数lowbit(i)的返回值为2k,k为i的二进制中最右端1的位号。这可以用一个表达式i&(-i)简单地实现,即2^k = i&(-i) 。

       这里利用了负数的存储特性,在计算机中负整数是以补码存储的,对于整数运算 x&(-x)有:

        1)当i为0时,即 0 & 0,结果为0;

        2)当i为奇数时,最后一个比特位为1,取反加1没有进位,故x和-x除最后一位外前面的位正好相反(按位与结果为0),最后1位都为1,按位与后结果为1。

        3)当i为偶数,且为2的m次方时,x的二进制表示中只有一位是1(设为从右往左的第m+1位),其右边有m位0,故x取反加1后,从右到左第有m个0,第m+1位及其左边全是1。这样,x& (-x) 得到的就是x。

       4)当i为偶数,却不为2的m次方的形式时,可以写作i= y * (2^k)。其中,y的最低位为1。实际上就是把i用一个奇数左移k位来表示。这时,i的二进制表示最右边有k个0,从右往左第k+1位为1。当对i取反时,最右边的k位0变成1,第k+1位变为0;再加1,最右边的k位就又变成了0,第k+1位因为进位的关系变成了1。左边的位因为没有进位,正好和i原来对应的位上的值相反。二者按位与,得到:第k+1位上为1,左边右边都为0。结果为2^k。

2.树状数组的应用

        下面我们通过实例介绍树状数组的应用。

2.1  单点更新、区间查询

         对给定数组的某个元素进行更新,然后对区间的和值进行查询。

例1  【模板】树状数组 1

        本题选自洛谷题库(https://www.luogu.com.cn/problem/P3374)

题目描述

已知一个数列,你需要进行下面两种操作:

将某一个数加上x

求出某区间每一个数的和

输入格式

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

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

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

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

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

输出格式

输出包含若干行整数,即为所有操作 22 的结果。

输入样例

5 5

1 5 4 2 3

1 1 3

2 2 5

1 3 -1

1 4 2

2 1 4

输出样例

14

16

      (1)编程思路。

       典型的树状数组应用模板题。直接编写并调用前面介绍的三个函数即可。

      (2)源程序。

#include <stdio.h>

#include <string.h>

int n;

int c[500001];

int lowbit(int x)

{

    return x&(-x);

}

void update(int i,int value)

{

    while (i<=n)

    {

        c[i]+=value;

        i+=lowbit(i);

    }

}

int getsum(int i)

{

    int res = 0;

    while(i > 0)

    {

        res += c[i];

        i -= lowbit(i);

    }

    return res;

}

int main()

{

    int m;

    scanf("%d%d",&n,&m);

    memset(c, 0, sizeof c);

    int i,p,x,y;

    for (i=1;i<=n;i++)

    {

        scanf("%d",&x);

        update(i,x);

    }

    for (i=1;i<=m;i++)

    {

        scanf("%d%d%d",&p,&x,&y);

        if (p==1) update(x,y);

        else printf("%d\n",getsum(y)-getsum(x-1));

    }

    return 0;

}

2.2  区间更新、单点查询

       设有数组A[n],若要将区间[x,y]内的所有元素的值全部加上k或者减去k,然后查询某个元素A[i]的值,这种时候该怎么做呢?

       如果直接采用数组A的初始各元素值建立树状数组C,然后将[x,y]区间内每个值都更新,最后利用getsum(i)-getsum(i-1)得到待查询元素A[i]的值。这种办法的复杂度肯定是不行的。最好的办法是引入差分,利用差分建树。

      设有数组A[n+1],且令A[0]=0,利用公式 B[i]=A[i]-A[i-1]  (1≤i≤n) 建立差分数组B。

      显然有 A[i]=B[1]+B[2]+B[3]+ …+B[i]

                      =A[1]-A[0]+A[2]-A[1]+…+A[i]-A[i-1] =A[i]

       这样,待查询数组元素A[i]的值就是差分数组B的前i项的和。

       当数组A的区间[x,y]内的所有元素(A[x]~A[y])的值全部加上k或者减去k时,区间内的元素的差值是不变的,因此对应B数组只有B[x] 加上k或者减去k、B[y+1] 减去k或加上k,其余元素值不变。

        这样,我们用差分数组B构造树状数组C,每次区间修改就只进行两次单点修改。 

例2  【模板】树状数组 2

          本题选自洛谷题库(https://www.luogu.com.cn/problem/P3368)

题目描述

已知一个数列,你需要进行下面两种操作:

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

求出某一个数的值。

输入格式

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

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

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

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

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

输出格式

输出包含若干行整数,即为所有操作 22 的结果。

输入样例

5 5

1 5 4 2 3

1 2 4 2

2 3

1 1 5 -1

1 3 5 7

2 4

输出样例

6

10

       (1)编程思路。

      典型的区间更新、单点查询模板题。利用数组相邻元素的差值构造树状数组。

      (2)源程序。

#include <stdio.h>

#include <string.h>

int n;

int a[500001],c[500001];

int lowbit(int x)

{

    return x&(-x);

}

void update(int i,int value)

{

    while (i<=n)

    {

        c[i]+=value;

        i+=lowbit(i);

    }

}

int getsum(int i)

{

    int res = 0;

    while(i > 0)

    {

        res += c[i];

        i -= lowbit(i);

    }

    return res;

}

int main()

{

    int m,i;

    scanf("%d%d",&n,&m);

    memset(c, 0, sizeof c);

    for (i = 1; i<=n; i++)

    {

        scanf("%d",&a[i]);

        update(i,a[i] - a[i-1]);

    }

    for (i=1;i<=m;i++)

    {

        int f,x,y,k;

        scanf("%d",&f);

        if (f==1)

        {

            scanf("%d%d%d",&x,&y,&k);

            update(x,k);

            update(y+1,-k);

        }

        if (f==2)

        {

            scanf("%d",&x);

            printf("%d\n",getsum(x));

        }

    }

    return 0;

}

例3  Color the ball

       本题选自杭州电子科技大学OJ题库(http://acm.hdu.edu.cn/showproblem.php?pid=1556)。

Problem Description

N个气球排成一排,从左到右依次编号为1,2,3....N。每次给定2个整数a b(a <= b),lele便为骑上他的“小飞鸽”牌电动车从气球a开始到气球b依次给每个气球涂一次颜色。但是N次以后lele已经忘记了第I个气球已经涂过几次颜色了,你能帮他算出每个气球被涂过几次颜色吗?

Input

每个测试实例第一行为一个整数N,(N <= 100000).接下来的N行,每行包括2个整数a b(1 <= a <= b <= N)。

当N = 0,输入结束。

Output

每个测试实例输出一行,包括N个整数,第I个数代表第I个气球总共被涂色的次数。

Sample Input

3

1 1

2 2

3 3

3

1 1

1 2

1 3

0

Sample Output

1 1 1

3 2 1

       (1)编程思路。

       设有数组A[N+1],A[i]的值表示编号为i的气球被涂色的次数,初始时元素值全为0。从气球a开始到气球b依次给每个气球涂一次颜色相当于对数组A的区间[a,b]中的每个元素加1。若定义数组A的差值数组B(差值数组的初始值显然也全为0),则对数组A的区间[a,b]中的各元素进行加1修改操作,只需对应对数组B的两个元素B[a]和B[b+1]进行单点修改即可,其中B[a]加1,B[b+1]减1。

       根据树状数组的原理,A[i]的值就是差值数组B的前i项的和,调用函数getsum(i)即可。

       (2)源程序。

#include <stdio.h>

#include <string.h>

int n;

int c[100001];

int lowbit(int x)

{

    return x&(-x);

}

void update(int i,int value)

{

    while (i<=n)

    {

        c[i]+=value;

        i+=lowbit(i);

    }

}

int getsum(int i)

{

    int res = 0;

    while(i > 0)

    {

        res += c[i];

        i -= lowbit(i);

    }

    return res;

}

int main()

{

    while (scanf("%d",&n) && n!=0)

    {

        memset(c, 0, sizeof c);

        for (int i=1; i<=n; i++)

        {

            int a,b;

            scanf("%d%d",&a,&b);

            update(a, 1);       

            update(b + 1, -1);    

        }

        for (int i = 1; i<n; i++)

        {

            printf("%d ",getsum(i));

        }

        printf("%d\n",getsum(n));

    }

    return 0;

}

例4  Matrix

       本题选自北京大学OJ题库(http://poj.org/problem?id=2155)。

Description

Given an N*N matrix A, whose elements are either 0 or 1. A[i, j] means the number in the i-th row and j-th column. Initially we have A[i, j] = 0 (1 <= i, j <= N).

We can change the matrix in the following way. Given a rectangle whose upper-left corner is (x1, y1) and lower-right corner is (x2, y2), we change all the elements in the rectangle by using "not" operation (if it is a '0' then change it into '1' otherwise change it into '0'). To maintain the information of the matrix, you are asked to write a program to receive and execute two kinds of instructions.

1. C x1 y1 x2 y2 (1 <= x1 <= x2 <= n, 1 <= y1 <= y2 <= n) changes the matrix by using the rectangle whose upper-left corner is (x1, y1) and lower-right corner is (x2, y2).

2. Q x y (1 <= x, y <= n) querys A[x, y].

Input

The first line of the input is an integer X (X <= 10) representing the number of test cases. The following X blocks each represents a test case.

The first line of each block contains two numbers N and T (2 <= N <= 1000, 1 <= T <= 50000) representing the size of the matrix and the number of the instructions. The following T lines each represents an instruction having the format "Q x y" or "C x1 y1 x2 y2", which has been described above.

Output

For each querying output one line, which has an integer representing A[x, y].

There is a blank line between every two continuous test cases.

Sample Input

1

2 10

C 2 1 2 2

Q 2 2

C 2 1 2 1

Q 1 1

C 1 1 2 1

C 1 2 1 2

C 1 1 2 2

Q 1 1

C 1 1 2 1

Q 2 1

Sample Output

1

0

0

1

      (1)编程思路。

       本题的题意是给定N*N矩阵A,矩阵中的元素值非0即1,初始时元素值全为0。对矩阵中的子矩阵[x1, y1, x2, y2](x1、y1表示子矩阵左上角元素下标,x2、y2表示子矩阵右下角元素坐标)中的各元素进行变反操作(0变1,1变0),查询矩阵中元素A[x][y]的值。

       根据题目中矩阵A的元素值不是 1 就是 0这一特点,我们只需记录每个元素值改变过几次,即可得到每个元素的值。

       由例3可知,当数组是一维时,若要修改[x,y]区间的值,只需修改x和y+1这两个点的值(将点x值加1,点y+1值减1)。查询k点的值时,调用函数getsum(k)即可。

       当数组为二维时,解决方法类同一维。要修改范围[x1, y1, x2, y2],只需修改这四个点:(x1,y1), (x1,y2+1), (x2+1,y1), (x2+1,y2+1)。查询点(x,y)的值时,调用函数getsum(x, y)。

       按树状数组的原理,需要将单点修改和求前缀和函数修改为二维的处理情况。

    (2)源程序。

#include <stdio.h>

#include <string.h>

int n;

int c[1001][1001];

int lowbit(int x)

{

    return x&(-x);

}

void update(int x, int y, int value)

{

    for (int i = x; i<= n; i+=lowbit(i))

       for (int j=y; j<= n; j+=lowbit(j))

          c[i][j]+=value;

}

int getsum(int x, int y)

{

    int res = 0;

    for (int i = x; i>0; i-=lowbit(i))

        for (int j = y; j > 0; j -= lowbit(j))

            res += c[i][j];

    return res;

}

int main()

{

    int t;

    scanf("%d",&t);

    while(t--)

    {

        int m,x1,y1,x2,y2;

        scanf("%d%d", &n, &m);

        memset(c, 0, sizeof(c));

        char op[5];

        while (m--)

        {

            scanf("%s%d%d", op, &x1, &y1);

            if (op[0] == 'C')

            {

                scanf("%d%d", &x2, &y2);

                update(x1, y1, 1);

                update(x1, y2+1, 1);

                update(x2+1, y1, 1);

                update(x2+1, y2+1, 1);

            }

            else

            {

                printf("%d\n", getsum(x1,y1) %2);

            }

        }

        printf("\n");

    }

    return 0;

}

例5  Flowers

       本题选自杭州电子科技大学OJ题库(http://acm.hdu.edu.cn/showproblem.php?pid=4325)。

Problem Description

As is known to all, the blooming time and duration varies between different kinds of flowers. Now there is a garden planted full of flowers. The gardener wants to know how many flowers will bloom in the garden in a specific time. But there are too many flowers in the garden, so he wants you to help him.

Input

The first line contains a single integer t (1 <= t <= 10), the number of test cases.

For each case, the first line contains two integer N and M, where N (1 <= N <= 10^5) is the number of flowers, and M (1 <= M <= 10^5) is the query times.

In the next N lines, each line contains two integer Si and Ti (1 <= Si <= Ti <= 10^9), means i-th flower will be blooming at time [Si, Ti].

In the next M lines, each line contains an integer Ti, means the time of i-th query.

Output

For each case, output the case number as shown and then print M lines. Each line contains an integer, meaning the number of blooming flowers.

Sample outputs are available for more details.

Sample Input

2

1 1

5 10

4

2 3

1 4

4 8

1

4

6

Sample Output

Case #1:

0

Case #2:

1

2

1

       (1)编程思路1。

       本题的题意是:有n朵花和m个时间点,第i朵花正在开放的时间是si和ti,问在第mi个时间点有多少朵花正在开放?

       类同例3的编程思路。初始时所有时间点都没有花儿开放。第i朵花正在开放的时间是si和ti,相当于对区间[si,ti]中每个元素加1(有一朵花开放)。问在第mi个时间点有多少朵花正在开放,调用函数getsum(mi)得到结果。

       先定义树状数组的大小为int c[100005];,仿例3编写如下的源程序。

      (2)可以Accepted但存在缺陷的源程序。

#include <stdio.h>

#include <string.h>

int n;

int c[100005];

int lowbit(int x)

{

    return x&(-x);

}

void update(int i,int value)

{

    while (i<=100005)

    {

        c[i]+=value;

        i+=lowbit(i);

    }

}

int getsum(int i)

{

    int res = 0;

    while(i > 0)

    {

        res += c[i];

        i -= lowbit(i);

    }

    return res;

}

int main()

{

    int t;

    scanf("%d",&t);

    for (int k=1;k<=t;k++)

    {

        memset(c, 0, sizeof c);

        int m;

        scanf("%d%d",&n,&m);

        int i,s,t;

        for (i=1; i<=n; i++)

        {

            scanf("%d%d",&s,&t);

            update(s, 1);

            update(t + 1, -1);

        }

        printf("Case #%d:\n",k);

        for (i = 1; i<=m; i++)

        {

            scanf("%d",&t);

            printf("%d\n",getsum(t));

        }

    }

    return 0;

}

      这个源程序提交给OJ系统后,可以Accepted。但实际上这个程序是存在问题的,因为按题意花开的开始时间和结束时间Si 和Ti数据范围为1 <= Si <= Ti <= 10^9,而程序中定义的数组C,最大为100005。之所以Accepted,是因为测试数据集太弱,与题目中数据说明存在偏差。如果测试数据中输入的Si或Ti大于10^5,程序运行会出错。但将数组C定义为int C[10e9+1]是不现实地,因为数组C会过大,无法通过编译。

      (3)编程思路2。

       由题意,输入的Si 和Ti数据范围为1 <= Si <= Ti <= 10^9,而花儿朵数N的范围为1 <= N <= 10^5,每朵花开放有两个时间点,查询时间点个数M的范围为1 <= N <= 10^5,因此可以想办法将1~ 10^9范围的数据缩小到1~3*10^5的范围。为了将较大的数据范围缩小到一个较小的数据范围,可以进行离散化处理。

       离散化的处理方法是:将原始数据按从小到大顺序排列,然后对数据编名次(相同的数据名次相同),然后各原始数据用其名次表示。

       以测试样例为例说明。

       例如,两朵花的开放时间点分别为1、4和4、8,3个查询时间点为1、4、6,这7个时间点按从小到大排序为1、1、4、4、6、8,相应名次为1、1、2、2、3、4,这样离散化后,1用1表示,4用2表示,6用3表示,8用4表示。

       又如,3朵花的开放时间点分别为100、420,420、8900和123456789、987654321,4个查询时间点为10、400、6000和87654,这10个时间点按从小到大排序为:

       10、100、400、420、420、6000、8900、87654、123456789、987654321,相应名次为1、2、3、4、4、5、6、7、8、9,这样离散化后,10用1表示,100用2表示,400用3表示,420用4表示,…,987654321用5表示。

       离散化之后就可以采用树状数组解决问题了。

      (4)正确进行离散化处理的源程序。

#include <stdio.h>

#include <string.h>

#include <algorithm>

using namespace std;

struct node

{

    int id,x;

};

struct node a[300005];

int c[300005],rankk[300005];

bool cmp(struct node a,struct node b)

{

    return a.x<b.x;

}

int lowbit(int x)

{

    return x&(-x);

}

void update(int i,int n,int value)

{

    while (i<=n)

    {

        c[i]+=value;

        i+=lowbit(i);

    }

}

int getsum(int i)

{

    int res = 0;

    while(i > 0)

    {

        res += c[i];

        i -= lowbit(i);

    }

    return res;

}

int main()

{

    int t;

    scanf("%d",&t);

    for (int k=1;k<=t;k++)

    {

        int n,m;

        scanf("%d%d",&n,&m);

        memset(c,0,sizeof(c));

        memset(rankk,0,sizeof(rankk));

        int temp=2*n+m,i;

        for (i=1;i<=temp;i++)

        {

             scanf("%d",&a[i].x);

             a[i].id=i;

        }

        sort(a+1,a+1+temp,cmp);

        int cnt=1;

        rankk[a[1].id]=cnt;

        for (i=2;i<=temp;i++)

        {

            if(a[i].x==a[i-1].x)    // 判重

                rankk[a[i].id]=cnt;

            else

                rankk[a[i].id]=++cnt;

        }

        for (i=1;i<2*n;i+=2)

        {

             update(rankk[i],cnt,1);

             update(rankk[i+1]+1,cnt,-1);

        }

        printf("Case #%d:\n",k);

        for (i=2*n+1;i<=temp;i++)

        {

            printf("%d\n",getsum(rankk[i]));

        }

    }

    return 0;

}

2.3  区间更新、区间查询

        由前面介绍的树状数组原理可知,树状数组的本质就是实现时间复杂度为O(log2n) 的单点更新,区间查询”,将其进行变形,利用差值建树状数组,可以实现“区间更新,单点查询”。那么,如何实现“区间更新,区间查询”呢?

       设有数组a[n+1],且令a[0]=0,利用公式 c[i]=a[i]-a[i-1]  (1≤i≤n) 建立差分数组c。

观察式子:

a[1]+a[2]+...+a[n]

=(a[1]-0)+(a[1]-0+a[2]-a[1])+…+ (a[1]-0 +a[2]-a[1]+…+a[n]-a[n-1])

= (c[1]) + (c[1]+c[2]) + … + (c[1]+c[2]+ … +c[n]) 

= n*c[1] + (n-1)*c[2] +... +c[n]

= n * (c[1]+c[2]+...+c[n]) - (0*c[1]+1*c[2]+...+(n-1)*c[n])

    再建立一个树状数组c2,其中c2[i] = (i-1)*c[i],由此,

a[1]+a[2]+...+a[n] = n* getsum (c,n) - getsum (c2,n)  

                   // 其中,getsum (c,n)表示树状数组c的前n项的和

    而x到y的区间和即为:

(y* getsum (c,y) - getsum (c2,y))- ((x-1)* getsum (c,x-1) - getsum (c2,x-1))

例6  【模板】线段树 1

    本题选自洛谷题库(https://www.luogu.com.cn/problem/ P3372)

题目描述

已知一个数列,你需要进行下面两种操作:

将某区间每一个数加上 k。

求出某区间每一个数的和。

输入格式

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

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

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

1 x y k:将区间 [x, y]内每个数加上 k。

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

输出格式

输出包含若干行整数,即为所有操作 2 的结果。

输入样例

5 5

1 5 4 2 3

2 2 4

1 2 3 2

2 3 4

1 1 5 1

2 1 4

输出样例

11

8

20

      (1)编程思路。

       典型的区间更新、区间查询模板题。利用数组相邻元素的差值构造树状数组c1,再利用树状数组c1构造树状数组c2。

      (2)源程序。

#include <stdio.h>

#include <string.h>

typedef long long LL;

#define MAXN 100001

LL n;

LL c1[MAXN],c2[MAXN];

LL lowbit(LL x)

{

       return x&(-x);

}

 

void update(LL x,LL value,LL *c)

{

       while(x<=n)

    {

              c[x]+=value;

              x+=lowbit(x);

       }

}

LL getsum(LL x,LL *c)

{

       LL sum=0;

       while(x>0)

    {

              sum+=c[x];

              x-=lowbit(x);

       }

       return sum;

}

int main()

{

       int q,op;

       LL x,y,d;

       scanf("%lld%d",&n,&q);

    memset(c1,0,sizeof(c1));

       memset(c2,0,sizeof(c2));

       y=0;

       for (int i=1;i<=n;++i)

       {

           scanf("%lld",&x);

           d=x-y;

           update(i,d,c1);

           update(i,(i-1)*d,c2);

           y=x;

       }

       for(int i=0;i<q;++i)

    {

           scanf("%d",&op);

              if (op==1)

              {

                     scanf("%lld%lld%lld",&x,&y,&d);

                     update(x,d,c1);

                     update(y+1,-d,c1);

                     update(x,(x-1)*d,c2);

                     update(y+1,-y*d,c2);

              }

        else

        {

                     scanf("%lld%lld",&x,&y);

                     printf("%lld\n",getsum(y,c1)*y-getsum(x-1,c1)*(x-1) -getsum(y,c2)+getsum(x-1,c2));

              }

       }

       return 0;

}

posted on 2020-12-18 13:32  aTeacher  阅读(331)  评论(0编辑  收藏  举报