zoukankan      html  css  js  c++  java
  • [模板]树状数组

    树状数组在O(logN)时间内对数组执行单点增加,区间和查询.

    观察这个树状数组:

    其中,每个节点存储着其横向覆盖的范围内所有元素的和,如c[16]存储1~16号元素的和,c[12]存储9~12号元素的和.

    节点x的覆盖范围表示为[x-lowbit(x)+1, x].

     从构造这颗树的角度来解释其结构,描述为每个节点c[x]的父节点都可以表示为c[x+lowbit(x)].

    由此可推出树的深度为O(logN),这意味着从任意节点出发,都可以快速地遍历其到根节点的路径上的每一个点:

        while (x <= n) {
            operations on c[x]...
            x += lowbit(x);
        }
    
    // 注: int lowbit(int x) { return x & -x; }

    此外,从任意节点x出发,想要不重不漏地遍历1~x号元素只需要:

        while (x > 0) {
            operations on c[x]...
            x -= lowbit(x);
        }

    以上就是树状数组支持的两种操作,复杂度都是O(logN).关于它为什么看起来像魔法,点这里.

    由于树状数组的第二种操作,它很适合用来计算前缀和.

    求取区间内所有数的和,使用前缀和表示,故维护树状数组c[i],操作1在位置x上加k,操作二计算前缀和c[1~y]与c[1~x-1]并输出两者差值.

    int lowbit(int x) { return x & -x; }
    void add(int x, int k) {        // 单点增加,在位置x增加k
        while (x <= n) {
            c[x] += k;
            x += lowbit(x);
        }
    }
    int sum(int x) {                // 区间和查询,求取前缀和c[1~x]
        int ret = 0;
        while (x > 0) {
            ret += c[x];
            x -= lowbit(x);
        }
        return ret;
    }

    故有:

    #include <algorithm>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    using namespace std;
    
    int n, m, s[500010];
    
    int lowbit(int x) { return x & -x; }
    void add(int x, int y) {
        while (x <= n) {
            s[x] += y;
            x += lowbit(x);
        }
    }
    int sum(int x) {
        int ans = 0;
        while (x > 0) {
            ans += s[x];
            x -= lowbit(x);
        }
        return ans;
    }
    
    int main() {
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; ++i) {
            int x;
            scanf("%d", &x);
            add(i, x);
        }
    
        for (int i = 1; i <= m; ++i) {
            int opr, x, y;
            scanf("%d%d%d", &opr, &x, &y);
            if (opr == 1)
                add(x, y);
            else
                cout << sum(y) - sum(x - 1) << endl;
        }
    
        return 0;
    }
    P3374

    既然有前缀和,对应地就可以运用差分解决如下问题:

    #include <algorithm>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    using namespace std;
    
    int n, m, s[500010], a[500010];
    
    inline int lowbit(int x) {return x & -x;}
    inline void add(int p, int x) {
        while(p <= n){
            s[p] += x;
            p += lowbit(p);
        }
    }
    inline int ask(int x){
        int ret = 0;
        while(x > 0){
            ret += s[x];
            x -= lowbit(x);
        }
        return ret;
    }
    
    int main(){
        scanf("%d%d", &n, &m);
        for(int i = 1; i <= n; i++) scanf("%d", a + i);
        
        while(m--){
            int opr, x, y, k;
            scanf("%d", &opr);
            if(opr == 1){
                scanf("%d%d%d", &x, &y, &k);
                add(x, k);
                add(y + 1, -k);
            }else{
                scanf("%d", &x);
                printf("%d
    ", a[x] + ask(x));
            }
        }
    
        return 0;
    }
    P3368

    可以将树状数组的效果简单地总结为:对一序列进行频繁局部(单点)操作时,在任意时刻都可以快速求取当前序列的任意前缀和.


    P1908 逆序对

    如利用桶的思想,将数x置入桶c[x]中时,检查c[x+1~n]之和,即得新产生的逆序数.

    树状数组满足了随时查询前缀和的操作.

    需要处理一些细节,还有离散化.

    #include <algorithm>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    using namespace std;
    
    struct S {
        int num, val;
        bool operator<(S &other) const {
            return (val == other.val ? num < other.num : val < other.val);
        }
    } s[500010];
    int c[500010], rk[500010], n;
    
    inline void add(int p, int d) {
        while (p <= n) {
            c[p] += d;
            p += p & -p;
        }
    }
    inline int ask(int p) {
        int ret = 0;
        while (p) {
            ret += c[p];
            p -= p & -p;
        }
        return ret;
    }
    
    int main() {
        scanf("%d", &n);
        for (int i = 1; i <= n; i++) scanf("%d", &s[i].val), s[i].num = i;
    
        sort(s + 1, s + 1 + n);
        long long ans = 0;
        for (int i = 1; i <= n; i++) rk[s[i].num] = i;
        for (int i = 1; i <= n; i++) {
            add(rk[i], 1);
            ans += i - ask(rk[i]);
        }
    
        printf("%lld
    ", ans);
    
        return 0;
    }
    P1908

    P3372 【模板】线段树 1

    由于数学是美的,两个树状数组可以过了这道题.这里树状数组的作用仍然是快速求前缀和.

     

    一篇更详细的博客:https://www.luogu.com.cn/blog/kingxbz/shu-zhuang-shuo-zu-zong-ru-men-dao-ru-fen

  • 相关阅读:
    App.js和App.css(用于移动应用的js和css)
    cookie和session使用
    html实现返回上一页的几种方法(javaScript:history.go(-1);)
    sublime找到成对标签(Ctrl+Shift+")
    Java NIO框架Netty课程(一) – Hello Netty
    信息增益的特征选择方法
    Java线程学习笔记(两) 线程异常处理
    薏米红豆粥的功效和实践演示
    文件翻译002片:Process Monitor帮助文档(Part 2)
    Spring MVC 3 深入总结
  • 原文地址:https://www.cnblogs.com/Gaomez/p/14574940.html
Copyright © 2011-2022 走看看