• 树状数组:单点修改,区间查询(详解)


    问题的提出 :
    给定一个序列 a,可以进行两种操作:

    • 1 i x :给定 i , x, 将 a[i] 加上 x;
    • 2 l r :给定 l , r, 求 a[l] + a[l + 1] + ··· + a[r + 1] 的值

    (单点修改,区间查询)


    首先,我们会想到直接用一个现行的数组。那么单点修改的时间复杂度将是 O(1)O(1),但是区间查询的时间复杂度却是 O(n)O(n), 数据范围一大,就很有可能会超时。
    那么,又有人会想到用一个前缀和数组,但是有没有想过,虽然区间查询的时间复杂度变成了 O(1)O(1),但单点修改就又变成了 O(n)O(n),因为我们在修改单点的时候还要维护这个前缀和数组。


    所以,为了优化时间复杂度,我们就需要引入一个概念:
    树状数组: (下面来自百度)
    树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。
    这种数据结构(算法)并没有C++和Java的库支持,需要自己手动实现。在Competitive Programming的竞赛中被广泛的使用。树状数组和线段树很像,但能用树状数组解决的问题,基本上都能用线段树解决,而线段树能解决的树状数组不一定能解决。相比较而言,树状数组效率要高很多。
    在这里插入图片描述
    上面就是一棵树。可能有同学就要问了:这张图有用吗?
    非常有用!
    每一个数子的子叶有多少个呢?这么看是看不出来的。其实每一个数的个数等于这个数化为二进制之后,从低位起往前查找,当找到第一个 1 的时候,就把 1 和后面的 0 截出来,化成 10 进制,这个十进制的数就是他子叶的个数(包括他自己)。下面是示例:

    • 如上图的 8 ,化成二进制就是 1000, 那么就是把 1000 化为十进制,也就是 8 有 8 个子叶
    • 又如上图的 6,化成二进制就是 110,那么就是把 10 化为十进制,也就是 6 有 3 个子叶

    但是像上面的操作就很复杂,也会消耗大量的时间,所以我们又可以得到两个公式:
    x - (x & (x - 1)) 或者 x & (-x) ; 这两个最为常用。

    1. x - (x & (x - 1)) : 我们举个栗子吧。比如 20 ,它化成 2 进制之后,就是 10010,那么它减一就是 :10001 再把他们两个与起来,就是 10000, 再用原数来减,就刚好是 10 ,返回时就会自动转换成 10 进制了。
    2. 可以在草稿纸上尝试自行证明~
      然后我们又可以看到,其实 C[8] = c[4] + c[6] + c[5] + c[8]; 那四个数加起来其实就是c[1~8]的和。
      我们就可以写出一个循环:
    void Update(int x, long long num_) {//第一个是下标,第二个是值
        for (int i = x; i <= n; i += Lowbit(i)) {//从这个位置开始,不超出数组的范围,每一次加上他的上一个(草稿纸证明)
            bit[i] += num_;//bit就是树状数组的缩写。其实就是一直往他的父亲查找,每一个父亲都加上他(建树)
        }
    }
    

    关于区间查询,我们很容易就可以想到前缀和。所以a[l~r] = sum® - sum(l - 1)。下面是前缀和的代码:

    long long Sum(int x) {
    	long long sum_ = 0;//累加
    	for (int i = x; i >= 1; i -= Lowbit_Set(i)) {//从这个点往他的儿子进行查找
    		sum_ += bit[i];
    	}
    	return sum_;
    }
    

    所以上面那道题我们就可以轻松地解决了。下面是代码:

    #include <cstdio>
    #include <cmath>
    #include <iostream>
    #include <algorithm>
    using namespace std;
    
    const int MAXN = 1e6 + 5;
    long long bit[MAXN];
    int a[MAXN];
    int n, m;
    
    int Lowbit_Set(int x) {
    	return x & (-x);
    }//查找子叶个数
    
    void Update_Set(int x, int num) {
    	for (int i = x; i <= n; i += Lowbit_Set(i)) {
    		bit[i] += num;
    	}
    }//如上
    long long Sum_Set(int x) {
    	long long sum_ = 0;
    	for (int i = x; i >= 1; i -= Lowbit_Set(i)) {
    		sum_ += bit[i];
    	}
    	return sum_;
    }//如上
    
    int main() {
    	scanf("%d %d", &n, &m);
    	for (int i = 1; i <= n; i++) { 
    		scanf("%d", &a[i]);
    		Update_Set(i, a[i]);//传参
    	} 
    	for (int i = 1; i <= m; i++) {
    		int x, y, z;
    		scanf("%d %d %d", &x, &y, &z);
    		if (x == 1) {
    			Update_Set(y, z);
    		}
    		if (x == 2) {
    			while(1) {}//下次记得不要抄代码哦!
    			long long sum1_ = Sum_Set(y - 1);
    			long long sum2_ = Sum_Set(z);
    			printf("%lld
    ", sum2_ - sum1_);//前缀和公式
    		}
    	}/*
    	for (int i = 1; i <= n; i++) {
    		printf("%lld ", bit[i]);
    	} */
    	//检查是否写对
    	return 0;
    } 
    
  • 相关阅读:
    MySQL my.cnf详解
    函数:sleep-exit-wait
    fork-小实验
    OS-进程调度
    CET-4流程
    SDK和API的区别
    生活-金钱管理-不是理财
    算法设计与分析:Strassen矩阵乘法
    力扣:二进制加法求和
    算法设计与分析:大整数乘法
  • 原文地址:https://www.cnblogs.com/cqbzyanglin/p/13509281.html
Copyright © 2020-2023  润新知