首页 > 编程语言 >【算法】树状数组

【算法】树状数组

时间:2024-10-13 21:10:31浏览次数:9  
标签:ch 树状 int sum 算法 数组 define

1. 算法简介

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

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

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

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

以下是数据对比:

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

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

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

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

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

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

2. 算法实现

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

可以观察到,树状数组 \(c_i\) \((1 \le i \le n)\) 所管辖的范围为 \([i,i-lowbit(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

维护差分序列 \(d_i\),则修改操作为单点,查询操作为查询 \(\sum\limits_{i=l}^r\sum\limits_{j=1}^i d_j\)。

然后拆式子 \(sum(x)=\sum\limits_{i=1}^x\sum\limits_{j=1}^i d_j=\sum\limits_{i=1}^x(x-i+1)d_i=x\sum\limits_{i=1}^xd_i+\sum\limits_{i=1}^xd_i\times (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;
}

开两个树状数组分别维护 \(\sum\limits_{i=1}^xd_i\),\(\sum\limits_{i=1}^xd_i\times (i+1)\) 即可。

标签:ch,树状,int,sum,算法,数组,define
From: https://www.cnblogs.com/Daniel-yao/p/17258699.html

相关文章

  • 【算法】莫队
    1.算法简介莫队算法有很多种:普通莫队,带修莫队,回滚莫队,树上莫队,二维莫队,莫队二次离线。莫队算法主要用于解决支持快速插入,删除贡献的区间优化问题。具体的,对于要求解贡献的区间\([l,r]\)来说,我们可以把以前求解过的区间\([L,R]\)的贡献保留下来,并通过移动\(L,R\),同时插入,......
  • 2024-10-13:用go语言,给定一个二进制数组 nums,长度为 n, 目标是让 Alice 通过最少的行动
    2024-10-13:用go语言,给定一个二进制数组nums,长度为n,目标是让Alice通过最少的行动次数从nums中拾取k个1。Alice可以选择任何索引aliceIndex,如果对应的nums[aliceIndex]是1,Alice会拾取一个1并将其设为0。之后,Alice可以选择以下两种行动之一:将一个0变为1(最多执行maxCh......
  • 算法复杂度
    一.复杂度的概念算法编写成可执行程序后,需要耗费时间空间资源,衡量一个算法的好坏是从时间和空间两个维度衡量。时间复杂度衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。二.时间复杂度算法的时间复杂度是一个函数T(N),他定义了该算法运行的时间,时......
  • 代码随想录算法训练营 | 198.打家劫舍,213.打家劫舍II,337.打家劫舍III
    198.打家劫舍题目链接:198.打家劫舍文档讲解︰代码随想录(programmercarl.com)视频讲解︰打家劫舍日期:2024-10-13想法:dp[i]到第i个房子时能偷的最多的钱;递推公式:是上上一栋房子的dp[i-2]加上这栋房子的钱nums[i]大还是上一家邻居偷的钱dp[i-1]的大;初始化因为有i-2;所以初始化......
  • Java——数组的定义与使用
    各位看官:如果您觉得这篇文章对您有帮助的话欢迎您分享给更多人哦感谢大家的点赞收藏评论,感谢您的支持!!!一:数组的概念以及定义,初始化1.1:数组概念以及定义数组概念:可以看成是相同类型元素的一个集合。数组定义:三种方法T[]数组名=newT[N];例如:int[]a......
  • C/C++贪心算法
    C++中的贪心算法一、基本概念贪心算法(又称贪婪算法,GreedyAlgorithm)是指,在对问题求解时,总是做出在当前看来是最好的选择,不从整体最优上加以考虑,所做出的仅是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题能产生整体最优解......
  • 基于Spring Boot+Vue的医疗健康的便民服务平台系统的设计与实现(协同过滤算法、实时聊
    ......
  • day05-Lambda、方法引用、算法、正则表达式
    day05-算法和数据结构一、Arrays类接下来我们学习的类叫做Arrays,其实Arrays并不是重点,但是我们通过Arrays这个类的学习有助于我们理解下一个知识点Lambda的学习。所以我们这里先学习Arrays,再通过Arrays来学习Lamdba这样学习会更丝滑一些_.1.1Arrays基本使用我们先认识一下Arr......
  • 基于牛顿拉夫逊算法优化长短期记忆网络结合注意力机制(NRBO-LSTM-Attention)(多输入多
    文章目录效果一览文章概述部分源码参考资料效果一览文章概述基于牛顿拉夫逊算法优化长短期记忆网络结合注意力机制(NRBO-LSTM-Attention)(多输入多输出)(多输入多输出)MATLAB完整源码和数据纯手工制作,代码质量极高,注释清晰,excel数据,方便替换1.data为数据集,10个......
  • 【算法题解记录】_1
    目录【算法题解记录】_1leetcode_115题【算法题解记录】_1还是干这个事了,要吃饭嘛,决定把在解题或者解完题进一步优化的时候,发现的有意思的地方记录一下leetcode_115题动态规划解的字符串匹配,矩阵全部都遍历,用时比较长,发现排在网友们的末尾纸上画了一下矩阵,行数代表被匹配串s......