【算法】树状数组

1. 算法简介#

先来看一个很现实的问题:

就拿 [luogu]P3372【模板】线段树 1 这道题为例。

按常规做法,应该是用普通线段树 + lazytag 即可,但这样做代码较长,达到了 118 行。

而如果用树状数组去做,只用 63 行就能搞定,用时更短,代码也很好理解。

以下是数据对比:

很明显,在两者都开了 O2 的情况下,树状数组在时间、空间、代码长度上完胜线段树!!!

Q: 树状数组这么好用,为什么不直接用树状数组完全替代线段树呢?

这就又要讲到树状数组的特性:它是一颗‘前缀树’。也就是树状数组只能用于维护前缀信息,如前缀和、前缀积、前缀异或等等。想要维护区间信息,就必须要使维护的区间信息有 ‘可减性’(这里和线段树是相反的,线段树需要满足 ‘可合并性’)。在一些区间信息无 ‘可减性’ 的时候,树状数组就无法胜任。例如区间最值、区间第 k 小等等,普通的树状数组就无能为力了。

Q: 学习树状数组需要哪些前置知识呢?

二进制、位运算、前缀和,差分。最好有线段树基础。

到现在,大家应该对树状数组有了一定的了解还不是很了解,那就来由我来给大家讲讲树状数组的知识吧!

2. 算法实现#

一个树状数组大概长这样(以区间和为例)

可以观察到,树状数组 ci (1in) 所管辖的范围为 [i,ilowbit(i)+1],其中 lowbit(i) 表示 i 在二进制下末尾 0 的。

比如 12(10)=1100(2),则 lowbit(12(10))=100(2)=4(10).

然后进行修改和查询操作的时候,可以利用 lowbit 跳跃节点。(参考例图)

比如要将 1 号节点加 k,先将 1 号节点加上 k;然后跳跃至 1+lowbit(1)=2 号点,加上 k;继续跳跃至 2+lowbit(2)=4 号点,加上 k,最后跳跃至 4+lowbit(4)=8 号点,加上 k

查询也是如此,只要依次跳跃至查询节点即可。

lowbit 函数实现:

int lb(int x) {
  return x & -x;
}

单点修改实现:

void add(int x, int k) {
  for (int i = x; i <= n; i += lb(i)) {
    t[i] += k;
  }
}

区间查询 [1,x] 实现:

int qry(int x) {
  int res = 0;
  for (int i = x; i; i -= lb(i)) {
    res += t[i];
  }
  return res;
}

2.1 单点加区间和#

P3374 【模板】树状数组 1

直接套用树状数组的单点加,区间和的操作即可。

点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007

using namespace std;

inline int read() {
  rint x=0,f=1;char ch=getchar();
  while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
  while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
  return x*f;
} 

const int N = 5e5 + 10;

int n, m, t[N];

int lb(int x) {
  return x & -x;
} 

void add(int x, int k) {
  for (int i = x; i <= n; i += lb(i)) {
    t[i] += k;
  }
}

int qry(int x) {
  int res = 0;
  for (int i = x; i; i -= lb(i)) {
    res += t[i];
  }
  return res;
}

signed main() {
  n = read(), m = read();
  For(i,1,n) add(i, read());
  while(m--) {
    int op, x, y;
    op = read(), x = read(), y = read();
    if(op == 1) add(x, y);
    else cout << qry(y) - qry(x - 1) << '\n';
  }
  return 0;
}

2.2 区间加单点和#

P3368 【模板】树状数组 2

树状数组维护差分。

每一次操作在差分序列上的 l 处加,r+1 处减等价在原序列的 [l,r] 加。单点和即求 [1,x] 的差分序列前缀和。

点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007

using namespace std;

inline int read() {
  rint x=0,f=1;char ch=getchar();
  while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
  while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
  return x*f;
} 

const int N = 5e5 + 10; 

int n, m, t[N], a[N];

int lb(int x) {
  return x & -x;
}

void add(int x, int k) {
  for (int i = x; i <= n; i += lb(i)) {
    t[i] += k;
  }
}

int qry(int x) {
  int res = 0;
  for (int i = x; i; i -= lb(i)) {
    res += t[i];
  }
  return res;
} 

signed main() {
  n = read(), m = read();
  For(i,1,n) a[i] = read();
  For(i,1,n) add(i, a[i] - a[i-1]);
  while(m--) {
    int op, x, y, k;
    op = read();
    if(op == 1) {
      x = read(), y = read(), k = read();
      add(x, k);
      add(y+1, -k);
    } else {
      x = read();
      cout << qry(x) << '\n';
    }
  }
  return 0;
}

2.3 区间加区间和#

P3372 【模板】线段树 1

维护差分序列 di,则修改操作为单点,查询操作为查询 i=lrj=1idj

然后拆式子 sum(x)=i=1xj=1idj=i=1x(xi+1)di=xi=1xdi+i=1xdi×(i+1)

开两个树状数组分别维护 i=1xdii=1xdi×(i+1) 即可。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)

using namespace std;

const int N = 1e5 + 10;

int n, m, a[N], t1[N], t2[N];

int lb(int x) {
  return x & -x;
}

void add(int x, int k) {
  for (int i = x; i <= n; i += lb(i)) {
    t1[i] += k;
    t2[i] += k * (x - 1);
  }
}

int qry(bool f, int x) {
  int ans = 0;
  for (int i = x; i; i -= lb(i)) {
    ans += (!f ? t1[i] : t2[i]);
  }
  return ans;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> m;
  For(i,1,n) {
    cin >> a[i];
    add(i, a[i] - a[i-1]);
  }
  while(m--) {
    int op, x, y, k;
    cin >> op >> x >> y;
    if(op == 1) {
      cin >> k;
      add(x, k);
      add(y+1, -k);
    } else {
      cout << (y * qry(0, y) - qry(1, y)) - (((x-1) * qry(0, x-1)) - qry(1, x-1)) << '\n';
    }
  }
  return 0;
}

作者:Daniel-yao

出处:https://www.cnblogs.com/Daniel-yao/p/17258699.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   Daniel_yzy  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示