【数据结构】树状数组

简介

用一下google随手搜索的一张图。

  1. 首先要理解其中每个节点存储的都是一个连续区间的和。
  2. 实际上就是一棵线段树把每个节点的右子树直接剪除的结果。
  3. Add操作的时候,从当前编号的叶子节点开始(不一定是长度为1的层,但一定是叶子),先修改当前节点,然后每次加上当前的层的长度(也就是lowbit)就能到达其正上方的第一层没有缺失的节点。直到到达树根(也就是节点n,最长的节点),不为2的整数幂的n可能会有两个根,这个不影响。
  4. Sum操作的时候,求得其实是前缀和,也是从叶子节点开始,加上当前节点的和,然后每次减去当前的层长度,就能跳到左上方的节点上。所以注意其实+=lowbit和-=lowbit并不是逆操作,稍微想一想也会知道并不可逆,这两个操作其实都是消除了末尾的1,都是往上层长度更长的节点去跳。
  5. 如果把Add和Sum操作写反的话,Add的时候往左上角加上了一片,看起来像是一个左右镜像的树状数组,Sum的时候就往正上方跳。感觉整个定义就全部都乱套了,所以一定是错的。

模板

单点加值,区间求和

这个是最基础的树状数组的用法了。初始值是Init之后一个一个Add上去的。

struct BinaryIndexTree {

    static const int MAXN = 500000 + 5;

    int n;
    int sm[MAXN];

    void Add(int x, int v) {
        for(int i = x; i <= n; i += i & (-i))
            sm[i] += v;
    }

    ll Sum(int x) {
        ll res = 0;
        for(int i = x; i; i -= i & (-i))
            res += sm[i];
        return res;
    }

    void Init(int _n) {
        n = _n;
        memset(sm, 0, sizeof(sm[0]) * (n + 1));
    }

} bit;

已通过:
https://www.luogu.com.cn/problem/P3374

单点改值,区间求和

额外记录一个va数组,然后每次比对这次要“加”的值是多少,套用单点加值。

struct BinaryIndexTree {

    static const int MAXN = 500000 + 5;

    int n;
    int va[MAXN];
    int sm[MAXN];

    void Add(int x, int v) {
        for(int i = x; i <= n; i += i & (-i))
            sm[i] += v;
    }

    ll Sum(int x) {
        ll res = 0;
        for(int i = x; i; i -= i & (-i))
            res += sm[i];
        return res;
    }

    void Init(int _n) {
        n = _n;
        memset(va, 0, sizeof(va[0]) * (n + 1));
        memset(sm, 0, sizeof(sm[0]) * (n + 1));
    }

    void Modify(int x, int v) {
        int d = v - va[x];
        Add(x, d);
        va[x] = v;
    }

} bit;

二维偏序

namespace BinaryIndexTree {

    const int MAXN = 200000 + 5;

    int n;
    int nn;
    int a[MAXN];
    int aa[MAXN];

    int bit[MAXN];

    void Add(int x, int v) {
        for(int i = x; i <= nn; i += i & (-i))
            bit[i] += v;
    }

    ll Sum(int x) {
        ll res = 0;
        for(int i = x; i; i -= i & (-i))
            res += bit[i];
        return res;
    }

    void Init1() {
        n = 0;
        nn = 0;
    }

    void Insert(int v) {
        a[++n] = v;
        aa[++nn] = v;
    }

    void Init2() {
        sort(aa + 1, aa + 1 + nn);
        nn = unique(aa + 1, aa + 1 + nn) - (aa + 1);
        memset(bit, 0, sizeof(bit[0]) * (nn + 1));
        for(int i = 1; i <= n; ++i) {
            a[i] = lower_bound(aa + 1, aa + 1 + nn, a[i]) - aa;
            Add(a[i], 1);
        }
    }

}

区间修改区间求和

设差分数组 \(d_i=a_i-a_{i-1}\) ,显然有 \(a_i=\sum\limits_{j=1}^i d_j\) ,那么 \(Sum(x)=\sum\limits_{i=1}^x a_i=\sum\limits_{i=1}^x \sum\limits_{j=1}^i d_j = \sum\limits_{i=1}^x (x-i+1)*d_i=(x+1)\sum\limits_{i=1}^xd_i - \sum\limits_{i=1}^x i*d_i\)

struct BinaryIndexTree {
//    const int MAXN = 3e5 + 5;
    int n;
    ll d1[MAXN], d2[MAXN];
    void Add(int x, int v) {
        for(int i = x; i <= n; i += i & (-i)) d1[i] += v, d2[i] += 1LL * x * v;
    }
    ll Sum(int x) {
        ll res = 0;
        for(int i = x; i; i -= i & (-i)) res += 1LL * (x + 1) * d1[i] - d2[i];
        return res;
    }
    int RangeAdd(int l, int r, int v) {
        Add(l, v), Add(r + 1, -v);
    }
    ll RangeSum(int l, int r) {
        return Sum(r) - Sum(l - 1);
    }
    void Init(int _n) {
        n = _n;
        memset(d1, 0, sizeof(d1[0]) * (n + 1));
        memset(d2, 0, sizeof(d2[0]) * (n + 1));
    }
} bit;

Tricks

O(n)建树、权值树状数组求第k小:https://oi-wiki.org/ds/fenwick/

权值树状数组,每个点i记录值为i的节点出现的频率。然后找一个最小的值x,使得小于等于x的所有频率之和恰好>=k。实际上的思路就是“权值线段树上二分”,根节点就是 log2(n) 的下整,每次确定是走左边子树还是走右边子树,先尝试走右边子树,如果爆了(右子树下标越界(不存在此右子树)、加上左子树的全部频次后>=k),就要走左子树。最后找到就是恰好<k的那个值(并且如果遇到0会把0也吃掉,延展到最后一个非0值的左边界),只需要把答案+1,就是刚好>=k的位置。

// 权值树状数组查询第k小
int kth(int k) {
  int cnt = 0, ret = 0;
  for (int i = log2(n); ~i; --i) {      // i 与上文 depth 含义相同
    ret += 1 << i;                      // 尝试扩展
    if (ret >= n || cnt + t[ret] >= k)  // 如果扩展失败
      ret -= 1 << i;
    else
      cnt += t[ret];  // 扩展成功后 要更新之前求和的值
  }
  return ret + 1;
}

时间戳优化,不知道是哪个小可爱想出来的不想清空的主意,额外多维护一个tag表示是第x组数据,如果遇到的节点记录的并不是第x组数据的tag,那么就清空它/不计算它。

// C++ Version
// 时间戳优化
int tag[MAXN], t[MAXN], Tag;

void reset() { ++Tag; }

void add(int k, int v) {
  while (k <= n) {
    if (tag[k] != Tag) t[k] = 0;
    t[k] += v, tag[k] = Tag;
    k += lowbit(k);
  }
}

int getsum(int k) {
  int ret = 0;
  while (k) {
    if (tag[k] == Tag) ret += t[k];
    k -= lowbit(k);
  }
  return ret;
}

感觉没有啥用的一个东西,记录tag不如记录k,reset的时候把add过的k全部强制设为0就好了,或者加上其相反的v(需要额外保存v)。

总之时间戳优化是个画蛇添足的东西。

扩展

权值树状数组/简单名次树:https://www.cnblogs.com/purinliang/p/14265133.html

二维树状数组:单点修改区间查询、区间修改单点查询(转化为差分然后变成第一个问题)、区间修改区间查询(通过看差分数组的使用次数维护额外的差分*使用次数的树状数组):https://www.cnblogs.com/hbhszxyb/p/14157271.html

整体思路跟一维的是一样的。

posted @ 2020-09-15 11:59  purinliang  阅读(197)  评论(0编辑  收藏  举报