理解树状数组这一篇文章就够啦
树状数组
TODO:
前言
在阅读本文之前,您可能需要先了解位运算、二叉树以及前缀和与差分等相关知识
本文中,若无特殊说明,数列下标均从
引入
什么是树状数组
树状数组是一种 通过数组来模拟"树形"结构,支持单点修改和区间查询的数据结构
因为它是通过二进制的性质构成的,所以它又被叫做 二进制索引树(
用于解决什么问题
树状数组常用于动态维护区间信息
例题
题目简述:对数列进行单点修改以及区间求和
常规解法
单点修改的时间复杂度为
区间求和的时间复杂度为
共
import java.io.*;
public class Main {
static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static int get() throws IOException {
in.nextToken();
return (int) in.nval;
}
public static void main(String[] args) throws IOException {
PrintWriter out = new PrintWriter(System.out);
int n = get(), m = get();
int[] a = new int[n];
for (int i = 0; i < n; ++i) a[i] = get();
while (m-- != 0) {
int command = get(), x = get(), y = get();
if (command == 1) {
a[x - 1] += y;
} else {
int sum = 0;
for (int i = x - 1; i < y; i++) sum += a[i];
out.println(sum);
}
}
out.close();
}
}
前缀和解法
区间求和通过前缀和优化,但单点修改的时候需要修改前缀和数组
单点修改的时间复杂度为
区间求和的时间复杂度为
共
import java.io.*;
public class Main {
static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static int get() throws IOException {
in.nextToken();
return (int) in.nval;
}
public static void main(String[] args) throws IOException {
PrintWriter out = new PrintWriter(System.out);
int n = get(), m = get();
int[] sum = new int[n + 1];
for (int i = 1; i <= n; ++i) sum[i] = sum[i - 1] + get();
while (m-- != 0) {
int command = get(), x = get(), y = get();
if (command == 1) {
for (int i = x; i <= n; ++i) sum[i] += y;
} else {
System.out.println(sum[y] - sum[x - 1]);
}
}
out.close();
}
}
树状数组解法
可以发现上述两种方法,不是单点修改的时间复杂度过高,就是区间求和的时间复杂度过高,导致最坏时间复杂度很高。
于是,树状数组出现了,它用来平衡这两种操作的时间复杂度。
树状数组的思想
每个正整数都可以表示为若干个
类似的,每次求前缀和,我们也希望将区间
也就是如果
树状数组的树形态与二叉树
每一个矩形代表树的一个节点,矩形大小表示所管辖的数列区间范围
一颗二叉树的形态如下图所示
我们发现,对于具有逆运算的运算,如求和运算,有如下式子
实际上,许多数据可以通过一些节点的差集获得
因此,上述二叉树的一些节点可以进行删除
树状数组的形态如下图所示
管辖区间
对于下图中的树状数组(黑色数字代表原始数组
从图中可以看出:
树状数组 | 管辖区间 |
---|---|
那么如何通过计算机确定
前面提到过树状数组的思想是基于二进制的
树状数组中,规定
- 设二进制最低位为第
位,则 恰好为 的二进制表示中,最低位的 所在的二进制位数; ( 的管辖区间长度)恰好为 二进制表示中,最低位的 以及后面所有 组成的数。
以
因为
因此,
我们记
其中 lowbit(x) = x & (~x + 1) = x & -x
树状数组树的性质
性质比较多,下面列出重要的几个性质,更多性质请参见OI Wiki,下面表述忽略二进制前导零
-
节点
的父节点为 -
设节点
,则其儿子数量为 (即 的二进制表示中尾随 的个数),这 个儿子的编号分别为如
, 的二进制表示为1000
,则 有三个儿子,这三个儿子的二进制编号分别为111
、110
、100
-
节点
的所有儿子对应 的管辖区间恰好拼接成-
如
, 的二进制表示为1000
, 的三个儿子的二进制编号分别为111
、110
、100
C[100]
表示A[001~100]
,C[110]
表示A[101~110]
,C[111]
表示A[111~111]
上述三个儿子管辖区间的并集恰好是
A[001~111]
,即
-
单点修改
根据管辖区间,逐层维护管辖区间包含这个节点的父节点(节点
void add(int x, int val) { // A[x] 加上 val
for (; x <= n; x += x & -x) {
C[x] += val;
}
}
区间查询
区间查询问题可以转化为前缀查询问题(前缀和思想),也就是查询区间
如计算
前缀查询的过程是:根据管辖区间,不断拆分区间,查找上一个前缀区间
对于
- 从
开始向前拆分,有 管辖 - 令
- 重复上述过程,直至
由于
// 查询前缀 A[1...x] 的和
int getSum(int x) {
int ans = 0;
for (; x != 0; x &= x - 1) ans += C[x];
//for (; x != 0; x -= x & -x) ans += C[x];
return ans;
}
// 查询区间 A[l...r] 的和
int queryRange(int l, int r) {
return getSum(r) - getSum(l - 1);
}
上述过程进行了两次前缀查询,实际上,对于
// 查询区间 A[l...r] 的和
int queryRange(int l, int r) {
// return getSum(r) - getSum(l - 1);
int ans = 0;
--l;
while (l < r) {
// 左边层数低,左边向前跳
int lowbitl = l & -l, lowbitr = r & -r;
if (l != 0 && lowbitl < lowbitr) {
ans -= C[l];
l -= lowbitl;
} else {
ans += C[r];
r -= lowbitr;
}
}
return ans;
}
单点查询
单点查询可以转化为区间查询,需要两次前缀查询,但有更好的方法
则
对于
- 查询
所管辖的区间 - 减去
的所有子节点的数据
//int queryOne(int x) {
// return queryRange(x, x);
//}
int queryOne(int x) {
int ans = c[x];
int lca = x & x - 1; // x - lowbit(x)
for (int i = x - 1; i > lca; i &= i - 1) {
ans -= C[i];
}
return ans;
}
建树
可以通过调用单点修改方法进行建树,时间复杂度
时间复杂度为
方法一:
每一个节点的值是由所有与自己直接相连的儿子的值求和得到的。因此可以倒着考虑贡献,即每次确定完儿子的值后,用自己的值更新自己的直接父亲。
void init() {
for (int i = 1; i <= n; ++i) {
C[i] += A[i];
// 找 i 的父节点
int father = i + (i & -i);
if (father <= n) C[father] += C[i];
}
}
方法二:
由于
我们也可以先用
同样的
void init() {
for (int i = 1; i <= n; ++i) {
C[i] = C[i - 1] + A[i];
}
for (int i = n; i > 0; --i) {
C[i] -= C[i & i - 1];
}
}
复杂度分析
空间复杂度为
单点修改、单点查询、区间查询操作的时间复杂度均为
建树的时间复杂度为
Code
import java.io.*;
public class Main {
static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static int get() throws IOException {
in.nextToken();
return (int) in.nval;
}
static int n;
static int[] c, a;
static void add(int x, int val) {
for (; x <= n; x += x & -x) {
c[x] += val;
}
}
static int getSum(int x) {
int ans = 0;
for (; x != 0; x &= x - 1) ans += c[x];
return ans;
}
static int queryOne(int x) {
int ans = c[x];
int lca = x & x - 1;
for (int i = x - 1; i > lca; i &= i - 1) {
ans -= c[i];
}
return ans;
}
static int queryRange(int l, int r) {
int ans = 0;
--l;
while (l < r) {
// 左边层数低,左边向前跳
int lowbitl = l & -l, lowbitr = r & -r;
if (l != 0 && lowbitl < lowbitr) {
ans -= c[l];
l -= lowbitl;
} else {
ans += c[r];
r -= lowbitr;
}
}
return ans;
}
static void init() {
for (int i = 1; i <= n; ++i) {
c[i] += a[i];
int father = i + (i & -i);
if (father <= n) c[father] += c[i];
}
}
public static void main(String[] args) throws IOException {
PrintWriter out = new PrintWriter(System.out);
n = get();
int m = get();
a = new int[n + 1];
c = new int[n + 1];
for (int i = 1; i <= n; ++i) a[i] = get();
init();
while (m-- != 0) {
int command = get(), x = get(), y = get();
if (command == 1) {
add(x, y);
} else {
out.println(queryRange(x, y));
}
}
out.close();
}
}
要点总结
-
注意树状数组的树型特征
-
的管辖元素个数为 ,管辖区间为 -
树状数组中,
的父节点编号为 -
树状数组的二叉查找树中,
的父节点(也即前缀区间)编号为 -
树状数组是一个维护前缀信息的树型数据结构
-
树状数组维护的信息需要满足结合律以及可差分(因为一些数据需要通过其他数据的差集获得)两个性质,如加法,乘法,异或等
结合律:
,其中 是一个二元运算符。可差分:具有逆运算的运算,即已知
和 可以求出 -
有时树状数组在其他辅助数组(如差分数组)的帮助下,可以解决更多的问题
-
由于树状数组需要逆运算抵消掉原运算(如加和减),而线段树只需要逐层拆分区间,在合并区间信息,并不需要抵消部分数值,所以说树状数组能解决的问题是线段树能解决的问题的子集
-
树状数组下标也可以从
开始,此时 的父节点编号为 , 的管辖元素个数为 ,管辖区间为 -
树状数组常常通过离散化这一技巧缩小数据范围来减少空间
-
树状数组是一种工具,许多问题可以借助这个工具解决,就如同滑动窗口可以借助双端队列解决一样
树状数组封装类
一个
class BIT {
int n;
int[] c;
// 请保证 a 的数据从下标 1 开始
public void init(int[] a) {
// assert(a.length > n);
for (int i = 1; i <= n; ++i) {
c[i] += a[i];
int father = i + (i & -i);
if (father <= n) c[father] += c[i];
}
}
public BIT(int _n) {
n = _n;
c = new int[n + 1];
}
// 请保证 a 的数据从下标 1 开始
public BIT(int[] a, int _n) {
this(_n);
init(a);
}
public void add(int i, int val) {
if (i > n) return;
for (; i <= n; i += i & -i) {
c[i] += val;
}
}
public int preSum(int i) {
int ans = 0;
for (; i != 0; i &= i - 1) ans += c[i];
return ans;
}
public int single(int i) {
int ans = c[i];
int lca = i & i - 1;
for (int j = i - 1; j > lca; j &= j - 1) {
ans -= c[j];
}
return ans;
}
public int range(int l, int r) {
int ans = 0;
--l;
while (l < r) {
// 左边层数低,左边向前跳
int lowbitl = l & -l, lowbitr = r & -r;
if (l != 0 && lowbitl < lowbitr) {
ans -= c[l];
l -= lowbitl;
} else {
ans += c[r];
r -= lowbitr;
}
}
return ans;
}
}
进阶
区间修改+单点查询
一些操作映射到前缀数组或者差分数组上可能会变得很简单
考虑序列
则对于序列
因此
import java.io.*;
public class Main {
static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static int get() throws IOException {
in.nextToken();
return (int) in.nval;
}
static int n;
static int[] d, a;
static void add(int x, int val) {
for (; x <= n; x += x & -x) {
d[x] += val;
}
}
static int getSum(int x) {
int ans = 0;
for (; x != 0; x &= x - 1) ans += d[x];
return ans;
}
public static void main(String[] args) throws IOException {
PrintWriter out = new PrintWriter(System.out);
n = get();
int m = get();
a = new int[n + 1];
d = new int[n + 1];
for (int i = 1; i <= n; ++i) a[i] = get();
// 初始化 c[i] 为 0,仅在 c 上差分,可以不用对 a 进行差分
while (m-- != 0) {
int command = get();
if (command == 1) {
int x = get(), y = get(), k = get();
if (k == 0) continue;
add(x, k);
if (y + 1 <= n) add(y + 1, -k);
} else {
int x = get();
out.println(getSum(x) + a[x]);
}
}
out.close();
}
}
区间修改+区间查询
对于区间查询
考虑序列
所以,前缀查询变为
上式可表述为下图蓝色部分面积
横着看看不出什么,但竖着看会发现每个数据加的个数与
也就是
又因为
因此需要用两个树状数组分别维护
-
用于维护
的树状数组,对于每次对 加 转化为 与 -
用于维护
的树状数组,对于每次对 加 转化为 与即在原来的基础上加上
与减去
import java.io.*;
public class Main {
static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static int get() throws IOException {
in.nextToken();
return (int) in.nval;
}
public static void main(String[] args) throws IOException {
PrintWriter out = new PrintWriter(System.out);
int n = get(), m = get();
int[] a = new int[n + 1];
for (int i = 1; i <= n; ++i) {
a[i] = get();
}
// 求前缀和
for (int i = 1; i <= n; ++i) {
a[i] += a[i - 1];
}
// 同样的,初始化为 0,仅在空数组上差分
BIT tree1 = new BIT(n), tree2 = new BIT(n);
while (m-- != 0) {
int command = get(), x = get(), y = get();
if (command == 1) {
long k = get();
tree1.add(x, k);
tree1.add(y + 1, -k);
tree2.add(x, x * k);
tree2.add(y + 1, -(y + 1) * k);
} else {
// A[1...y] 的和
long preY = a[y] + (y + 1) * tree1.preSum(y) - tree2.preSum(y);
// A[1...x-1] 的和
--x;
long preX = a[x] + (x + 1) * tree1.preSum(x) - tree2.preSum(x);
out.println(preY - preX);
}
}
out.close();
}
}
class BIT {
int n;
long[] c;
// 请保证 a 的数据从下标 1 开始
public void init(int[] a) {
// assert(a.length > n);
for (int i = 1; i <= n; ++i) {
c[i] += a[i];
int father = i + (i & -i);
if (father <= n) c[father] += c[i];
}
}
public BIT(int _n) {
n = _n;
c = new long[n + 1];
}
// 请保证 a 的数据从下标 1 开始
public BIT(int[] a, int _n) {
n = _n;
c = new long[n + 1];
init(a);
}
public void add(int i, long val) {
if (i > n) return;
for (; i <= n; i += i & -i) {
c[i] += val;
}
}
public long preSum(int i) {
long ans = 0;
for (; i != 0; i &= i - 1) ans += c[i];
return ans;
}
public long single(int i) {
long ans = c[i];
int lca = i & i - 1;
for (int j = i - 1; j > lca; j &= j - 1) {
ans -= c[j];
}
return ans;
}
public long range(int l, int r) {
long ans = 0;
--l;
while (l < r) {
// 左边层数低,左边向前跳
int lowbitl = l & -l, lowbitr = r & -r;
if (l != 0 && lowbitl < lowbitr) {
ans -= c[l];
l -= lowbitl;
} else {
ans += c[r];
r -= lowbitr;
}
}
return ans;
}
}
也可以写成封装类的形式
import java.io.*;
public class Main {
static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static int get() throws IOException {
in.nextToken();
return (int) in.nval;
}
public static void main(String[] args) throws IOException {
PrintWriter out = new PrintWriter(System.out);
int n = get(), m = get();
int[] a = new int[n + 1];
for (int i = 1; i <= n; ++i) {
a[i] = get();
}
// 求前缀和
for (int i = 1; i <= n; ++i) {
a[i] += a[i - 1];
}
ExBIT tree = new ExBIT(n);
while (m-- != 0) {
int command = get(), x = get(), y = get();
if (command == 1) {
tree.add(x, y, get());
} else {
out.println(a[y] - a[x - 1] + tree.range(x, y));
}
}
out.close();
}
}
class BIT {
int n;
long[] c;
// 请保证 a 的数据从下标 1 开始
public void init(int[] a) {
// assert(a.length > n);
for (int i = 1; i <= n; ++i) {
c[i] += a[i];
int father = i + (i & -i);
if (father <= n) c[father] += c[i];
}
}
public BIT(int _n) {
n = _n;
c = new long[n + 1];
}
// 请保证 a 的数据从下标 1 开始
public BIT(int[] a, int _n) {
n = _n;
c = new long[n + 1];
init(a);
}
public void add(int i, long val) {
if (i > n) return;
for (; i <= n; i += i & -i) {
c[i] += val;
}
}
public long preSum(int i) {
long ans = 0;
for (; i != 0; i &= i - 1) ans += c[i];
return ans;
}
public long single(int i) {
long ans = c[i];
int lca = i & i - 1;
for (int j = i - 1; j > lca; j &= j - 1) {
ans -= c[j];
}
return ans;
}
public long range(int l, int r) {
long ans = 0;
--l;
while (l < r) {
// 左边层数低,左边向前跳
int lowbitl = l & -l, lowbitr = r & -r;
if (l != 0 && lowbitl < lowbitr) {
ans -= c[l];
l -= lowbitl;
} else {
ans += c[r];
r -= lowbitr;
}
}
return ans;
}
}
// 差分增量
class ExBIT {
int n;
BIT tree1, tree2;
public ExBIT(int _n) {
n = _n;
tree1 = new BIT(_n);
tree2 = new BIT(_n);
}
// 区间加对应差分数组的 两个端点操作
public void add(int l, int r, long k) {
tree1.add(l, k);
tree1.add(r + 1, -k);
tree2.add(l, l * k);
tree2.add(r + 1, -(r + 1) * k);
}
// 差分增量的前缀和
public long preSum(int i) {
return (i + 1) * tree1.preSum(i) - tree2.preSum(i);
}
// 差分增量的区间和
public long range(int l, int r) {
return preSum(r) - preSum(l - 1);
}
}
题目
P4939 Agent2 - 洛谷
题意简述:有两个操作
- 对区间
的数均加 - 查询第
个数的值
进阶中的 区间修改+单点查询
import java.io.*;
public class Main {
static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static int get() throws IOException {
in.nextToken();
return (int) in.nval;
}
static int n;
static int[] d;
static void add(int x, int val) {
for (; x <= n; x += x & -x) {
d[x] += val;
}
}
static int getSum(int x) {
int ans = 0;
for (; x != 0; x &= x - 1) ans += d[x];
return ans;
}
public static void main(String[] args) throws IOException {
PrintWriter out = new PrintWriter(System.out);
n = get();
int m = get();
d = new int[n + 1];
while (m-- != 0) {
int command = get();
if (command == 0) {
int x = get(), y = get();
add(x, 1);
if (y + 1 <= n) add(y + 1, -1);
} else {
int x = get();
out.println(getSum(x));
}
}
out.close();
}
}
P5057 简单题 - 洛谷
题目简述:有两个操作
- 对区间
的数进行反转(1变0,0变1) - 单点查询
反转等同于与
而异或也满足树状数组的两个要求,因此使用树状数组解决该题
import java.io.*;
public class Main {
static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static int get() throws IOException {
in.nextToken();
return (int) in.nval;
}
static int n, m;
static int[] c;
static void change(int x) {
for (; x <= n; x += x & -x) c[x] ^= 1;
}
static int askPre(int x) {
int ans = 0;
for (; x != 0; x &= x - 1) ans ^= c[x];
return ans;
}
public static void main(String[] args) throws IOException {
PrintWriter out = new PrintWriter(System.out);
n = get();
m = get();
c = new int[n + 1];
while (m-- != 0) {
int command = get();
if (command == 1) {
int l = get(), r = get();
change(l);
if (r < n) change(r + 1);
} else {
out.println(askPre(get()));
}
}
out.close();
}
}
P1908 逆序对 - 洛谷
题意简述:求数组中的逆序对
求解逆序对可以用归并排序求解,此处不做讨论
从前向后遍历数组,同时将其加入到桶中,记录每个数出现的个数,并加上该位置之前且比当前数大的数的个数(有点绕,看例子可能会清晰点)
桶:用
数组: 1 3 5 4 2 1 桶的下标: 0 1 2 3 4 5 6
一: 加入 1 到桶中 ans+=cnt[2...max] ans=0 桶: 0 1 0 0 0 0 0
二: 加入 3 到桶中 ans+=cnt[4...max] ans=0 桶: 0 1 0 1 0 0 0
三: 加入 5 到桶中 ans+=cnt[6...max] ans=0 桶: 0 1 0 1 0 1 0
四: 加入 4 到桶中 ans+=cnt[5...max] ans=1 桶: 0 1 0 1 1 1 0
五: 加入 2 到桶中 ans+=cnt[3...max] ans=4 桶: 0 1 1 1 1 1 0
六: 加入 1 到桶中 ans+=cnt[2...max] ans=8 桶: 0 2 1 1 1 1 0
也就是需要求
也即实现 单点加 与 区间查询,使用树状数组求解
但是题目中
可以发现,该题中我们只关心数据间的相对大小关系,而不关心数据本身大小,采用离散化的方式,将数据缩小(一种映射关系)
举个例子:
原数据: 1 100 200 500 50
新数据: 1 3 4 5 2
这样最大的数据就缩小到了 5
代码如下:
import java.io.*;
import java.util.Arrays;
public class Main {
static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static int get() throws IOException {
in.nextToken();
return (int) in.nval;
}
static int n;
static int[] d;
static void add(int x, int val) {
for (; x <= n; x += x & -x) {
d[x] += val;
}
}
static int getSum(int x) {
int ans = 0;
for (; x != 0; x &= x - 1) ans += d[x];
return ans;
}
// 离散化
static void lis(int[] a, int n) {
int[] temp = new int[n];
System.arraycopy(a, 0, temp, 0, n);
Arrays.sort(temp);
for (int i = 0; i < n; ++i) {
a[i] = Arrays.binarySearch(temp, a[i]) + 1;
}
}
public static void main(String[] args) throws IOException {
PrintWriter out = new PrintWriter(System.out);
n = get();
int[] num = new int[n];
for (int i = 0; i < n; ++i) num[i] = get();
lis(num, n);
d = new int[n + 1];
long ans = 0;
for (int i = 0; i < n; ++i) {
add(num[i], 1);
ans += i - getSum(num[i]) + 1;
}
out.println(ans);
out.close();
}
}
315. 计算右侧小于当前元素的个数 - 力扣
同样是求逆序对,但是需要求的是每个位置的逆序对数量,就不能像上面那样顺序遍历了
class Solution {
int n;
void lis(int[] a) {
int[] temp = new int[n];
System.arraycopy(a, 0, temp, 0, n);
Arrays.sort(temp);
for (int i = 0; i < n; ++i) {
a[i] = Arrays.binarySearch(temp, a[i]) + 1;
}
}
int[] c;
void add(int i) {
for (; i <= n; i += i & -i) {
++c[i];
}
}
int getSum(int i) {
int ans = 0;
for (; i != 0; i &= i - 1) {
ans += c[i];
}
return ans;
}
public List<Integer> countSmaller(int[] nums) {
n = nums.length;
c = new int[n + 1];
lis(nums);
List<Integer> ans = new ArrayList<>(n);
for (int i = n - 1; i >= 0; --i) {
add(nums[i]);
ans.add(getSum(nums[i] - 1));
}
Collections.reverse(ans);
return ans;
}
}
307. 区域和检索 - 数组可修改 - 力扣
题意简述:有两个操作
- 单点赋值
- 区间和查询
单点赋值可以改为单点查询+单点修改(查询这个值再减去这个值)
当然也可以多开一个
class NumArray {
int n;
BIT tree;
int[] nums;
public NumArray(int[] _nums) {
n = _nums.length;
nums = _nums;
tree = new BIT(nums, n);
}
public void update(int index, int val) {
tree.add(index + 1, val - nums[index]);
nums[index] = val;
}
public int sumRange(int left, int right) {
return tree.range(left + 1, right + 1);
}
}
class BIT {
int n;
int[] c;
public void init(int[] a) {
// assert(a.length > n);
for (int i = 1; i <= n; ++i) {
c[i] += a[i - 1];
int father = i + (i & -i);
if (father <= n) c[father] += c[i];
}
}
public BIT(int _n) {
n = _n;
c = new int[n + 1];
}
public BIT(int[] a, int _n) {
this(_n);
init(a);
}
public void add(int i, int val) {
if (i > n) return;
for (; i <= n; i += i & -i) {
c[i] += val;
}
}
public int preSum(int i) {
int ans = 0;
for (; i != 0; i &= i - 1) ans += c[i];
return ans;
}
public int single(int i) {
int ans = c[i];
int lca = i & i - 1;
for (int j = i - 1; j > lca; j &= j - 1) {
ans -= c[j];
}
return ans;
}
public int range(int l, int r) {
int ans = 0;
--l;
while (l < r) {
// 左边层数低,左边向前跳
int lowbitl = l & -l, lowbitr = r & -r;
if (l != 0 && lowbitl < lowbitr) {
ans -= c[l];
l -= lowbitl;
} else {
ans += c[r];
r -= lowbitr;
}
}
return ans;
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端
· DeepSeek本地性能调优
· 一文掌握DeepSeek本地部署+Page Assist浏览器插件+C#接口调用+局域网访问!全攻略