每日一题做题记录,参考官方和三叶的题解 |
题目要求
思路一:树状数组
需实现单点修改和区间求和两个功能,所以想到使用树状数组。
树状数组(二叉索引树,Binary Indexed Tree)
- 学习参考链接
- 初衷是用于解决数据压缩里的累计频率的计算问题,Cumulative Frequency;
- 现用于高效计算前缀和or区间和,且支持在 O ( log n ) O(\log n) O(logn)时间内修改单点值;
- 利用二进制划分区间,每个节点的值都是其所有子节点值的总和,也即将所划分的区间上界索引处的值更新为区间所有值之和;
- 涉及两个基本计算,单点增加和前缀和查询,复杂度均为 O ( log n ) O(\log n) O(logn)。
方法 | 功能 |
---|---|
lowbit(key) | 低位运算,计算非负整数 k e y key key在二进制表示下最低位的 1 1 1与其后所有的 0 0 0所构成的数值 |
add(idx, val) | 单点增加,将 i d x idx idx处的值增加 v a l val val(并更新其他节点) |
query(key) | 查询前缀和,查询序列前 k e y key key项和,即 log n \log n logn个区间的总和 |
Java
class NumArray {
//树状数组定义
int[] tr;
int lowbit(int x) {
return x & -x;
}
void add(int x, int u) {
for(int i = x; i <= n; i += lowbit(i))
tr[i] += u;
}
int query(int x) {
int res = 0;
for(int i = x; i > 0; i -= lowbit(i))
res += tr[i];
return res;
}
int[] num;
int n;
public NumArray(int[] nums) {
num = nums;
n = num.length;
tr = new int[n + 10];
for(int i = 0; i < n; i++)
add(i + 1, num[i]);
}
public void update(int index, int val) {
add(index + 1, val - num[index]); //更新各父节点
num[index] = val; //修改目标
}
public int sumRange(int left, int right) {
return query(right + 1) - query(left); //前缀和相减
}
}
- 时间复杂度:插入和查询复杂度均为 O ( log n ) O(\log n) O(logn)
- 空间复杂度: O ( n ) O(n) O(n)
C++
【基本和Java一样,除了一小点vector
的处理】
class NumArray {
public:
//树状数组定义
vector<int> tr;
int lowbit(int x) {
return x & -x;
}
void add(int x, int u) {
for(int i = x; i <= n; i += lowbit(i))
tr[i] += u;
}
int query(int x) {
int res = 0;
for(int i = x; i > 0; i -= lowbit(i))
res += tr[i];
return res;
}
vector<int> num;
int n;
NumArray(vector<int>& nums) {
num = nums;
n = num.size();
tr.resize(n + 10);
for(int i = 0; i < n; i++)
add(i + 1, num[i]);
}
void update(int index, int val) {
add(index + 1, val - num[index]); //更新各父节点
num[index] = val; //修改目标
}
int sumRange(int left, int right) {
return query(right + 1) - query(left); //前缀和相减
}
};
- 时间复杂度:插入和查询复杂度均为 O ( log n ) O(\log n) O(logn)
- 空间复杂度: O ( n ) O(n) O(n)
思路二:线段树
一个比树状数组更🐂更通用的方法。
线段树
- 学习参考链接
- 一种维护区间信息的数据结构,可在 O ( log n ) O(\log n) O(logn)时间内实现单点修改、区间修改、区间查询(包括求和、最值)等操作;
- 基本思想就是划分区间最终将线段划分为一个树形结构,通过合并相应区间来处理区间信息;
- 懒惰标记:(一个本题不需要的思想),通过延迟对节点信息的修改来减少不必要的操作次数,仅标记其被更改但不更新,下一次再访问到的时候再进行修改,用在
pushdown()
方法里,即从父辈节点开始向下更新值。
方法 | 功能 |
---|---|
pushup(sta) | 更新 s t a sta sta的所有父辈节点(由子向上) |
build(sta, l, r) | 从编号 s t a sta sta的节点开始构造范围为 [ l , r ] [l,r] [l,r]的树节点 |
update(sta, idx, val) | 从编号 s t a sta sta的节点开始,在 i d x idx idx处的值增加 v a l val val |
query(sta, l, r) | 从编号 s t a sta sta的节点开始,查询 [ l , r ] [l,r] [l,r]的区间和 |
Java
class NumArray {
Node[] tr;
class Node {
int l, r, v;
Node(int _l, int _r) {
l = _l;
r = _r;
}
}
void build(int sta, int l, int r) {
tr[sta] = new Node(l, r);
if(l == r)
return;
//递归构造
int m = l + r >> 1;
build(sta << 1, l, m);
build(sta << 1 | 1, m + 1, r);
}
void pushup(int sta) {
tr[sta].v = tr[sta << 1].v + tr[sta << 1 | 1].v;
}
void update(int sta, int idx, int val) {
if(tr[sta].l == idx && tr[sta].r == idx) {
tr[sta].v += val;
return;
}
int m = tr[sta].l + tr[sta].r >> 1;
if(idx <= m)
update(sta << 1, idx, val);
else
update(sta << 1 | 1, idx, val);
pushup(sta);
}
int query(int sta, int l, int r) {
if(l <= tr[sta].l && tr[sta].r <= r)
return tr[sta].v;
int m = tr[sta].l + tr[sta].r >> 1;
int res = 0;
if(l <= m)
res += query(sta << 1, l, r);
if(r > m)
res += query(sta << 1 | 1, l, r);
return res;
}
int[] num;
public NumArray(int[] nums) {
num = nums;
int n = num.length;
tr = new Node[n * 4];
build(1, 1, n);
for(int i = 0; i < n; i++)
update(1, i + 1, num[i]);
}
public void update(int index, int val) {
update(1, index + 1, val - num[index]);//更新各父节点
num[index] = val; //修改目标
}
public int sumRange(int left, int right) {
return query(1, left + 1, right + 1);
}
}
- 时间复杂度:插入和查询复杂度均为 O ( log n ) O(\log n) O(logn)
- 空间复杂度: O ( n ) O(n) O(n)
C++
这个地方是有点复杂的,没有定义新的类型,靠多维护几个参数解决。
class NumArray {
private:
vector<int> tr;
int n;
void build(int sta, int l, int r, vector<int> &nums) {
if(l == r) {
return;
}
int m = (l + r) >> 1;
build(sta << 1, l, m, nums);
build(sta << 1 | 1, m + 1, r, nums);
}
void pushup(int sta) {
tr[sta] = tr[sta << 1] + tr[sta << 1 | 1];
}
void update(int sta, int l, int r, int idx, int val) {
if(l == idx && r == idx){
tr[sta] += val;
return;
}
int m = (l + r) >> 1;
if(idx <= m)
update(sta << 1, l, m, idx, val);
else
update(sta << 1 | 1, m + 1, r, idx, val);
pushup(sta);
}
int query(int sta, int l, int r, int left, int right) {
if(left <= l && right >= r)
return tr[sta];
int m = (l + r) >> 1;
int res = 0;
if(left <= m)
res += query(sta << 1, l, m, left, right);
if(right > m)
res += query(sta << 1 | 1, m + 1, r, left, right);
return res;
}
public:
vector<int> num;
NumArray(vector<int> &nums) :tr(nums.size() * 4), n(nums.size()) {
num = nums;
build(1, 1, n, num);
for(int i = 0; i < n; i++)
update(1, 1, n, i + 1, num[i]);
}
void update(int index, int val) {
update(1, 1, n, index + 1, val - num[index]); //更新各父节点
num[index] = val; //修改目标
}
int sumRange(int left, int right) {
return query(1, 1, n, left + 1, right + 1);
}
};
- 时间复杂度:插入和查询复杂度均为 O ( log n ) O(\log n) O(logn)
- 空间复杂度: O ( n ) O(n) O(n)
总结
看起来很简单的题目,结果完全触及知识盲区,get了两个处理区间的数据结构,然后三叶姐姐总结对于区间类问题考虑问题的方向:
1.简单求区间和,用「前缀和」
2.多次将某个区间变成同一个数,用「线段树」
3.其他情况,用「树状数组」
另:
题目要求 | 前缀和 | 树状数组 | 差分 | 线段树 | 备注 |
---|---|---|---|---|---|
数组不变,求区间和 | √ | √ | √ | ||
多次修改某个数(单点),求区间和 | √ | √ | 本题符合 | ||
多次修改某个区间,输出最终结果 | √ | ||||
多次修改某个区间, 求区间和 | √ | √ | 根据修改区间范围大小选择 | ||
多次将某个区间变成同一个数, 求区间和 | √ | √ | 根据修改区间范围大小选择 |
欢迎指正与讨论! |