算法归纳4-前缀和/差分/树状数组/线段树
1,对比
- https://blog.csdn.net/honghuidan/article/details/77527808
- 两者相同点:单点/区间修改,区间查询
- 区间查询:前缀和
- 区间修改,单点查询:差分
- 单点修改,区间查询:树状数组,线段树
- 区间修改,区间查询:线段树+懒标记
- 不同点:
- 树状数组只能维护前缀操作和(前缀和,前缀积,前缀最大最小),而线段树可以维护区间操作和。
- 树状数组:
- 某些操作是存在逆元的,这样就给人一种树状数组可以维护区间信息的错觉:维护区间和,模质数意义下的区间乘积,区间xor和。能这样做的本质是取右端点的前缀和,然后对左端点左边的前缀和的逆元做一次操作,所以树状数组的区间询问其实是在两次前缀和询问。
- 所以我们能看到树状数组能维护一些操作的区间信息但维护不了另一些的:最大/最小值,模非质数意义下的乘法,原因在于这些操作不存在逆元,所以就没法用两个前缀和做区间查询。
- 线段树
- 线段树就不一样了,线段树直接维护的就是区间信息,所以一切满足结合律的操作都能维护区间和,并且lazy标记的存在还能使线段树能够支持区间修改,这点是树状数组做不到的。
- 可以说树状数组能做的事情其实是线段树的一个子集,大多数情况下使用树状数组真的只是因为它好写并且常数小而已。
- 不过随着zkw线段树的普及,树状数组仅有的两点优势也不复存在了……估计要成为时泪了吧。
2,前缀和
3,差分
4,树状数组
5,线段树
- https://leetcode.cn/problems/my-calendar-iii/solution/xian-duan-shu-by-xiaohu9527-rfzj/
- 线段树详解「汇总级别整理 🔥🔥🔥」
- 懒标记:将节点的值加一,表示节点所表示的区间内的所有元素都加了一,而不是去具体修改每一个元素值。(例如:节点表示区间最大值的时候就很好用)
- 懒标记使得线段树支持区间修改。
- 线段树将更新和查询的时间复杂度都变为O(logn)
- 线段树的结构:
- 基于原数组,生成一个新的
数组表示线段树的结构
- 树的
根节点在新数组index=1
处 - 新数组中所有节点(索引为index)的左右子节点分别为
2*index,2*index+1
- 原数组中的每一个元素在线段树数组中都是叶子节点,线段树数组中的其他节点(非叶子节点)表示都是原数组的一段区间和,根节点存储整个原数组的和(这里有两个数组,一个原数组,一个基于原数组生成的线段树数组)
- 基于原数组,生成一个新的
- 查询操作
- 根据查询区间,从新数组的根结点(index=1)出发,递归地向两边扩展(
2*index, 2*index+1
),在子树中找到合适的区间和返回,时间复杂度O(logn)
- 根据查询区间,从新数组的根结点(index=1)出发,递归地向两边扩展(
- 更新操作
- 根据修改的节点,从新数组对应的叶子节点出发,一直更新其父节点直到更新根节点,时间复杂度O(logn)
- 离散化:
- 如果一道题仅仅是「值域很大」的离线题(提前知晓所有的询问),我们还能通过「离散化」来进行处理,将值域映射到一个小空间去,从而解决 MLE(爆内存) 问题。
- 动态开点:
- 729. 我的日程安排表 I
- 有时由于「强制在线」的原因,我们无法进行「离散化」,同时值域很大,因此如果我们想要使用「线段树」进行求解,只能采取「动态开点」的方式进行。
- 动态开点的优势在于,不需要事前构造空树,而是在插入操作
add
和查询操作query
时根据访问需要进行「开点」操作。由于我们不保证查询和插入都是连续的,因此对于父节点u
而言,我们不能通过u << 1
和u << 1 | 1
的固定方式进行访问,而要将节点tr[u]
的左右节点所在tr
数组的下标进行存储,分别记为ls
和rs
属性。对于tr[u].ls = 0
和tr[u].rs = 0
则是代表子节点尚未被创建,当需要访问到它们,而又尚未创建的时候,则将其进行创建。 - 非动态开点的线段树数组大小为
4*N
,动态开点的线段树数组大小为6*M*logN
- 代码实例1:
//没有懒标记,原数组单点修改,区间查询
//包含操作:生成线段树、原数组单点修改、区间查询
import java.util.Arrays;
public class test {
/**
* 基于原数组生成线段树数组
* @param arr 原数组
* @param tree 生成的线段树数组
* @param start 当前阶段处理的原数组起始索引(区间和起始索引)
* @param end 当前阶段处理的原数组结束索引(区间和结束索引)
* @param node_idx 与[start, end]相对应的区间和在线段树数组中的索引
*/
public static void build_tree(int[] arr, int[] tree, int start, int end, int node_idx){
if(start==end){
//此时到达叶子节点
tree[node_idx] = arr[start];
return;
}
int leftNode_idx = 2*node_idx;
int rightNode_idx = 2*node_idx+1;
int middle = (start+end)/2;
build_tree(arr, tree, start, middle, leftNode_idx);
build_tree(arr, tree, middle+1, end, rightNode_idx);
tree[node_idx] = tree[leftNode_idx]+tree[rightNode_idx]; //后序
}
/**
* 由线段树数组查询得到原数组区间和
* @param arr 原数组
* @param tree 线段树数组
* @param start 当前阶段处理的原数组起始索引
* @param end 当前阶段处理的原数组结束索引
* @param l 要查询的区间和的原数组起始索引
* @param r 要查询的区间和的原数组结束索引
* @param node_idx 与[start, end]相对应的区间和在线段树数组中的索引
* @return
*/
public static int query(int[] arr, int[] tree, int start, int end, int l, int r, int node_idx){
if(l>end||r<start){
//要查询的区间超出了当前区间的范围
return 0;
}else if(l<=start && end<=r){
//要查询的区间覆盖了当前的区间范围
return tree[node_idx];
}else{
int leftNode_idx = node_idx*2;
int rightNode_idx = node_idx*2+1;
int middle = (start+end)/2;
int left_sum = query(arr, tree, start, middle, l, r, leftNode_idx);
int right_sum = query(arr, tree, middle+1, end, l, r, rightNode_idx);
return left_sum+right_sum;
}
}
/**
* 线段树单点修改-和建树很像
* @param arr 原数组
* @param tree 线段树数组
* @param start 当前阶段处理的原数组起始索引
* @param end 当前阶段处理的原数组结束索引
* @param node_idx 与[start, end]相对应的区间和在线段树数组中的索引
* @param update_idx 要修改的元素在原数组中的索引
* @param val 原数组元素修改后的值
*/
public static void update(int[] arr, int[] tree, int start, int end, int node_idx, int update_idx, int val){
if(start==end){
//修改点即可
arr[start] = tree[node_idx] = val;
}else{
int leftNode_idx = node_idx*2;
int rightNode_idx = node_idx*2+1;
int middle = (start+end)/2;
if(update_idx>middle){
//要更新的值在右子树
update(arr, tree, middle+1, end, rightNode_idx, update_idx, val);
}else{
//要更新的值在左子树
update(arr, tree, start, middle, leftNode_idx, update_idx, val);
}
tree[node_idx] = tree[leftNode_idx]+tree[rightNode_idx];
}
}
public static void main(String[] args) {
int[] a = {10, 11, 12, 13, 14, 15};
//生成线段树
int[] tree = new int[a.length*4];
build_tree(a, tree, 0, a.length-1, 1);
System.out.println("生成的线段树为"+Arrays.toString(tree));
//区间查询
int l=0;
int r=a.length-1;
System.out.println(query(a, tree, 0, a.length-1, l, r, 1));
//区间修改
update(a, tree, 0, a.length-1, 1, a.length-1, 20);
System.out.println("修改后的线段树为"+Arrays.toString(tree));
System.out.println(query(a, tree, 0, a.length-1, l, r, 1));
}
}
- 代码实例2:
307. 区域和检索 - 数组可修改
class NumArray {
//基本操作0-基础类
class Node {
int l,r,v;
public Node(int l, int r){
this.l=l;
this.r=r;
}
}
Node[] tr;
//基本操作1-线段树创建
void build(int u, int l, int r){
//u,l,r分别为1,1,原数组长度
tr[u] = new Node(l, r);
if(l==r) return;
int mid = l+r >> 1;
build(u<<1, l, mid);
build(u<<1|1, mid+1, r);
}
//基本操作2-原数组单点更新后线段树修改
void update(int u, int x, int v){
//u,x,v分别为1,index+1,原数组index索引处对应的值
if(tr[u].l==x && tr[u].r==x){
tr[u].v += v;
return;
}
int mid = tr[u].l + tr[u].r >> 1;
if(x<=mid){
update(u<<1, x, v);
}else{
update(u<<1|1, x, v);
}
pushup(u);
}
void pushup(int u){
tr[u].v = tr[u<<1].v + tr[u<<1|1].v;
}
//基本操作3-基于线段树对原数组进行区间查询
int query(int u, int l, int r){
//u,l,r分别为1,查询索引left+1,查询索引right+1
if(l<=tr[u].l && tr[u].r<=r){
return tr[u].v;
}
int mid = tr[u].l + tr[u].r >> 1;
int ans = 0;
if(l<=mid){
ans+=query(u<<1, l, r);
}
if(r>mid){
ans+=query(u<<1|1, l, r);
}
return ans;
}
//使用1-线段树初始化
int[] nums;
public NumArray(int[] _nums) {
nums = _nums;
int n = nums.length;
tr = new Node[n*4];
build(1, 1, n);
for(int i=0; i<n; i++){
update(1, i+1, nums[i]);
}
}
//使用2-原数组单点修改
public void update(int index, int val) {
update(1, index+1, val-nums[index]);
nums[index] = val;
}
//使用3-原数组区间查询
public int sumRange(int left, int right) {
return query(1, left+1, right+1);
}
}
代码示例3:(线段树+懒标记+动态开点)
- 729. 我的日程安排表 I
-
import java.util.Arrays; class MyCalendar { class Node{ // val 表示当前节点的总预约数 // ls 表示当前节点的左节点在tr的下标 // rs 表示当前节点的右节点在tr的下标(由于动态开点并没有将所有的点都保存在tr中,所以左右节点2*n+1 2*n+2的关系就不一定存在了) // add 懒标记 int val, ls, rs, add; public String toString(){ return ls+" "+rs+" "+val; } } int N; int M; int cnt; Node[] tr; public MyCalendar() { N = (int)1e9; //实际上总的叶子节点数目 M = 144000; cnt = 1; //记录下一个节点在tr中的索引,第一个点是根节点,索引为0,ls为0,rs为1e9 tr = new Node[M]; //如果是非动态开点就是4*N,这种可以用下标表示父子关系;这里是动态开点,父子关系要保留在Node所包含的变量中 } void updata(int u, int lt, int rt, int l, int r, int v){ if(l<=lt && rt<=r){ tr[u].val += (rt-lt+1)*v; tr[u].add += v; return; } lazyBuild(u); pushDown(u, rt-lt+1); int m = (lt+rt)/2; if(m>=l){ updata(tr[u].ls, lt, m, l, r, v); } if(m<r){ updata(tr[u].rs, m+1, rt, l, r, v); } pushUp(u); } int query(int u, int lt, int rt, int l, int r){ if(l<=lt && rt<=r){ return tr[u].val; } lazyBuild(u); pushDown(u, (rt-lt+1)); int m = (lt+rt)/2; int ans = 0; if(l<=m){ ans = query(tr[u].ls, lt, m, l, r); } if(r>m){ ans+=query(tr[u].rs, m+1, rt, l, r); } return ans; } void lazyBuild(int u){ if(tr[u]==null){ tr[u] = new Node(); } if(tr[u].ls==0){ tr[u].ls = ++cnt; tr[tr[u].ls] = new Node(); } if(tr[u].rs==0){ tr[u].rs = ++cnt; tr[tr[u].rs] = new Node(); } } void pushDown(int u, int len){ tr[tr[u].ls].add += tr[u].add; tr[tr[u].rs].add += tr[u].add; tr[tr[u].ls].val += (len-len/2)*tr[u].add; tr[tr[u].rs].val += len/2*tr[u].add; tr[u].add=0; } void pushUp(int u){ tr[u].val = tr[tr[u].ls].val+tr[tr[u].rs].val; } public boolean book(int start, int end) { if(query(0, 0, N, start, end-1)>0){ return false; }else{ updata(0, 0, N, start, end-1, 1); return true; } } }
- 例题:
6-计算middle用移位操作
-1 >> 1; //结果是-1
-1/2; //结果是0,判断的时候有时会进入死循环
https://leetcode.cn/problems/count-of-range-sum/submissions/
行动是治愈恐惧的良药,而犹豫拖延将不断滋养恐惧。