概念
基本结构以及建树

这样的一棵树应该是这样的:
我们使用一个数组对树形数据进行存储,因此可以注意到一个关键点,根节点与叶子节点的下标关系(注意这个方便的计算规律来自下标从1开始):
struct SegmentTree<T> {inner: Vec<T>,data: Vec<T>}impl<T> Debug for SegmentTree<T>where T: Debug{fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {f.write_fmt(format_args!("从原数组{:?}\n获取到的线段树结果为:{:?}",self.data, self.inner))}}impl<T> SegmentTree<T>where T : Copy + Ord + PartialEq + Default + Add<Output=T>{pub fn new(data: Vec<T>) -> SegmentTree<T> {Self {// 这个初始化的数组的大小足够应付10^5个数据inner: vec![T::default(); data.len() * 3],data}}pub fn build(&mut self) {if self.data.len() == 0 { return; }self.build_section(0, self.data.len()-1, 1);}/// 线段树建树,对 [s..t] 区间建立线段树,当前根的编号为 ppub fn build_section(&mut self, s: usize, t: usize, p: usize) {if self.data.len() == 0 { return; }// 递归结束条件,根节点时,节点值为原值if s == t { self.inner[p] = self.data[s]; }else {let mid = s + ((t - s) >> 1);// 后序遍历self.build_section(s, mid, p*2);self.build_section(mid+1, t, p*2+1);// 每个节点的值为左右节点值之和self.inner[p] = self.inner[p*2] + self.inner[p*2+1];}}}
区间修改
参考资料知乎
如果要求修改区间 [l..=r],把所有包含在区间 [l..=r]中的节点都遍历一次、修改一次,时间复杂度无法承受。我们这里要引入一个叫做 「懒惰标记」 的东西。
懒标记是线段树的精髓所在。对于区间修改,朴素的想法是用递归的方式一层层修改(类似于线段树的建立),但这样的时间复杂度比较高。使用懒标记后,对于那些正好是线段树节点的区间,我们不继续递归下去,而是打上一个标记,将来要用到它的子区间的时候,再向下传递。


在具体编写代码的时候存在三种情况,先放一个完整的图在这里,注意我们使用线段树是为了区间查询,因此需要有节点与对应区间进行匹配的过程:

第二步是剪枝的关键,当我们找到一个完全在目标区间内的区间的时候,才是打上懒标记,然后节省继续往下递归的开销:
第三步比较麻烦,需要拆解区间

这样就可以看代码了:
pub fn query(&mut self, l: usize, r: usize) -> i32 {self.query_section(l, r, 1, 1, self.data.len())}/// 区间查询的实现,[l..=r] 区间查询和,[cl..=cr]为当前区间,p为此时根节点pub fn query_section(&mut self, l: usize, r: usize, p: usize, cl: usize, cr: usize) -> i32 {return if cl > r || cr < l { 0 }else if cl >= l && cr <= r {self.inner[p]} else {let mid = cl + ((cr - cl) >> 1);self.push_down(p, (cr - cl + 1) as i32);self.query_section(l, r, p * 2, cl, mid)+ self.query_section(l, r, p * 2 + 1, mid + 1, cr)}}pub fn update(&mut self, l: usize, r: usize, d: i32) {self.update_section(l, r, 1, d, 1, self.data.len());}/// 区间更新的实现,[l..=r] 区间中更新加上值 d,[cl..=cr]为当前区间,p为此时根节点pub fn update_section(&mut self, l: usize, r: usize, p: usize, d: i32, cl: usize, cr: usize) {// 剪枝,如果当前区间与更新区间无交集if cl>r || cr<l { return; }else if cl >= l && cr <= r {// 第二种情况,当前区间在更新区间内部,我们不继续往下,而是将此最大区间进行更新,并使用懒标记// 因为是一整个区间都加上了d,因此这里需要的是乘以区间长度self.inner[p] += (cr - cl + 1) as i32 * d;// 注意,非叶子节点才进行懒标记,而且只是标记d,也即区间中每个元素的更新值if cr > cl {self.marker[p] += d;}}else {// 两个区间相交但不包含,这时把当前区间一分为二,分别进行处理。// 如果存在懒标记,要先把懒标记传递给子节点let mid = cl + ((cr - cl) >> 1);// 懒标记传递给子节点self.push_down(p, (cr - cl + 1) as i32);self.update_section(l, r, p*2, d, cl, mid);self.update_section(l, r, p*2+1, d , mid+1, cr);// 因为是传递下去到最大区间,那么回溯的时候需要根据子节点更新当前节点的值self.inner[p] = self.inner[p*2] + self.inner[p*2+1];}}/// 标记往下传递一层fn push_down(&mut self, p: usize, len: i32) {// 懒标记传递给子节点,就一层self.marker[p*2] += self.marker[p];self.marker[p*2+1] += self.marker[p];// 同时更新子节点的值,需要考虑区间长度,因为我们标记记录的是d,但是区间更新// 需要考虑多个dself.inner[p*2] += self.marker[p] * (len - len/2);self.inner[p*2+1] += self.marker[p] * (len / 2);self.marker[p] = 0;}
测试代码:
#[test]
fn test_update_query() {
let mut tree = SegmentTree::new(vec![1,2,3,4,5]);
tree.build();
println!("新建树为:\n{:?}", tree);
let i = tree.query(1, 4);
println!("{}", i);
tree.update(1, 4, 1);
println!("{:?}", tree);
let i = tree.query(1, 4);
println!("{}", i);
}
动态开点的线段树
933. 最近的请求次数
三叶的题解中提到了这种做法,但是不太看得懂
