欢迎光临
我们一直在努力

数状数组的学习与分析

文章目录

  • 树状数组是什么
  • 树状数组完整原理推导
    • 一.树状数组核心思想
    • 二.前缀和查询原理
    • 三.单点更新原理
      • 重点注释:
    • 四.多语言代码展示
      • 引用牛客PEEK73 【模板】动态区间和Ⅰ ‖ 单点修改 + 区间查询
      • Java的代码如下:
      • C++代码如下:
      • C语言代码如下:
      • Python代码如下:
      • 树状数组代码时间+空间复杂度汇总表
  • 树状数组的更多应用:
    • 1.区间增量与单点求值
      • 引用:牛客PEEK74 区间增量与单点求值
    • 2.区间增值和区间求和
      • 引用洛谷P3372 【模板】线段树 1
        • 解法一.树状数组暴力解法
        • 解法二.双树状数组模拟解法
        • 一、基础定义
        • 二、步骤1:证明差分数组前缀和

          c

          [

          k

          ]

          =

          a

          k

          c[k] = a_k

          c[k]=ak

        • 三、步骤2:推导原数组前

          k

          k

          k 项总和

          S

          (

          k

          )

          =

          a

          1

          +

          a

          2

          +

          +

          a

          k

          S(k) = a_1+a_2+\\dots+a_k

          S(k)=a1+a2++ak

          • 交换二重求和顺序
          • 拆分求和项
        • 四、最终结论
        • 解法三.线段树经典解法
    • 3.二进制区间取反与单点求值
      • 引用:牛客PEEK75 区间取反与单点求值
        • 思考
    • 4.二维单点增加和范围查询
    • 5.二维范围增加和范围查询
      • 引用洛谷P4514
        • 二维区间加、区间求和完整数学推导
          • 1. 基础定义
          • 2. 定义全局前缀和

            S

            (

            x

            ,

            y

            )

            S(x,y)

            S(x,y)

          • 3. 交换求和顺序
          • 4. 展开多项式

            (

            x

            p

            +

            1

            )

            (

            y

            q

            +

            1

            )

            (x-p+1)(y-q+1)

            (xp+1)(yq+1)

          • 5. 代入拆分四项求和
          • 6. 定义四个二维前缀和(对应代码4棵树)
  • 总结一下

树状数组是什么

试想一下:在上世纪在上世纪 90 年代,无损压缩领域广泛使用算术编码(一种可以逼近理论压缩极限的无损压缩算法,核心原理是将一整段文本映射到区间[0,1)内唯一小数完成压缩,编码、解码全程需要频繁查询符号累积出现总次数,也就是累积频率表,因此要解决自适应算术编码中累积频率表动态维护的问题),所以这个时候前缀和,暴力遍历求解,平衡二叉树就挑起了担子,我们知道前缀和在求加和的时候需要O(n) 的时间复杂度(即求l,r的下标数组和需要构建前缀和数组,然后前缀和数组的第r项减去第l-1项即可),而暴力求解即为O(n),在多次查询加和的时候就会更耗时间,而虽然平衡二叉树可以做 O(logn) 的时间复杂度,但逻辑复杂,代码量大,且依赖指针节点内存开销大、内存访问不连续缓存效率差,实际运行常数就会很高,所以这个时候还是会想到计算机底层的0和1,如果我能把数字插为2的幂(即以二进制的形式)用于计算加和,且利用 lowbit 拆分下标分段存储局部区间和的思想,树状数组就诞生了。

树状数组完整原理推导

我们用一段推导来表达它的原理:

一.树状数组核心思想

我们在这里对任意数组下标

x

x

x

t

r

e

e

[

x

]

tree[x]

tree[x]专门维护一段连续区间: 区间右端点固定为

x

x

x,区间长度等于

l

o

w

b

i

t

(

x

)

lowbit(x)

lowbit(x),这样的区域为下标x所管辖的区域。 假设有一个下标1~16的数组,我们每次通过对半开的形式,然后计算各下标单独管辖范围:

  • x

    =

    1

    x=1

    x=1

    l

    o

    w

    b

    i

    t

    (

    1

    )

    =

    1

    lowbit(1)=1

    lowbit(1)=1,管辖区间

    [

    1

    ]

    [1]

    [1]

  • x

    =

    2

    x=2

    x=2

    l

    o

    w

    b

    i

    t

    (

    2

    )

    =

    2

    lowbit(2)=2

    lowbit(2)=2,管辖区间

    [

    1

    ,

    2

    ]

    [1,2]

    [1,2]

  • x

    =

    3

    x=3

    x=3

    l

    o

    w

    b

    i

    t

    (

    3

    )

    =

    1

    lowbit(3)=1

    lowbit(3)=1,管辖区间

    [

    3

    ]

    [3]

    [3]

  • x

    =

    4

    x=4

    x=4

    l

    o

    w

    b

    i

    t

    (

    4

    )

    =

    4

    lowbit(4)=4

    lowbit(4)=4,管辖区间

    [

    1

    ,

    2

    ,

    3

    ,

    4

    ]

    [1,2,3,4]

    [1,2,3,4]

  • x

    =

    5

    x=5

    x=5

    l

    o

    w

    b

    i

    t

    (

    5

    )

    =

    1

    lowbit(5)=1

    lowbit(5)=1,管辖区间

    [

    5

    ]

    [5]

    [5]

  • x

    =

    6

    x=6

    x=6

    l

    o

    w

    b

    i

    t

    (

    6

    )

    =

    2

    lowbit(6)=2

    lowbit(6)=2,管辖区间

    [

    5

    ,

    6

    ]

    [5,6]

    [5,6]

  • x

    =

    7

    x=7

    x=7

    l

    o

    w

    b

    i

    t

    (

    7

    )

    =

    1

    lowbit(7)=1

    lowbit(7)=1,管辖区间

    [

    7

    ]

    [7]

    [7]

  • x

    =

    8

    x=8

    x=8

    l

    o

    w

    b

    i

    t

    (

    8

    )

    =

    8

    lowbit(8)=8

    lowbit(8)=8,管辖区间

    [

    1

    8

    ]

    [1\\sim8]

    [18]

  • x

    =

    9

    x=9

    x=9,管辖

    [

    9

    ]

    [9]

    [9]

  • x

    =

    10

    x=10

    x=10,管辖

    [

    9

    ,

    10

    ]

    [9,10]

    [9,10]

  • x

    =

    11

    x=11

    x=11,管辖

    [

    11

    ]

    [11]

    [11]

  • x

    =

    12

    x=12

    x=12,管辖

    [

    9

    ,

    10

    ,

    11

    ,

    12

    ]

    [9,10,11,12]

    [9,10,11,12]

  • x

    =

    13

    x=13

    x=13,管辖

    [

    13

    ]

    [13]

    [13]

  • x

    =

    14

    x=14

    x=14,管辖

    [

    13

    ,

    14

    ]

    [13,14]

    [13,14]

  • x

    =

    15

    x=15

    x=15,管辖

    [

    15

    ]

    [15]

    [15]

  • x

    =

    16

    x=16

    x=16,管全部。 也就是说在

    1

    n

    1\\sim n

    1n范围内,任意下标

    x

    x

    x,以

    x

    x

    x作为子数组右边界、区间长度为

    l

    o

    w

    b

    i

    t

    (

    x

    )

    lowbit(x)

    lowbit(x)的连续区间,就是树状数组

    t

    r

    e

    e

    [

    x

    ]

    tree[x]

    tree[x]独立管理的区域;所有以

    x

    x

    x为右边界的前缀区间,都可以拆分为多段上述标准区间累加,也就是前缀和查询的底层逻辑。 左神课上的原图: 在这里插入图片描述

二.前缀和查询原理

求前缀和

s

u

m

(

1

x

)

sum(1\\sim x)

sum(1x)流程:

  • 累加当前

    x

    x

    x对应的管辖区间和

    t

    r

    e

    e

    [

    x

    ]

    tree[x]

    tree[x]

  • x

    =

    x

    l

    o

    w

    b

    i

    t

    (

    x

    )

    x = x – lowbit(x)

    x=xlowbit(x),跳到下一段靠左区间的右边界;

  • 循环操作直至

    x

    =

    0

    x=0

    x=0。 举例:求

    s

    u

    m

    (

    1

    6

    )

    sum(1\\sim6)

    sum(16)

  • x

    =

    6

    x=6

    x=6

    l

    o

    w

    b

    i

    t

    =

    2

    lowbit=2

    lowbit=2,管辖

    [

    5

    ,

    6

    ]

    [5,6]

    [5,6],累加

    t

    r

    e

    e

    [

    6

    ]

    tree[6]

    tree[6]

    x

    =

    6

    2

    =

    4

    x=6-2=4

    x=62=4

  • x

    =

    4

    x=4

    x=4

    l

    o

    w

    b

    i

    t

    =

    4

    lowbit=4

    lowbit=4,管辖

    [

    1

    ,

    4

    ]

    [1,4]

    [1,4],累加

    t

    r

    e

    e

    [

    4

    ]

    tree[4]

    tree[4]

    x

    =

    4

    4

    =

    0

    x=4-4=0

    x=44=0 最终总和:

    t

    r

    e

    e

    [

    4

    ]

    +

    t

    r

    e

    e

    [

    6

    ]

    tree[4]+tree[6]

    tree[4]+tree[6],完整覆盖

    1

    6

    1\\sim6

    16所有元素。 通俗点来说就是比如说我们要求1~12的累加和,就先看12的二进制(1100),对应到图表中12管9 ~12下标的元素,然后我们找到它二进制最右边1,然后把它消去,变为(1000)就是8,然后在对应上面,它管1 ~8,那么我们按这样的变换把所有元素加起来就是1 ~12的和了

  • 三.单点更新原理

    给下标

    x

    x

    x数值增加

    v

    v

    v流程:

  • t

    r

    e

    e

    [

    x

    ]

    +

    =

    v

    tree[x] += v

    tree[x]+=v,更新当前

    x

    x

    x管辖区间;

  • x

    =

    x

    +

    l

    o

    w

    b

    i

    t

    (

    x

    )

    x = x + lowbit(x)

    x=x+lowbit(x),跳到所有覆盖

    x

    x

    x的更大右边界;

  • 循环操作直至

    x

    >

    n

    x>n

    x>n

  • 原理:所有右端点大于

    x

    x

    x、且管辖区间包含下标

    x

    x

    x的树状数组节点,都需要同步更新数值。 这里也是一样,对一个值进行更行,比如给第3个元素加一个v,我们要看它的树状数组每个下标,看有哪几个管辖这下标3,它们的值都要加上一个v,那应该怎么做呢?其实还是位运算,3的二进制是0011,那我们减去最右边的1,然后再加上原二进制加上它最右侧的1,就是0010+0010=0100(就是4的二进制),和上面的推导相符,然后接着这样(直到越界)就可找到每一个管辖了3的下标,把它们都加上一个v,这样我们就修改成功了。

    重点注释:

    树状数组的核心是要取一个数字二进制的最右边的1,那为啥x&(-x)能实现呢,推导过程如下:

    11的二进制是00001011,对它进形结论中的操作:
    +11原码:00001011
    -11原码:10001011
    -11反码:11110100
    -11补码:11110101
    0 0 0 0 1 0 1 1
    1 1 1 1 0 1 0 1
    ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
    0 0 0 0 0 0 0 1
    可知11的二进制最低位的1恰好是原数最低位的1
    其实就是负数用补码(按位取反 + 1),使x与-x仅在最低位 1 处同为 1,按位与便只取出该位权值,得到它二进制最低位1的位置
    即证明

    四.多语言代码展示

    基于以上原理:

    引用牛客PEEK73 【模板】动态区间和Ⅰ ‖ 单点修改 + 区间查询

    牛客PEEK73 题目链接如下: 牛客树状数组模版题 同样的牛客这个题对应洛谷的P3374,链接如下: 洛谷P3374树状数组模版题

    Java的代码如下:

    import java.io.*;
    import java.util.*;
    public class Main{
    static BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
    static PrintWriter out=new PrintWriter(System.out);
    static StringTokenizer st;
    static int N=500010;//按题目开出一个合适的空间
    static long []Tree;
    static int op;
    static int n;
    static int m;
    public static int lowbit(int x){//核心方法:取出二进制最右边的1
    return x&(x);
    }
    public static void add(int x,int v){
    while(x<=N){//和上面的推导一样
    Tree[x]+=v;//把这些管辖了x的下标元素都加v
    x+=lowbit(x);//找到每位管辖x的下标
    }
    }
    public static long query(int x){
    long res=0;
    while(x>0){
    res+=Tree[x];//再把每个下标的元素相加
    x-=lowbit(x);//我们不断抹去x的二进制最右边的1
    }
    return res;
    }
    public static void main(String[] args) throws IOException{
    Tree = new long[N + 1];
    st=new StringTokenizer(br.readLine());
    n=Integer.parseInt(st.nextToken());
    m=Integer.parseInt(st.nextToken());
    st=new StringTokenizer(br.readLine());
    for(int i=1;i<=n;i++){
    int num=Integer.parseInt(st.nextToken());
    add(i,num);//这个一定不要忘记了,就是要初始化
    }
    while(m>0){
    st=new StringTokenizer(br.readLine());
    op=Integer.parseInt(st.nextToken());
    if(op==1){
    int i=Integer.parseInt(st.nextToken());
    int x=Integer.parseInt(st.nextToken());
    add(i,x);
    }else{
    int l=Integer.parseInt(st.nextToken());
    int r=Integer.parseInt(st.nextToken());
    out.println(query(r)query(l1));
    }
    }
    out.flush();
    br.close();
    out.close();
    }
    }

    C++代码如下:

    //思路和前面的一样
    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const ll N = 10000010;
    static ll Tree[N];
    static ll n, m,op;
    static ll lowbit(ll x) {
    return x & (x);
    }
    static void add(ll x, ll v) {
    while (x <= N) {
    Tree[x] += v;
    x += lowbit(x);
    }
    }
    static ll query(ll x) {
    ll res = 0;
    while (x > 0) {
    res += Tree[x];
    x -= lowbit(x);
    }
    return res;
    }
    int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
    int num;
    cin >> num;
    add(i, num);
    }
    while (m) {
    cin>>op;
    if(op==1){
    int i,x;
    cin>>i>>x;
    add(i,x);
    }else{
    int l,r;
    cin>>l>>r;
    cout<<query(r)query(l1)<<"\\n";
    }
    }
    return 0;
    }

    C语言代码如下:

    //和前面的思路也一样
    #include <stdio.h>
    typedef long long ll;
    const int N = 500010;
    ll Tree[N];
    static int lowbit(int x) {
    return x & (x);
    }
    static void add(int x, ll v) {
    while (x <= N) {
    Tree[x] += v;
    x += lowbit(x);
    }
    }
    static ll query(int x) {
    ll res = 0;
    while (x > 0) {
    res += Tree[x];
    x -= lowbit(x);
    }
    return res;
    }
    int main() {
    int n, m;
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; i++) {
    ll num;
    scanf("%lld", &num);
    add(i, num);
    }
    while (m) {
    int op;
    scanf("%d", &op);
    if (op == 1) {
    int i;
    ll x;
    scanf("%d %lld", &i, &x);
    add(i, x);
    } else {
    int l, r;
    scanf("%d %d", &l, &r);
    printf("%lld\\n", query(r) query(l 1));
    }
    }
    return 0;
    }

    Python代码如下:

    # 和前面的思路也相同
    import sys
    def main():
    data = list(map(int, sys.stdin.read().split()))
    ptr = 0
    N = 500010
    Tree = [0] * (N + 1)
    def lowbit(x):
    return x & x
    def add(x, v):
    while x <= N:
    Tree[x] += v
    x += lowbit(x)
    def query(x):
    res = 0
    while x > 0:
    res += Tree[x]
    x -= lowbit(x)
    return res
    n = data[ptr]
    ptr += 1
    m = data[ptr]
    ptr += 1
    for i in range(1, n + 1):
    num = data[ptr]
    ptr += 1
    add(i, num)

    output = []
    for _ in range(m):
    op = data[ptr]
    ptr += 1
    if op == 1:
    i = data[ptr]
    ptr += 1
    val = data[ptr]
    ptr += 1
    add(i, val)
    else:
    l = data[ptr]
    ptr += 1
    r = data[ptr]
    ptr += 1
    res = query(r) query(l 1)
    output.append(str(res))

    print('\\n'.join(output))

    if __name__ == "__main__":
    main()

    它的时间复杂度如下:

    树状数组代码时间+空间复杂度汇总表

    模块名称核心功能单次时间复杂度空间开销补充说明
    lowbit(x) 函数 提取数字二进制最右侧1对应数值

    O

    (

    1

    )

    O(1)

    O(1)

    O

    (

    1

    )

    O(1)

    O(1)

    纯位运算,无额外空间
    add(x, v) 单点更新函数 下标x位置增加v,更新树状数组

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)

    O

    (

    1

    )

    O(1)

    O(1)

    仅局部变量,不新开数组
    query(x) 前缀和查询函数 计算

    [

    1

    ,

    x

    ]

    [1,x]

    [1,x] 累加和

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)

    O

    (

    1

    )

    O(1)

    O(1)

    仅临时存储结果res
    区间和查询 query®-query(l-1) 计算

    [

    l

    ,

    r

    ]

    [l,r]

    [l,r] 区间和

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)

    O

    (

    1

    )

    O(1)

    O(1)

    两次前缀查询,无额外空间
    数组初始化循环 读入原始数组初始化树状数组

    O

    (

    n

    log

    n

    )

    O(n\\log n)

    O(nlogn)

    不新增空间,仅调用add
    m次操作主循环 处理m次修改/查询

    O

    (

    m

    log

    n

    )

    O(m\\log n)

    O(mlogn)

    O

    (

    1

    )

    O(1)

    O(1)

    每次操作临时变量,空间常数
    全局数组 Tree[N] 树状数组存储容器

    O

    (

    N

    )

    O(N)

    O(N)

    定义常量 N=10000010,静态全局数组占用N个long long空间
    整体程序总复杂度 完整程序 时间:

    O

    (

    (

    n

    +

    m

    )

    log

    n

    )

    O((n+m)\\log n)

    O((n+m)logn)

    空间:

    O

    (

    N

    )

    O(N)

    O(N)

    N为预设最大数据范围,与输入n无关

    树状数组的更多应用:

    1.区间增量与单点求值

    引用:牛客PEEK74 区间增量与单点求值

    牛客PEEK74 题目链接如下: 牛客PEEK74区间增量与单点求值 同样的牛客这道题对应洛谷P3368,题目链接如下: 洛谷P3368区间增量与单点求值 其实在这里这个题唯一的改变就是从只改单个值,变为了区间改值,那我们在区间改值下能想到啥?就是差分,差分顾名思义,就是数组的前后两项相减,即:b[i]=a[i]-a[i-1],经典的模版如下:

    //实现一维差分
    import java.io.*;
    import java.util.*;
    public class Main{
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static PrintWriter out = new PrintWriter(System.out);
    static StringTokenizer st;
    static int n,m;
    public static void main(String[] args) throws IOException{
    st=new StringTokenizer(br.readLine());
    n=Integer.parseInt(st.nextToken());
    m=Integer.parseInt(st.nextToken());
    int[] a=new int[n+1];
    st=new StringTokenizer(br.readLine());
    for(int i=1;i<=n;i++){
    a[i]=Integer.parseInt(st.nextToken());
    }
    int []b=new int[n+2];
    b[1]=a[1];//差分数组的第一项就是原数组的第一项
    for(int i=2;i<=n;i++){
    b[i]=a[i]a[i1];//构建一个差分数组
    }
    while(m > 0){
    st=new StringTokenizer(br.readLine());
    int l=Integer.parseInt(st.nextToken());
    int r=Integer.parseInt(st.nextToken());
    int k=Integer.parseInt(st.nextToken());
    //对于修改l~r范围的值都加个k,为啥可以这样呢?
    //原理:差分数组求前缀和还原原数组
    //1.b[l]+=k:从l位置往后所有前缀和全部增加k
    //2.b[r+1]-=k:从r+1位置往后抵消增加的k,保证r之后数值不变
    //最终仅区间[l,r]内所有元素统一加上k,单次修改仅2次操作O(1)
    b[l]+=k;
    b[r+1]-=k;
    }
    for(int i=1;i<=n;i++){
    a[i]=a[i1]+b[i];//这里记得要加和回去进行前缀和变为原数组
    }
    for(int i=1;i<=n;i++){
    out.print(a[i]);//格式化输出
    if(i!=n) out.print(" ");
    }
    out.flush();
    out.close();
    br.close();
    }
    }

    所以基于这个原理,我们在创建树状数组的时候就应该初始化原数组的差分形式,在单点求值求值的时候就直接进行一个前缀和操作(对应树状数组的query方法)就可以了,而区间增加一个量就按上面这个差分的操作(对应树状数组的add方法),这道题就可以解了:

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const ll N = 500010;
    static ll Tree[N];
    static ll n, m;
    static ll lowbit(ll x) {
    return x & (x);
    }
    static void add(ll x, ll v) {
    while (x <= n) {
    Tree[x] += v;
    x += lowbit(x);
    }
    }
    static ll query(ll x) {
    ll res = 0;
    while (x > 0) {
    res += Tree[x];
    x -= lowbit(x);
    }
    return res;
    }
    int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    cin >> n >> m;
    vector<ll> a(n + 1);
    for (int i = 1; i <= n; i++) {
    cin >> a[i];
    add(i, a[i] a[i 1]);//初始化的是原数组的差分形式
    }
    while (m) {
    int op;
    cin >> op;
    if (op == 1) {
    int x, y, k;
    cin >> x >> y >> k;
    //和上面代码一样,以差分的方式对区间求值
    add(x, k);
    add(y + 1, k);
    } else {
    int x;
    cin >> x;
    cout << query(x) << "\\n";//差分求和就是原数组
    }
    }
    return 0;
    }

    2.区间增值和区间求和

    其实这个问题对于树状数组来说有难度,引用洛谷P3372线段数模版,在这里我会有三种不同的解法,但不是每一种都合适,也不是每一种都能解这个题:

    引用洛谷P3372 【模板】线段树 1

    洛谷P3372 题目链接如下: 洛谷P3372 在这里我们先不去想线段树的解法,在看到这个题目的要求,要我们实现区间增值和区间求和,能想到啥,前缀和和差分的结合不就能实现了吗,那怎么加在一起?以上面的树状数组为载体就可以了

    解法一.树状数组暴力解法

    代码如下:

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const ll MAX=1000010;
    static ll Tree[MAX];
    static ll sum[MAX];
    static int n,m;
    static int lowbit(int x) {
    return x&(x);
    }
    static void add1(int x,int v) {
    while (x<=n) {
    Tree[x]+=v;
    x+=lowbit(x);
    }
    }
    static ll query(int x) {
    ll res=0;
    while (x>0) {
    res+=Tree[x];
    x-=lowbit(x);
    }
    return res;
    }
    ll get_sum(int x){//这里实现了一个对差分和再求和的过程,即求前缀和
    sum[0] = 0;
    for(int i=1;i<=x;i++){
    sum[i] = sum[i1] + query(i);
    }
    return sum[x];
    }
    int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    cin>>n>>m;
    vector<int> a(n+1, 0);
    for (int i = 1; i <= n; i++) {
    cin>>a[i];
    add1(i, a[i] a[i1]);//也是传入它的差分形式
    }
    while (m) {
    int op;
    cin >> op;
    if (op == 1) {
    int L, R, k;
    cin >> L >> R >> k;
    //这里和前面差分的逻辑是一样的
    add1(L, k);
    add1(R+1, k);
    }else {
    int L,R;
    cin >> L >> R;
    ll total = get_sum(R) get_sum(L1);//前缀和的标准操作
    cout << total << '\\n';
    }
    }
    return 0;
    }

    理论上,这种解法是对的,但是,在考虑时间复杂度的时候就不行了,它的时间复杂度如下:

    代码模块功能单次时间复杂度说明
    lowbit(int x) 获取二进制最低位1

    O

    (

    1

    )

    O(1)

    O(1)

    位运算,常数时间
    add1(int x, int v) 树状数组单点加值

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)

    循环最多

    log

    2

    n

    \\log_2 n

    log2n

    query(int x) 求差分数组

    [

    1

    ,

    x

    ]

    [1,x]

    [1,x]前缀和

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)

    循环最多

    log

    2

    n

    \\log_2 n

    log2n

    get_sum(int x) 求原数组

    [

    1

    ,

    x

    ]

    [1,x]

    [1,x]前缀和

    O

    (

    n

    log

    n

    )

    O(n\\log n)

    O(nlogn)

    循环1~x,每次调用一次

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)的query

    初始化读入数组循环 构建差分树状数组

    O

    (

    n

    log

    n

    )

    O(n\\log n)

    O(nlogn)

    n次add1,每次

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)

    op==1 区间加

    [

    L

    ,

    R

    ]

    +

    k

    [L,R]+k

    [L,R]+k

    区间批量增加k

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)

    两次add1操作
    op==2 查询

    [

    L

    ,

    R

    ]

    [L,R]

    [L,R]总和

    区间求和

    O

    (

    n

    log

    n

    )

    O(n\\log n)

    O(nlogn)

    get_sum®、get_sum(L-1)各

    O

    (

    n

    log

    n

    )

    O(n\\log n)

    O(nlogn)

    整体程序总时间复杂度 完整程序

    O

    (

    n

    log

    n

    +

    m

    n

    log

    n

    )

    O(n\\log n + m\\cdot n\\log n)

    O(nlogn+mnlogn)

    m次查询操作会造成极高时间开销

    所以可想而知,我们这个代码的时间复杂度不是我们想要的O(logn),且它过不了这个题的,如果提交就是如下结果: 运行结果 很显然是超时了的,时间复杂度太大了。

    所以很显然我们要对它进行优化:

    解法二.双树状数组模拟解法

    在使用这个解法前,我们先思考一下:对于树状数组来说,要解这个题,还是前缀和和差分的结合,但我们要优化,推导过程如下:

    一、基础定义

    设原数组

    a

    [

    1

    n

    ]

    a[1\\dots n]

    a[1n],规定

    a

    0

    =

    0

    a_0=0

    a0=0 差分数组

    b

    [

    1

    n

    ]

    b[1\\dots n]

    b[1n]

    b

    i

    =

    a

    i

    a

    i

    1

    b_i = a_i – a_{i-1}

    bi=aiai1

    c

    [

    k

    ]

    c[k]

    c[k] 为差分数组

    b

    b

    b 的前缀和:

    c

    [

    k

    ]

    =

    i

    =

    1

    k

    b

    i

    =

    b

    1

    +

    b

    2

    +

    +

    b

    k

    c[k] = \\sum_{i=1}^k b_i = b_1 + b_2 + \\dots + b_k

    c[k]=i=1kbi=b1+b2++bk

    二、步骤1:证明差分数组前缀和

    c

    [

    k

    ]

    =

    a

    k

    c[k] = a_k

    c[k]=ak

    b

    i

    =

    a

    i

    a

    i

    1

    b_i = a_i – a_{i-1}

    bi=aiai1 代入展开:

    c

    [

    k

    ]

    =

    b

    1

    +

    b

    2

    +

    b

    3

    +

    +

    b

    k

    =

    (

    a

    1

    a

    0

    )

    +

    (

    a

    2

    a

    1

    )

    +

    (

    a

    3

    a

    2

    )

    +

    +

    (

    a

    k

    a

    k

    1

    )

    \\begin{aligned} c[k] &= b_1 + b_2 + b_3 + \\dots + b_k \\\\ &= (a_1-a_0) + (a_2-a_1) + (a_3-a_2) + \\dots + (a_k – a_{k-1}) \\end{aligned}

    c[k]=b1+b2+b3++bk=(a1a0)+(a2a1)+(a3a2)++(akak1) 裂项全部抵消,

    a

    0

    =

    0

    a_0=0

    a0=0

    c

    [

    k

    ]

    =

    a

    k

    c[k] = a_k

    c[k]=ak 等价关系:

    i

    =

    1

    k

    b

    i

    =

    a

    k

    \\sum_{i=1}^k b_i = a_k

    i=1kbi=ak

    三、步骤2:推导原数组前

    k

    k

    k 项总和

    S

    (

    k

    )

    =

    a

    1

    +

    a

    2

    +

    +

    a

    k

    S(k) = a_1+a_2+\\dots+a_k

    S(k)=a1+a2++ak

    a

    i

    =

    t

    =

    1

    i

    b

    t

    a_i = \\sum_{t=1}^i b_t

    ai=t=1ibt 代入总和公式:

    S

    (

    k

    )

    =

    i

    =

    1

    k

    a

    i

    =

    i

    =

    1

    k

    (

    t

    =

    1

    i

    b

    t

    )

    S(k) = \\sum_{i=1}^k a_i = \\sum_{i=1}^k \\left( \\sum_{t=1}^i b_t \\right)

    S(k)=i=1kai=i=1k(t=1ibt)

    交换二重求和顺序

    b

    t

    b_t

    bt

    i

    =

    t

    ,

    i

    =

    t

    +

    1

    ,

    ,

    i

    =

    k

    i=t,i=t+1,\\dots,i=k

    i=t,i=t+1,,i=k 中一共出现

    k

    t

    +

    1

    k-t+1

    kt+1 次:

    S

    (

    k

    )

    =

    t

    =

    1

    k

    b

    t

    (

    k

    t

    +

    1

    )

    =

    t

    =

    1

    k

    b

    t

    [

    k

    (

    t

    1

    )

    ]

    =

    t

    =

    1

    k

    [

    k

    b

    t

    (

    t

    1

    )

    b

    t

    ]

    \\begin{aligned} S(k) &= \\sum_{t=1}^k b_t \\cdot (k – t + 1) \\\\ &= \\sum_{t=1}^k b_t \\cdot \\big[ k – (t-1) \\big] \\\\ &= \\sum_{t=1}^k \\Big[ k\\cdot b_t – (t-1)\\cdot b_t \\Big] \\end{aligned}

    S(k)=t=1kbt(kt+1)=t=1kbt[k(t1)]=t=1k[kbt(t1)bt]

    拆分求和项

    S

    (

    k

    )

    =

    k

    t

    =

    1

    k

    b

    t

      


      

    t

    =

    1

    k

    (

    t

    1

    )

    b

    t

    S(k) = k\\cdot \\sum_{t=1}^k b_t \\;-\\; \\sum_{t=1}^k (t-1)\\cdot b_t

    S(k)=kt=1kbtt=1k(t1)bt

    四、最终结论

    原数组前

    k

    k

    k 项累加和公式:

    a

    1

    +

    a

    2

    +

    +

    a

    k

    =

    k

    i

    =

    1

    k

    b

    i

      


      

    i

    =

    1

    k

    (

    i

    1

    )

    b

    i

    \\boldsymbol{a_1+a_2+\\dots+a_k = k \\cdot \\sum_{i=1}^k b_i \\;-\\; \\sum_{i=1}^k (i-1)\\cdot b_i}

    a1+a2++ak=ki=1kbii=1k(i1)bi

  • i

    =

    1

    k

    b

    i

    \\displaystyle\\sum_{i=1}^k b_i

    i=1kbi:Tree1 维护,存储差分数组

    b

    i

    b_i

    bi

  • i

    =

    1

    k

    (

    i

    1

    )

    b

    i

    \\displaystyle\\sum_{i=1}^k (i-1)b_i

    i=1k(i1)bi:Tree2 维护,存储

    (

    i

    1

    )

    b

    i

    (i-1)\\cdot b_i

    (i1)bi

  • 所以基于以上推导过程,我们要添加几个方法: 以下为左神上课原图(上面的sum方法其实就是模版里的query方法): 左神上课原图 通俗一点来说我们要求l~r范围内的累加和就是先去维护一个方法query(求1 ~k的累加和),l ~r的累加和就是query(1-r)-query(1-(l-1)),,就是图中的range方法,而要区间增值,其实也是和差分是一样的,照着公式来说:

    a

    1

    +

    a

    2

    +

    +

    a

    k

    =

    k

    i

    =

    1

    k

    b

    i

      


      

    i

    =

    1

    k

    (

    i

    1

    )

    b

    i

    \\boldsymbol{a_1+a_2+\\dots+a_k = k \\cdot \\sum_{i=1}^k b_i \\;-\\; \\sum_{i=1}^k (i-1)\\cdot b_i}

    a1+a2++ak=ki=1kbii=1k(i1)bi

  • i

    =

    1

    k

    b

    i

    \\displaystyle\\sum_{i=1}^k b_i

    i=1kbi:Tree1 维护,存储差分数组

    b

    i

    b_i

    bi

  • i

    =

    1

    k

    (

    i

    1

    )

    b

    i

    \\displaystyle\\sum_{i=1}^k (i-1)b_i

    i=1k(i1)bi:Tree2 维护,存储

    (

    i

    1

    )

    b

    i

    (i-1)\\cdot b_i

    (i1)bi

  • 差分我修改的话就要分两个部分来修改(就和上图一样),正好对应了我们设出的两个树状数组,所以代码如下: java代码:

    import java.io.*;
    import java.util.*;
    public class Main{
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static PrintWriter out = new PrintWriter(System.out);
    static StringTokenizer st;
    final static long MAX=1000010;
    // Tree1:维护差分数组b[i]的前缀和 Σb[i]
    static long []Tree1;
    // Tree2:维护 (i-1)*b[i] 的前缀和 Σ(i-1)*b[i]
    static long []Tree2;
    static int n,m;
    public static int lowbit(int x){
    return x&(x);
    }
    // 单点更新两棵树:差分数组x位置增加v
    public static void update(int x,long v){
    // 循环向上更新所有包含x的区间节点
    for(int i=x;i<MAX;i+=lowbit(i)){
    Tree1[i]+=v;
    // Tree2对应存入 (x-1)*v,和前面的推导公式相同
    Tree2[i]+=(x1)*v;
    }
    }
    // 区间 [l,r] 整体加上数值v,进行一个差分的操作
    public static void RangeAdd(int l,int r,long v){
    update(l,v); // l位置增量v,l之后全部+ v
    update(r+1,v); // r+1位置减v,这样就可以抵消增量,而且仅l~r生效
    }
    // 查询原数组前x项总和,套用前面推出来的公式 S(x) = x*Σb[i] – Σ(i-1)*b[i]
    public static long getSum(int x){
    long res1=0; // 存储Tree1前缀和 Σb[i]
    long res2=0; // 存储Tree2前缀和 Σ(i-1)*b[i]
    // 从x往左拆分区间累加
    for(int i=x;i>0;i-=lowbit(i)){
    res1+=Tree1[i];
    res2+=Tree2[i];
    }
    return res1*xres2;
    }
    // 查询任意区间 [l,r] 的累加和
    // 区间和 = 前r项总和 – 前l-1项总和
    public static long RangeQuery(int l,int r){
    return getSum(r)getSum(l1);
    }
    public static void main(String[] args) throws IOException{
    st=new StringTokenizer(br.readLine());
    n=Integer.parseInt(st.nextToken());
    m=Integer.parseInt(st.nextToken());
    //Math.toIntExact方法是用于将Long转为int,其实也可以(int)来强转的
    Tree1=new long[Math.toIntExact(MAX)];
    Tree2=new long[Math.toIntExact(MAX)];
    st=new StringTokenizer(br.readLine());
    for(int i=1;i<=n;i++){
    long num=Long.parseLong(st.nextToken());
    // 单点赋值就是相当于在区间[i,i]都增加num
    RangeAdd(i,i,num);
    }
    while(m > 0){
    st=new StringTokenizer(br.readLine());
    int op=Integer.parseInt(st.nextToken());
    if(op==1){
    int l=Integer.parseInt(st.nextToken());
    int r=Integer.parseInt(st.nextToken());
    long k=Long.parseLong(st.nextToken());
    RangeAdd(l,r,k);
    }else{
    int l=Integer.parseInt(st.nextToken());
    int r=Integer.parseInt(st.nextToken());
    out.println(RangeQuery(l,r));
    }
    }
    out.flush();
    out.close();
    br.close();
    }
    }

    C++代码:

    //和Java的逻辑相同
    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const ll MAX=1000010;
    ll Tree1[MAX];
    ll Tree2[MAX];
    static int n,m,op;
    static int lowbit(int x) {
    return x&(x);
    }
    static void update(int x,ll v) {
    for (int i = x; i <= n; i+=lowbit(i)) {
    Tree1[i]+=v;
    Tree2[i]+=v*(x1);
    }
    }
    static void RangeAdd(int l,int r,ll v) {
    update(l,v);
    update(r+1,v);
    }
    static ll getSum(int x) {
    ll res1=0;
    ll res2=0;
    for (int i = x; i > 0; i-=lowbit(i)) {
    res1+=Tree1[i];
    res2+=Tree2[i];
    }
    return res1*xres2;
    }
    static ll RangeQuery(int l,int r) {
    return getSum(r)getSum(l1);
    }
    int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    cin>>n>>m;
    for (int i = 1; i <= n; i++) {
    ll num;
    cin>>num;
    RangeAdd(i,i,num);
    }
    while (m) {
    cin>>op;
    if (op==1) {
    int l,r;
    ll k;
    cin>>l>>r>>k;
    RangeAdd(l,r,k);
    }else {
    int l,r;
    cin>>l>>r;
    cout<<RangeQuery(l,r)<<"\\n";
    }
    }
    return 0;
    }

    所以对于我们双树状数组的解法来说它的时间复杂度为:

    代码模块功能说明单次时间复杂度
    lowbit(int x) 取二进制最低位1

    O

    (

    1

    )

    O(1)

    O(1)

    update(int x, long v) 单点更新两棵树状数组

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)

    RangeAdd(int l, int r, long v) 区间

    [

    l

    ,

    r

    ]

    [l,r]

    [l,r]整体加值

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)

    getSum(int x) 求原数组前

    x

    x

    x项总和

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)

    RangeQuery(int l, int r) 查询区间

    [

    l

    ,

    r

    ]

    [l,r]

    [l,r]累加和

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)

    数组初始化循环 读入n个数字构建树

    O

    (

    n

    log

    n

    )

    O(n\\log n)

    O(nlogn)

    m次操作主循环 处理m次修改/查询

    O

    (

    m

    log

    n

    )

    O(m\\log n)

    O(mlogn)

    程序整体总复杂度 完整程序运行

    O

    (

    (

    n

    +

    m

    )

    log

    n

    )

    O((n+m)\\log n)

    O((n+m)logn)

    对比这个和上面那个写法来说,它的时间就大大缩小了。

    解法三.线段树经典解法

    在这里不仔细说明线段树的原理了,其实线段树就是一种二叉树,对于树状数组在解题时遇到不可查分信息时,树状数组往往思路复杂,这个时候线段树就挺身而出了,它的原理是基于分治递归来的,就是说我维护一个二叉树,对于叶子节点就存单个元素,而非叶子节点就存这段区间的总和以及最值等信息,而子节点数值更新后,和大多数二叉树一样,父节点的值 = 左子节点 + 右子节点,在区间修改的时候,我们就会用到懒标记,其实就是能不往下更新就先不更新,延迟下放的一个操作,所以代码如下: java代码如下:

    //线段树代码的实现
    import java.io.*;
    import java.util.*;
    public class Main{
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static PrintWriter out = new PrintWriter(System.out);
    static StringTokenizer st;
    final static int MAX=1000010;
    // 表示线段树数组:存储每个区间的总和,开4倍空间防止越界
    static long[] tree;
    // 表示懒标记数组:保存待下传的区间增量,延迟更新子节点
    static long[] lazy;
    static int n,m;
    public static void pushUp(int node){//这个方法其实是用于向上合并节点的
    tree[node] = tree[node*2] + tree[node*2+1];
    }
    public static void pushDown(int node,int l,int r){
    // 没有待下放增量的操作的话,就直接返回无需操作
    if(lazy[node]==0) return;
    int mid = (l+r)/2;
    int left = node*2; // 左孩子节点编号
    int rightNode = node*2+1; // 右孩子节点编号
    // 更新左子树总和与懒标记
    tree[left] += lazy[node] * (mid l + 1);
    lazy[left] += lazy[node];
    // 更新右子树总和与懒标记
    tree[rightNode] += lazy[node] * (r mid);
    lazy[rightNode] += lazy[node];
    // 当前节点标记已下放,清空懒标记
    lazy[node] = 0;
    }
    public static void build(int node,int l,int r,long[] a){
    // 叶子节点,对应原数组单个元素,直接赋值
    if(l == r){
    tree[node] = a[l];
    return;
    }
    int mid = (l+r)/2;
    // 递归构造左子树 [l,mid]
    build(node*2, l, mid, a);
    // 递归构造右子树 [mid+1,r]
    build(node*2+1, mid+1, r, a);
    // 左右子树构造完成,更新当前节点区间和
    pushUp(node);
    }
    public static void RangeAdd(int node,int l,int r,int L,int R,long v){
    // 当前区间完全被修改区间包含,直接打懒标记返回
    if(L<=l && r<=R){
    tree[node] += v * (r l + 1);
    lazy[node] += v;
    return;
    }
    // 不完全包含,先下放旧标记再递归子节点
    pushDown(node, l, r);
    int mid = (l+r)/2;
    // 左区间有重叠,递归修改左子节点
    if(L <= mid) RangeAdd(node*2, l, mid, L, R, v);
    // 右区间有重叠,递归修改右子节点
    if(R > mid) RangeAdd(node*2+1, mid+1, r, L, R, v);
    // 子节点修改完成,更新当前节点总和
    pushUp(node);
    }
    public static long RangeQuery(int node,int l,int r,int L,int R){
    // 当前区间完全在查询范围内,直接返回区间和
    if(L<=l && r<=R){
    return tree[node];
    }
    // 访问子节点前下放懒标记,保证数据正确
    pushDown(node, l, r);
    int mid = (l+r)/2;
    long res = 0;
    // 左区间存在查询范围,累加左子树结果
    if(L <= mid) res += RangeQuery(node*2, l, mid, L, R);
    // 右区间存在查询范围,累加右子树结果
    if(R > mid) res += RangeQuery(node*2+1, mid+1, r, L, R);
    return res;
    }
    public static void main(String[] args) throws IOException{
    st=new StringTokenizer(br.readLine());
    n=Integer.parseInt(st.nextToken());
    m=Integer.parseInt(st.nextToken());
    // 初始化线段树与懒标记数组,4倍空间
    tree = new long[MAX * 4];
    lazy = new long[MAX * 4];
    // 存储原始输入数组,下标从1开始
    long[] a = new long[n+1];
    st=new StringTokenizer(br.readLine());
    for(int i=1;i<=n;i++){
    a[i]=Long.parseLong(st.nextToken());
    }
    // 根节点编号1,构建1~n的线段树
    build(1, 1, n, a);
    while(m > 0){
    st=new StringTokenizer(br.readLine());
    int op=Integer.parseInt(st.nextToken());
    if(op==1){
    int l=Integer.parseInt(st.nextToken());
    int r=Integer.parseInt(st.nextToken());
    long k=Long.parseLong(st.nextToken());
    RangeAdd(1,1,n,l,r,k);
    }else{
    int l=Integer.parseInt(st.nextToken());
    int r=Integer.parseInt(st.nextToken());
    out.println(RangeQuery(1,1,n,l,r));
    }
    }
    out.flush();
    out.close();
    br.close();
    }
    }

    C++代码如下:

    //逻辑和Java的相同
    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int MAX = 1000010;
    ll tree[MAX * 4];
    ll lazy[MAX * 4];
    int n, m;
    void pushUp(int node) {
    tree[node] = tree[node * 2] + tree[node * 2 + 1];
    }
    void pushDown(int node, int l, int r) {
    if (lazy[node] == 0) return;
    int mid = (l + r) / 2;
    int left = node * 2;
    int rightNode = node * 2 + 1;
    tree[left] += lazy[node] * (mid l + 1);
    lazy[left] += lazy[node];
    tree[rightNode] += lazy[node] * (r mid);
    lazy[rightNode] += lazy[node];
    lazy[node] = 0;
    }
    void build(int node, int l, int r, ll a[]) {
    if (l == r) {
    tree[node] = a[l];
    return;
    }
    int mid = (l + r) / 2;
    build(node * 2, l, mid, a);
    build(node * 2 + 1, mid + 1, r, a);
    pushUp(node);
    }
    void RangeAdd(int node, int l, int r, int L, int R, ll v) {
    if (L <= l && r <= R) {
    tree[node] += v * (r l + 1);
    lazy[node] += v;
    return;
    }
    pushDown(node, l, r);
    int mid = (l + r) / 2;
    if (L <= mid) RangeAdd(node * 2, l, mid, L, R, v);
    if (R > mid) RangeAdd(node * 2 + 1, mid + 1, r, L, R, v);
    pushUp(node);
    }
    ll RangeQuery(int node, int l, int r, int L, int R) {
    if (L <= l && r <= R) {
    return tree[node];
    }
    pushDown(node, l, r);
    int mid = (l + r) / 2;
    ll res = 0;
    if (L <= mid) res += RangeQuery(node * 2, l, mid, L, R);
    if (R > mid) res += RangeQuery(node * 2 + 1, mid + 1, r, L, R);
    return res;
    }
    int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    cin >> n >> m;
    ll *a = new ll[n + 1];
    for (int i = 1; i <= n; i++) {
    cin >> a[i];
    }
    build(1, 1, n, a);
    delete[] a;
    while (m > 0) {
    int op;
    cin >> op;
    if (op == 1) {
    int l, r;
    ll k;
    cin >> l >> r >> k;
    RangeAdd(1, 1, n, l, r, k);
    } else {
    int l, r;
    cin >> l >> r;
    cout << RangeQuery(1, 1, n, l, r) << '\\n';
    }
    }
    return 0;
    }

    所以从这里来看,线段树的代码量更为巨大,而它的时间复杂度为:

    模块功能单次复杂度
    pushUp 合并左右子节点和

    O

    (

    1

    )

    O(1)

    O(1)

    pushDown 下放懒标记

    O

    (

    1

    )

    O(1)

    O(1)

    build 线段树建树

    O

    (

    n

    )

    O(n)

    O(n)

    RangeAdd 区间[l,r]加k

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)

    RangeQuery 查询[l,r]区间和

    O

    (

    log

    n

    )

    O(\\log n)

    O(logn)

    m次操作循环 m次修改/查询

    O

    (

    m

    log

    n

    )

    O(m\\log n)

    O(mlogn)

    整体总复杂度 完整程序

    O

    (

    n

    +

    m

    log

    n

    )

    O(n + m\\log n)

    O(n+mlogn)

    因此和树状数组的时间复杂度大致相同,但它们有啥区别呢,为啥说树状数组不适合用于解不可差分信息呢?其实很好理解,树状数组其实本质上是和前缀和,差分有一样的思想,就是说: 树状数组的所有操作,建立在两条特性: 修改是靠单点增量; 查询是前缀合并**,且合并运算满足: f(1∼x)=f(1∼mid)+f(mid+1∼x) 加法、求和满足这个,属于可差分信息: 原数组区间和可以拆差分,区间修改也能用差分两点更新实现。 因此像区间最大值、区间最小、区间 GCD这些不可差分信息,他就无法简单的实现了。

    3.二进制区间取反与单点求值

    引用:牛客PEEK75 区间取反与单点求值

    牛客PEEK75 题目链接如下: 牛客PEEK区间取反和单点求值

    我一开始看到这个题目还以为是区间正数变为负数,负数变为正数,看错了题目,其实它是给我们一个长度为n的字符串(只包括0和1),然后要我们去查询那个位置是0还是1,翻转的话就是把范围l~r范围内的字符由0改为1,由1改为0,那翻转其实就是对它进行一个异或操作就行了,在这里也还是要用到差分的思想,就对原区间进行异或,就对差分后的两头分别异或就行了,所以代码如下: java代码如下:

    import java.io.*;
    import java.util.*;
    public class Main{
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static PrintWriter out = new PrintWriter(System.out);
    static StringTokenizer st;
    static int n,m;
    static int []Tree;
    static int []a;
    public static int lowbit(int x){//和树状数组一样是取最右位的1
    return x&(x);
    }
    public static void add(int x,int v) {//这里是一个异或的思想
    for(int i=x;i<=n;i+=lowbit(i)){//和树状树组模板一样,去找它管的下标
    Tree[i]^=v;//不断异或就能实现它的翻转
    }
    }
    public static int query(int x){//这里则为向下遍历,累加前缀所有翻转标记,就是一个异或叠加的过程
    int res=0;
    for(int i=x;i>0;i-=lowbit(i)){
    res^=Tree[i];
    }
    return res;
    }
    public static void main(String[] args) throws Exception {
    st=new StringTokenizer(br.readLine());
    n=Integer.parseInt(st.nextToken());
    m=Integer.parseInt(st.nextToken());
    String s=br.readLine().trim();
    a=new int[n+1];
    Tree=new int[n+1];
    // 将下标1~n的输入的01字符串存入数组a
    for(int i=1;i<=n;i++){
    a[i]=s.charAt(i1)'0';
    }
    while(m > 0){
    st=new StringTokenizer(br.readLine());
    int op=Integer.parseInt(st.nextToken());
    if(op==1){
    int l=Integer.parseInt(st.nextToken());
    int r=Integer.parseInt(st.nextToken());
    //类似于差分的异或差分:l位置打翻转标记,r+1位置抵消后面的翻转就可以了
    add(l,1);
    add(r+1,1);
    }else{
    int x=Integer.parseInt(st.nextToken());
    //原始数字 ^ 总翻转次数,就可以得到翻转后的结果了
    out.println(a[x]^query(x));
    }
    }
    out.flush();
    out.close();
    br.close();
    }
    }

    C++代码如下:

    //和Java代码的逻辑相同
    #include <bits/stdc++.h>
    using namespace std;
    int n;
    vector<int> Tree;
    vector<int> a;
    static int lowbit(int x) {
    return x & x;
    }
    static void add(int x, int k) {
    for (int i = x; i <= n; i += lowbit(i)) {
    Tree[i] ^= k;
    }
    }
    static int query(int x) {
    int res = 0;
    for (int i = x; i > 0; i -= lowbit(i)) {
    res ^= Tree[i];
    }
    return res;
    }
    int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int m;
    cin >> n >> m;
    string s;
    cin >> s;
    a.resize(n + 1);
    Tree.resize(n + 1, 0);
    for (int i = 0; i < n; ++i) {
    a[i + 1] = s[i] '0';
    }
    while (m) {
    int op;
    cin >> op;
    if (op == 1) {
    int l, r;
    cin >> l >> r;
    add(l, 1);
    add(r + 1, 1);
    } else {
    int x;
    cin >> x;
    cout << (a[x] ^ query(x)) << "\\n";
    }
    }
    return 0;
    }

    思考

    那思考:如果是给一个数组,要对区间的数字进行取反(即正数变为负数,负数变为正数)和单点查询,又该如何写呢? 其实这样就是一个树状数组乘法的模板了,还是一样的差分思想,就是我对区间l~r要实现数组符号改变就是差分形式下把r+1和l都乘个-1,在单点查询的时候则是去看他的符号就可以了,因为这个时候我们的query方法并不是要记录累加和,而是要进行一个乘法的操作,即原值 × 累积正负标记就可以了,所以代码如下: java代码如下:

    import java.io.*;
    import java.util.*;
    public class Main{
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static PrintWriter out = new PrintWriter(System.out);
    static StringTokenizer st;
    static int n,m;
    static int[] Tree; // 记录树状数组,存乘法标记,初始全1
    static int[] a; // 记录原始数组,存正负整数
    static int lowbit(int x){
    return x & x;
    }
    // 单点更新:x位置所有父节点 *= v(v固定为-1)
    static void add(int x, int v){
    for(int i = x; i <= n; i += lowbit(i)){
    Tree[i] *= v;
    }
    }
    // 查询1~x标记的累积乘积,返回1 / -1
    static int query(int x){
    int res = 1;
    for(int i = x; i > 0; i -= lowbit(i)){
    res *= Tree[i];
    }
    return res;
    }
    public static void main(String[] args) throws Exception{
    st = new StringTokenizer(br.readLine());
    n = Integer.parseInt(st.nextToken());
    m = Integer.parseInt(st.nextToken());
    a = new int[n + 1];
    Tree = new int[n + 1];
    // 乘法树状数组初始值全部为1
    Arrays.fill(Tree, 1);
    // 读入原始数组
    st = new StringTokenizer(br.readLine());
    for(int i = 1; i <= n; i++){
    a[i] = Integer.parseInt(st.nextToken());
    }
    while(m > 0){
    st = new StringTokenizer(br.readLine());
    int op = Integer.parseInt(st.nextToken());
    if(op == 1){
    // 进行操作1:区间[l,r]全部正负取反 *= -1
    int l = Integer.parseInt(st.nextToken());
    int r = Integer.parseInt(st.nextToken());
    add(l, 1);
    if(r + 1 <= n) add(r + 1, 1);
    }else{
    //进行操作2:单点查询x当前值
    int x = Integer.parseInt(st.nextToken());
    // 原值 × 累积正负标记
    out.println(a[x] * query(x));
    }
    }
    out.flush();
    out.close();
    br.close();
    }
    }

    C++代码如下:

    //和Java代码逻辑相同
    #include <bits/stdc++.h>
    using namespace std;
    const int MAXN = 1000010;
    int n, m;
    int Tree[MAXN]; // 乘法树状数组,存符号标记,初始全1
    int a[MAXN]; // 原始数组
    // lowbit 取出二进制最低位的1
    int lowbit(int x) {
    return x & x;
    }
    // 单点更新:x位置所有管辖节点乘v,本题v固定为-1
    void add(int x, int v) {
    for (int i = x; i <= n; i += lowbit(i)) {
    Tree[i] *= v;
    }
    }
    // 查询1~x前缀标记累乘,返回总符号 1 / -1
    int query(int x) {
    int res = 1; // 乘法单位元是1
    for (int i = x; i > 0; i -= lowbit(i)) {
    res *= Tree[i];
    }
    return res;
    }
    int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    // 树状数组初始全部置1
    fill(Tree, Tree + MAXN, 1);
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
    cin >> a[i];
    }
    while (m) {
    int op;
    cin >> op;
    if (op == 1) {
    // 区间[l,r]全部数字正负取反(* -1)
    int l, r;
    cin >> l >> r;
    add(l, 1);
    add(r + 1, 1);
    } else {
    // 单点查询x当前数值
    int x;
    cin >> x;
    cout << a[x] * query(x) << '\\n';
    }
    }
    return 0;
    }

    4.二维单点增加和范围查询

    二维单点增加和范围查询,顾名思义,其实就是对一个二维矩阵单点进行加上一个值,和查询子矩阵的和,其实就是二维前缀和的思想了,所以在看解答之前,我们先看看二维前缀和,先看代码:

    import java.io.*;
    import java.util.*;
    public class Main {
    static int[][] matrix;
    static long[][] sum;
    public static void NumMatrix() {
    int n = matrix.length 1;
    int m = matrix[0].length 1;
    // 初始化前缀和数组,开大一点防止越界
    sum = new long[n + 2][m + 2];
    for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
    // 二维前缀和核心公式
    sum[i][j] = sum[i 1][j] // 上方区域和
    + sum[i][j 1] // 左方区域和
    sum[i 1][j 1] // 重复区域和(减重复)
    + matrix[i][j]; // 当前位置的值
    }
    }
    }
    public static long getSum(int a, int b, int c, int d) {
    // 子矩阵和公式(容斥原理)
    return sum[c][d] // 大矩形总和
    sum[a 1][d] // 减去上部分
    sum[c][b 1] // 减去左部分
    + sum[a 1][b 1]; // 加回多减的重复部分
    }

    public static void main(String[] args) throws IOException {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    PrintWriter out = new PrintWriter(System.out);
    StringTokenizer st;
    st = new StringTokenizer(br.readLine());
    int n = Integer.parseInt(st.nextToken());
    int m = Integer.parseInt(st.nextToken());
    int q = Integer.parseInt(st.nextToken());
    matrix = new int[n + 2][m + 2];
    for (int i = 1; i <= n; i++) {
    st = new StringTokenizer(br.readLine());
    for (int j = 1; j <= m; j++) {
    matrix[i][j] = Integer.parseInt(st.nextToken());
    }
    }
    // 构建前缀和数组
    NumMatrix();
    // 处理 q 次查询
    while (q > 0) {
    st = new StringTokenizer(br.readLine());
    // 读取查询的子矩阵坐标
    int x1 = Integer.parseInt(st.nextToken());
    int y1 = Integer.parseInt(st.nextToken());
    int x2 = Integer.parseInt(st.nextToken());
    int y2 = Integer.parseInt(st.nextToken());
    // 查询并输出结果
    out.println(getSum(x1, y1, x2, y2));
    }
    out.flush();
    br.close();
    out.close();
    }
    }

    所以在这里我们基于以上的思想我们一样可以进行,而范围查询其实就是二维下的query范围相减即可,所以代码如下: java代码如下:

    import java.io.*;
    import java.util.*;
    public class Main {
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static PrintWriter out = new PrintWriter(System.out);
    static StringTokenizer st;
    static int n, m;
    final static int N = 2010;
    static int[][] Tree = new int[N][N];
    static int[][] matrix = new int[N][N];
    public static int lowbit(int x) {
    return x & (x);
    }
    // 单点修改:在 (x, y) 位置加上 v
    public static void update(int x, int y, int v) {
    for (int i = x; i <= n; i += lowbit(i)) {
    for (int j = y; j <= m; j += lowbit(j)) {
    Tree[i][j] += v;
    }
    }
    }
    // 查询二维前缀和:从 (1, 1) 到 (x, y) 的和
    public static int query(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 += Tree[i][j];
    }
    }
    return res;
    }
    // 利用二维前缀和的容斥原理,求子矩阵的和
    public static long getSum(int x1, int y1, int x2, int y2) {
    return (long) (query(x2, y2) query(x1 1, y2) query(x2, y1 1) + query(x1 1, y1 1));
    }
    public static void main(String[] args) throws Exception {
    st = new StringTokenizer(br.readLine());
    n = Integer.parseInt(st.nextToken());
    m = Integer.parseInt(st.nextToken());
    // 1. 读入初始矩阵,并构建二维树状数组
    for (int i = 1; i <= n; i++) {
    st = new StringTokenizer(br.readLine());
    for (int j = 1; j <= m; j++) {
    matrix[i][j] = Integer.parseInt(st.nextToken());
    // 将原矩阵的值作为增量,加入到树状数组中
    update(i, j, matrix[i][j]);
    }
    }
    int q = Integer.parseInt(br.readLine().trim());
    while (q > 0) {
    st = new StringTokenizer(br.readLine());
    int op = Integer.parseInt(st.nextToken());
    if (op == 1) {
    //单点修改,将 (x, y) 的值加上 v
    int x = Integer.parseInt(st.nextToken());
    int y = Integer.parseInt(st.nextToken());
    int v = Integer.parseInt(st.nextToken());
    update(x, y, v);
    matrix[x][y] += v; // 需要同步更新原矩阵
    } else {
    //查询左上角 (x1, y1) 到 右下角 (x2, y2) 的子矩阵和
    int x1 = Integer.parseInt(st.nextToken());
    int y1 = Integer.parseInt(st.nextToken());
    int x2 = Integer.parseInt(st.nextToken());
    int y2 = Integer.parseInt(st.nextToken());
    out.println(getSum(x1, y1, x2, y2));//这样就可以得到我们想要的矩阵和了
    }
    }
    out.flush();
    out.close();
    br.close();
    }
    }

    C++代码如下:

    //逻辑和Java代码其实还是一样的
    #include <bits/stdc++.h>
    using namespace std;
    const int N = 2010;
    typedef long long ll;
    int n, m;
    int Tree[N][N];
    int matrix[N][N];
    static int lowbit(int x) {
    return x & (x);
    }
    static void update(int x, int y, int v) {
    for (int i = x; i <= n; i += lowbit(i)) {
    for (int j = y; j <= m; j += lowbit(j)) {
    Tree[i][j] += v;
    }
    }
    }
    static int query(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 += Tree[i][j];
    }
    }
    return res;
    }
    ll getSum(int x1, int y1, int x2, int y2) {
    return 1LL * query(x2, y2) query(x1 1, y2) query(x2, y1 1) + query(x1 1, y1 1);
    }
    int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
    for (int j = 1; j <= m; ++j) {
    cin >> matrix[i][j];
    update(i, j, matrix[i][j]);
    }
    }
    int q;
    cin >> q;
    while (q) {
    int op;
    cin >> op;
    if (op == 1) {
    int x, y, v;
    cin >> x >> y >> v;
    update(x, y, v);
    matrix[x][y] += v;
    } else {
    int x1, y1, x2, y2;
    cin >> x1 >> y1 >> x2 >> y2;
    cout << getSum(x1, y1, x2, y2) << '\\n';
    }
    }
    return 0;
    }

    5.二维范围增加和范围查询

    引用洛谷P4514

    洛谷模版题 题目链接如下: 洛谷P4514二维树状数组模版题

    其实对于一维的范围增值来说,我们使用了一维差分,所以其实很自然的就可以想到能不能用二维差分来解决这个题呢?其实我们可以验证一下,先看一段二维差分模板:

    #include <bits/stdc++.h>
    using namespace std;
    vector<vector<long>> matrix;
    vector<vector<long>> diff;
    static int n, m;
    // 根据原矩阵 matrix,初始化二维差分数组 diff
    // 对应一维:crr[i] = arr[i] – arr[i-1]
    void getDiff() {
    for (int i = 1; i <= n; i++) { // 遍历每一行
    for (int j = 1; j <= m; j++) { // 遍历每一列
    // 二维差分核心公式:当前值 – 左边 – 上边 + 左上角(抵消重复减)
    diff[i][j] = matrix[i][j] matrix[i][j1] matrix[i1][j] + matrix[i1][j1];
    }
    }
    }
    // 根据公式对矩形区域 (a,b) → (c,d) 内所有数统一加 k
    // 二维差分标准操作:只修改 4 个点,O(1) 完成区间修改
    void getNum(int a,int b,int c,int d,int k) {
    diff[a][b] += k; // 矩形左上角 +k
    diff[a][d+1] -= k; // 矩形右上角右边 -k
    diff[c+1][b] -= k; // 矩形左下角下边 -k
    diff[c+1][d+1] += k; // 矩形右下角右下角 +k(抵消前面的减法)
    }
    // 其实对差分数组 diff 求二维前缀和,还原成最终的答案矩阵
    // 对应一维:arr[i] = arr[i-1] + crr[i]
    void getSum() {
    for (int i = 1; i <= n; i++) { // 遍历每一行
    for (int j = 1; j <= m; j++) { // 遍历每一列
    // 二维前缀和公式:当前值 + 左边 + 上边 – 左上角(去重)
    diff[i][j] = diff[i][j] + diff[i][j1] + diff[i1][j] diff[i1][j1];
    }
    }
    }
    int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    cin >> n >> m;
    matrix.resize(n+1, vector<long>(m+1));
    diff.resize(n+2, vector<long>(m+2));
    int q;
    cin >> q;
    for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
    cin >> matrix[i][j];
    }
    }
    getDiff();
    while (q>0) {
    int x1, y1, x2, y2, k;
    cin >> x1 >> y1 >> x2 >> y2 >> k;
    getNum(x1, y1, x2, y2, k);
    }
    getSum();
    for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
    cout << diff[i][j] << " ";
    }
    cout << "\\n";
    }
    return 0;
    }

    其实基于暴力的思想,直接用二位差分来是可以解的,和前面那个一维的是一样的,要二维区间加一个值和查询其实就是二维差分来加值,对二维差分直接前缀和一次就是原矩阵,再前缀和一次就是我们要的子矩阵的和了,代码如下:

    //暴力解法
    import java.io.*;
    import java.util.*;
    public class Main {
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static PrintWriter out = new PrintWriter(System.out);
    static StringTokenizer st;
    static int n, m;
    static long[][] diff;//储存差分信息
    static long[][] a;//原矩阵
    static long[][] pre;//储存前缀和信息
    public static void main(String[] args) throws IOException {
    String line = br.readLine();
    while (line != null && line.trim().isEmpty()) {//读第一行
    line = br.readLine();
    }
    st = new StringTokenizer(line);
    st.nextToken();
    n = Integer.parseInt(st.nextToken());
    m = Integer.parseInt(st.nextToken());
    diff = new long[n + 2][m + 2];
    a = new long[n + 1][m + 1];
    pre = new long[n + 1][m + 1];
    String op;
    while ((op = br.readLine()) != null) {//按题目的要求,从输入数据的第二行开始到文件尾,只要有就一直输入。
    if (op.trim().isEmpty()) continue;
    st = new StringTokenizer(op);
    char type = st.nextToken().charAt(0);
    if (type == 'L') {//如果是L就进行一个二维差分的操作
    int a0 = Integer.parseInt(st.nextToken());
    int b0 = Integer.parseInt(st.nextToken());
    int c0 = Integer.parseInt(st.nextToken());
    int d0 = Integer.parseInt(st.nextToken());
    long delta = Long.parseLong(st.nextToken());
    int x1 = Math.min(a0, c0), x2 = Math.max(a0, c0);
    int y1 = Math.min(b0, d0), y2 = Math.max(b0, d0);
    diff[x1][y1] += delta;
    diff[x2 + 1][y1] -= delta;
    diff[x1][y2 + 1] -= delta;
    diff[x2 + 1][y2 + 1] += delta;
    }
    else if (type == 'k') {//如果是k就进行二维前缀和的操作
    int a0 = Integer.parseInt(st.nextToken());
    int b0 = Integer.parseInt(st.nextToken());
    int c0 = Integer.parseInt(st.nextToken());
    int d0 = Integer.parseInt(st.nextToken());
    int x1 = Math.min(a0, c0), x2 = Math.max(a0, c0);
    int y1 = Math.min(b0, d0), y2 = Math.max(b0, d0);
    for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
    a[i][j] = diff[i][j] + a[i1][j] + a[i][j1] a[i1][j1];
    pre[i][j] = pre[i1][j] + pre[i][j1] pre[i1][j1] + a[i][j];
    }
    }
    long ans = pre[x2][y2] pre[x11][y2] pre[x2][y11] + pre[x11][y11];
    out.println(ans);
    }
    }
    out.flush();
    out.close();
    br.close();
    }
    }

    显而易见,以上代码当 n = m = 2048 时,单次查询约 4×10⁶ 次运算;若查询次数接近 2×10⁵,总运算量将超过 8×10¹¹,就远远超出时间限制了,它的时间复杂度如下:

    分类代码模块单次操作时间复杂度变量与详细说明
    L矩形加操作 二维差分四点修改

    O

    (

    1

    )

    O(1)

    O(1)

    仅4次数组加减,无循环;自动处理坐标大小互换
    k查询操作内部步骤 遍历全矩阵还原原矩阵a、构建前缀和pre

    O

    (

    n

    m

    )

    O(nm)

    O(nm)

    双重循环i∈[1,n],j∈[1,m],每次查询完整重建整个矩阵与前缀和
    k查询操作内部步骤 容斥计算矩形答案

    O

    (

    1

    )

    O(1)

    O(1)

    仅四则运算
    单次k查询整体 重建矩阵+前缀和+区间求和

    O

    (

    n

    m

    )

    O(nm)

    O(nm)

    核心耗时为全矩阵双重循环
    全局总复杂度 整个程序所有操作

    O

    (

    Q

    L

    +

    Q

    k

    n

    m

    )

    O(Q_L + Q_k \\cdot nm)

    O(QL+Qknm)

    Q

    L

    Q_L

    QL:L操作总数;

    Q

    k

    Q_k

    Qk:k查询总数;瓶颈为每次查询都完整遍历n×m矩阵

    所以提交的结果就可想而知的超时了: 运行结果 所以我们我们要对原来的思路进行一次优化,推导过程如下:

    二维区间加、区间求和完整数学推导
    1. 基础定义

    设:

  • 原矩阵:

    a

    i

    ,

    j

    a_{i,j}

    ai,j

  • 二维差分数组:

    d

    i

    ,

    j

    d_{i,j}

    di,j

  • 二维差分性质:对矩形

    [

    a

    ,

    b

    ]

    [

    c

    ,

    d

    ]

    [a,b] \\sim [c,d]

    [a,b][c,d] 整体加

    v

    v

    v,仅需4个单点修改

    d

    d

    d

    {

    d

    a

    ,

    b

    d

    a

    ,

    b

    +

    v

    d

    a

    ,

    d

    +

    1

    d

    a

    ,

    d

    +

    1

    v

    d

    c

    +

    1

    ,

    b

    d

    c

    +

    1

    ,

    b

    v

    d

    c

    +

    1

    ,

    d

    +

    1

    d

    c

    +

    1

    ,

    d

    +

    1

    +

    v

    \\begin{cases} d_{a,b} \\gets d_{a,b} + v \\\\ d_{a,d+1} \\gets d_{a,d+1} – v \\\\ d_{c+1,b} \\gets d_{c+1,b} – v \\\\ d_{c+1,d+1} \\gets d_{c+1,d+1} + v \\end{cases}

    da,bda,b+vda,d+1da,d+1vdc+1,bdc+1,bvdc+1,d+1dc+1,d+1+v

    还原原矩阵:

    a

    i

    ,

    j

    a_{i,j}

    ai,j

    d

    d

    d 的二维前缀和

    a

    i

    ,

    j

    =

    p

    =

    1

    i

    q

    =

    1

    j

    d

    p

    ,

    q

    a_{i,j} = \\sum_{p=1}^i \\sum_{q=1}^j d_{p,q}

    ai,j=p=1iq=1jdp,q

    2. 定义全局前缀和

    S

    (

    x

    ,

    y

    )

    S(x,y)

    S(x,y)

    S

    (

    x

    ,

    y

    )

    S(x,y)

    S(x,y) 代表左上角

    (

    1

    ,

    1

    )

    (1,1)

    (1,1)

    (

    x

    ,

    y

    )

    (x,y)

    (x,y) 整个矩形所有数字之和:

    S

    (

    x

    ,

    y

    )

    =

    i

    =

    1

    x

    j

    =

    1

    y

    a

    i

    ,

    j

    S(x,y) = \\sum_{i=1}^x \\sum_{j=1}^y a_{i,j}

    S(x,y)=i=1xj=1yai,j

    a

    i

    ,

    j

    =

    p

    =

    1

    i

    q

    =

    1

    j

    d

    p

    ,

    q

    a_{i,j} = \\sum_{p=1}^i\\sum_{q=1}^j d_{p,q}

    ai,j=p=1iq=1jdp,q 代入:

    S

    (

    x

    ,

    y

    )

    =

    i

    =

    1

    x

    j

    =

    1

    y

    (

    p

    =

    1

    i

    q

    =

    1

    j

    d

    p

    ,

    q

    )

    S(x,y) = \\sum_{i=1}^x \\sum_{j=1}^y \\left( \\sum_{p=1}^i \\sum_{q=1}^j d_{p,q} \\right)

    S(x,y)=i=1xj=1y(p=1iq=1jdp,q)

    3. 交换求和顺序

    固定差分点

    (

    p

    ,

    q

    )

    (p,q)

    (p,q),统计

    d

    p

    ,

    q

    d_{p,q}

    dp,q 会被累加多少次:

    i

    i

    i 取值范围

    [

    p

    ,

     

    x

    ]

    [p,\\ x]

    [p, x]

    j

    j

    j 取值范围

    [

    q

    ,

     

    y

    ]

    [q,\\ y]

    [q, y] 总累加次数 = 行数 × 列数 =

    (

    x

    p

    +

    1

    )

    (

    y

    q

    +

    1

    )

    (x-p+1)(y-q+1)

    (xp+1)(yq+1)

    因此改写求和式:

    S

    (

    x

    ,

    y

    )

    =

    p

    =

    1

    x

    q

    =

    1

    y

    d

    p

    ,

    q

    (

    x

    p

    +

    1

    )

    (

    y

    q

    +

    1

    )

    S(x,y) = \\sum_{p=1}^x \\sum_{q=1}^y d_{p,q} \\cdot (x-p+1)(y-q+1)

    S(x,y)=p=1xq=1ydp,q(xp+1)(yq+1)

    4. 展开多项式

    (

    x

    p

    +

    1

    )

    (

    y

    q

    +

    1

    )

    (x-p+1)(y-q+1)

    (xp+1)(yq+1)

    (

    x

    p

    +

    1

    )

    (

    y

    q

    +

    1

    )

    =

    [

    (

    x

    +

    1

    )

    p

    ]

    [

    (

    y

    +

    1

    )

    q

    ]

    =

    (

    x

    +

    1

    )

    (

    y

    +

    1

    )

    (

    x

    +

    1

    )

    q

    (

    y

    +

    1

    )

    p

    +

    p

    q

    \\begin{align*} (x-p+1)(y-q+1) &= \\big[(x+1)-p\\big] \\cdot \\big[(y+1)-q\\big] \\\\ &= (x+1)(y+1) – (x+1)q – (y+1)p + pq \\end{align*}

    (xp+1)(yq+1)=[(x+1)p][(y+1)q]=(x+1)(y+1)(x+1)q(y+1)p+pq

    5. 代入拆分四项求和

    将展开式代入

    S

    (

    x

    ,

    y

    )

    S(x,y)

    S(x,y)

    S

    (

    x

    ,

    y

    )

    =

    p

    =

    1

    x

    q

    =

    1

    y

    d

    p

    ,

    q

    [

    (

    x

    +

    1

    )

    (

    y

    +

    1

    )

    (

    y

    +

    1

    )

    p

    (

    x

    +

    1

    )

    q

    +

    p

    q

    ]

    =

    (

    x

    +

    1

    )

    (

    y

    +

    1

    )

    p

    =

    1

    x

    q

    =

    1

    y

    d

    p

    ,

    q

    (

    y

    +

    1

    )

    p

    =

    1

    x

    q

    =

    1

    y

    p

    d

    p

    ,

    q

    (

    x

    +

    1

    )

    p

    =

    1

    x

    q

    =

    1

    y

    q

    d

    p

    ,

    q

    +

    p

    =

    1

    x

    q

    =

    1

    y

    p

    q

    d

    p

    ,

    q

    \\begin{align*} S(x,y) &= \\sum_{p=1}^x\\sum_{q=1}^y d_{p,q} \\cdot \\Big[(x+1)(y+1) – (y+1)p – (x+1)q + pq\\Big] \\\\ &= (x+1)(y+1)\\sum_{p=1}^x\\sum_{q=1}^y d_{p,q} \\\\ &\\quad – (y+1)\\sum_{p=1}^x\\sum_{q=1}^y p\\cdot d_{p,q} \\\\ &\\quad – (x+1)\\sum_{p=1}^x\\sum_{q=1}^y q\\cdot d_{p,q} \\\\ &\\quad + \\sum_{p=1}^x\\sum_{q=1}^y pq\\cdot d_{p,q} \\end{align*}

    S(x,y)=p=1xq=1ydp,q[(x+1)(y+1)(y+1)p(x+1)q+pq]=(x+1)(y+1)p=1xq=1ydp,q(y+1)p=1xq=1ypdp,q(x+1)p=1xq=1yqdp,q+p=1xq=1ypqdp,q

    6. 定义四个二维前缀和(对应代码4棵树)

    令4个二维前缀和:

    {

    T

    1

    (

    x

    ,

    y

    )

    =

    p

    =

    1

    x

    q

    =

    1

    y

    d

    p

    ,

    q

    T

    2

    (

    x

    ,

    y

    )

    =

    p

    =

    1

    x

    q

    =

    1

    y

    p

    d

    p

    ,

    q

    T

    3

    (

    x

    ,

    y

    )

    =

    p

    =

    1

    x

    q

    =

    1

    y

    q

    d

    p

    ,

    q

    T

    4

    (

    x

    ,

    y

    )

    =

    p

    =

    1

    x

    q

    =

    1

    y

    p

    q

    d

    p

    ,

    q

    \\begin{cases} T_1(x,y) = \\displaystyle\\sum_{p=1}^x\\sum_{q=1}^y d_{p,q} \\\\[6pt] T_2(x,y) = \\displaystyle\\sum_{p=1}^x\\sum_{q=1}^y p\\cdot d_{p,q} \\\\[6pt] T_3(x,y) = \\displaystyle\\sum_{p=1}^x\\sum_{q=1}^y q\\cdot d_{p,q} \\\\[6pt] T_4(x,y) = \\displaystyle\\sum_{p=1}^x\\sum_{q=1}^y pq\\cdot d_{p,q} \\end{cases}

    T1(x,y)=p=1xq=1ydp,qT2(x,y)=p=1xq=1ypdp,qT3(x,y)=p=1xq=1yqdp,qT4(x,y)=p=1xq=1ypqdp,q

    S

    (

    x

    ,

    y

    )

    S(x,y)

    S(x,y) 最简公式:

    S

    (

    x

    ,

    y

    )

    =

    (

    x

    +

    1

    )

    (

    y

    +

    1

    )

    T

    1

    (

    y

    +

    1

    )

    T

    2

    (

    x

    +

    1

    )

    T

    3

    +

    T

    4

    \\boldsymbol{S(x,y) = (x+1)(y+1)T_1 – (y+1)T_2 – (x+1)T_3 + T_4}

    S(x,y)=(x+1)(y+1)T1(y+1)T2(x+1)T3+T4

    和代码 sum(x,y) 内部计算公式完全对应:

    ans += (x + 1) * (y + 1) * Tree1[i][j] (y + 1) * Tree2[i][j] (x + 1) * Tree3[i][j] + Tree4[i][j];

    根据上面的公式,就可以写出代码如下: java代码如下:

    import java.io.*;
    import java.util.*;
    public class Main {
    public static int MAXN = 2050;
    public static int MAXM = 2050;
    // 维护信息 : d[i][j]
    public static int[][] Tree1 = new int[MAXN][MAXM];
    // 维护信息 : d[i][j] * i
    public static int[][] Tree2 = new int[MAXN][MAXM];
    // 维护信息 : d[i][j] * j
    public static int[][] Tree3 = new int[MAXN][MAXM];
    // 维护信息 : d[i][j] * i * j
    public static int[][] Tree4 = new int[MAXN][MAXM];
    public static int n, m;
    public static int lowbit(int i) {
    return i & i;
    }
    // 单点更新 (x,y) 增加v
    public static void add(int x, int y, int v) {
    int v1 = v;
    int v2 = x * v;
    int v3 = y * v;
    int v4 = x * y * v;
    for (int i = x; i <= n; i += lowbit(i)) {
    for (int j = y; j <= m; j += lowbit(j)) {
    Tree1[i][j] += v1;
    Tree2[i][j] += v2;
    Tree3[i][j] += v3;
    Tree4[i][j] += v4;
    }
    }
    }
    // 查询(1,1) ~ (x,y) 前缀和
    public static int sum(int x, int y) {
    int ans = 0;
    for (int i = x; i > 0; i -= lowbit(i)) {
    for (int j = y; j > 0; j -= lowbit(j)) {
    // 套用数学推导拆分公式 S=(x+1)(y+1)T1 – (y+1)T2 – (x+1)T3 + T4
    ans += (x + 1) * (y + 1) * Tree1[i][j] (y + 1) * Tree2[i][j] (x + 1) * Tree3[i][j] + Tree4[i][j];
    }
    }
    return ans;
    }
    // 矩形区间 [a,b] ~ [c,d] 整体加v
    public static void add(int a, int b, int c, int d, int v) {
    add(a, b, v);
    add(a, d + 1, v);
    add(c + 1, b, v);
    add(c + 1, d + 1, v);
    }
    // 查询矩形 [a,b] ~ [c,d] 的总和
    public static int range(int a, int b, int c, int d) {
    return sum(c, d) sum(a 1, d) sum(c, b 1) + sum(a 1, b 1);
    }
    public static void main(String[] args) throws Exception {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    StreamTokenizer in = new StreamTokenizer(br);
    PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
    String op;
    int a, b, c, d, v;
    //按照题目要求:循环读取所有输入,直到文件末尾
    while (in.nextToken() != StreamTokenizer.TT_EOF) {
    op = in.sval;
    if (op.equals("X")) {
    in.nextToken();
    n = (int) in.nval;
    in.nextToken();
    m = (int) in.nval;
    } else if (op.equals("L")) {
    in.nextToken();
    a = (int) in.nval;
    in.nextToken();
    b = (int) in.nval;
    in.nextToken();
    c = (int) in.nval;
    in.nextToken();
    d = (int) in.nval;
    in.nextToken();
    v = (int) in.nval;
    add(a, b, c, d, v);
    } else {
    in.nextToken();
    a = (int) in.nval;
    in.nextToken();
    b = (int) in.nval;
    in.nextToken();
    c = (int) in.nval;
    in.nextToken();
    d = (int) in.nval;
    out.println(range(a, b, c, d));
    }
    }
    out.flush();
    out.close();
    br.close();
    }
    }

    C++代码如下:

    //逻辑和Java大致相同
    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int MAX = 2050;
    static int n, m;
    // 创建二维树状数组
    ll Tree1[MAX][MAX], Tree2[MAX][MAX], Tree3[MAX][MAX], Tree4[MAX][MAX];
    inline int lowbit(int x) {
    return x & x;
    }
    // 创建同步单点更新
    void update(int x, int y, ll val) {
    for (int i = x; i <= n; i += lowbit(i)) {
    for (int j = y; j <= m; j += lowbit(j)) {
    Tree1[i][j] += val;
    Tree2[i][j] += val * x;
    Tree3[i][j] += val * y;
    Tree4[i][j] += val * x * y;
    }
    }
    }
    // 二维差分区间加 [a,b]~[c,d] +val
    void range_add(int a, int b, int c, int d, ll val) {
    update(a, b, val);
    update(a, d + 1, val);
    update(c + 1, b, val);
    update(c + 1, d + 1, val);
    }
    // 查询 (1,1)~(x,y) 前缀总和
    ll query(int x, int y) {
    ll s1 = 0, s2 = 0, s3 = 0, s4 = 0;
    for (int i = x; i; i -= lowbit(i)) {
    for (int j = y; j; j -= lowbit(j)) {
    s1 += Tree1[i][j];
    s2 += Tree2[i][j];
    s3 += Tree3[i][j];
    s4 += Tree4[i][j];
    }
    }
    ll res = (x + 1) * (y + 1) * s1 (y + 1) * s2 (x + 1) * s3 + s4;
    return res;
    }
    // 容斥求矩形 [x1,y1]~[x2,y2] 和
    ll get_sum(int x1, int y1, int x2, int y2) {
    return query(x2, y2) query(x1 1, y2) query(x2, y1 1) + query(x1 1, y1 1);
    }
    int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    char op;
    cin >> op >> n >> m;
    int a, b, c, d;
    ll delta;
    while (cin >> op) {
    if (op == 'L') {
    // 区间加
    cin >> a >> b >> c >> d >> delta;
    range_add(a, b, c, d, delta);
    } else if (op == 'k') {
    // 区间查询
    cin >> a >> b >> c >> d;
    cout << get_sum(a, b, c, d) << '\\n';
    }
    }
    return 0;
    }

    结果如下: 运行结果

    其实对应一维的树状数组他的总体时间复杂度为O(logn),所以类比到二维的时间复杂度其实就是O(logn)*O(logm),分析如下:

    函数名(C++)对应Java函数名函数功能单次时间复杂度补充说明
    lowbit lowbit 取数字二进制最低位1

    O

    (

    1

    )

    O(1)

    O(1)

    单纯位运算,常数时间无循环
    update add(int x,int y,int v) 四棵树同步单点更新

    O

    (

    log

    n

    log

    m

    )

    O(\\log n \\cdot \\log m)

    O(lognlogm)

    二维树状数组两层循环,行循环跳

    log

    n

    \\log n

    logn次,列循环跳

    log

    m

    \\log m

    logm次,内部仅常数次数组赋值

    query sum(int x,int y) 查询

    (

    1

    ,

    1

    )

    (

    x

    ,

    y

    )

    (1,1) \\sim (x,y)

    (1,1)(x,y)前缀总和

    O

    (

    log

    n

    log

    m

    )

    O(\\log n \\cdot \\log m)

    O(lognlogm)

    两层倒序遍历树节点,内部仅四则运算,常数忽略
    range_add add(int a,b,c,d,v) 整块矩形区间加值

    O

    (

    log

    n

    log

    m

    )

    O(\\log n \\cdot \\log m)

    O(lognlogm)

    内部调用4次单点update,常数4不影响复杂度阶
    get_sum range(int a,b,c,d) 求任意矩形区间和

    O

    (

    log

    n

    log

    m

    )

    O(\\log n \\cdot \\log m)

    O(lognlogm)

    内部调用4次前缀查询query,常数4不影响复杂度阶

    所以对比暴力解法,这种算法就快多了,但是却学要思考和数学上的推导,所以代码搞我千百遍,我带代码还要依旧如初恋!

    总结一下

    其实对于树状数组而言,写出他的模版并不是一件难事,但是算法算法,本质上还是难在应用上了,有的时候学习它就觉得很懂了,但一用起来却就百般地抓耳挠腮,不知所措,其实还是用的不够多,因为算法,本质上还是为现实服务的,学习它不可能是死的,这种算法有啥优点,有啥缺点,啥时候使用,都是要搞懂的地方,所以人生万事须自为,硅步江山即辽阔!亲身经历的bug才会让人记忆犹新,所以还是加油吧!

    赞(0)
    未经允许不得转载:171主机测评 » 数状数组的学习与分析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址