什么是线段树
线段树的思想有点类似于树状数组,都是降低区间和等的操作复杂度。
对于一个正常的序列,很难维护对区间信息,如一个区间的和,区间的最值等问题。但是线段树可以以 O ( l g n ) O(lgn) O(lgn)的复杂度维护这些。
下图是来源与网络的一张线段树的图片,首先我们明确一下线段树的节点的结构。
// l,r 分别表示当前树节点表示的左右区间的边界,val是当前节点所代表的区间的区间信息
// lazytag是这个区间的懒标记。
class Tree{
int l = 0;
int r = 0;
int val = 0;
int lazy_tag = 0;
// 虽然没有方法,但是会有一个默认初始化方法。
}
线段树类的格式
我们还是及尽量把问题板子化,一个标准的线段树类,可以有以下部分函数构成。
SegmentTree
函数: 初始化这个类。build
函数:递归的方法建立区间树。query
函数change
函数pushdow
和update
函数
其中完全板子的前四个函数,和需要与维护信息简单修改的方法。
public class SegmentTree {
int[] input; // 我们读入的数值
Tree[] tree;
// node表示当前节点的编号,r,l表示当前节点代表的区间范围
// 建树
public SegmentTree(int n, int[] input){
//tree
tree = new Tree[4*n];
for (int i = 0; i<4*n; i++){
tree[i] = new Tree();
}
this.input = input;
}
// 在主函数调用的时候一般是build(1, 0, n-1);保证我们从1开始。
public void build(int x, int l, int r){
tree[x].l = l;
tree[x].r = r;
if (r == l){
tree[x].val = a[l];
//sum[node] = a[l];
return;
}
int mid = (l+r)/2;
int left_node = 2*x;
int right_node = 2*x+1;
build(left_node, l, mid);
build(right_node, mid+1, r);
// 更新当前节点的数值
// 如果是最小值,变为min[node] = min(min[left_node]. min(right_node))
tree[x].val = tree[left_node].val+tree[right_node].val;
}
// x 表示当前线段树的节点,[l,r]为要修改的位置,val 为要修改的值
public void change(int x, int l, int r, int val){
if (tree[x].l == l && tree[x].r == r){
update(x, val);
return;
}
int mid = (tree[x].r+tree[x].l)/2;
int left_node = 2*x;
int right_node = 2*x+1;
pushdown(x); // 检查当前是否存在懒标记
if (r<=mid){
// 说明只需要操作左侧的子树
change(left_node, l, r, val); // !!!!注意这里是 l,r
}
else if (l>=mid+1){
// 说明只需要操作右侧的子树
change(right_node, l, r, val);
}else{
change(left_node, l, mid, val);
change(right_node, mid+1, r, val);
}
// 在修改完了子节点之后,修改本节点 也可写为pushup函数
tree[x].val = tree[left_node].val+ tree[right_node].val;
}
// node为当前线段树的节点,xy为询问区间和的范围
public int query(int x, int l, int r){
if (tree[x].l == l && tree[x].r == r){
return tree[x].val;
}
int mid = (tree[x].r+tree[x].l)/2;
int left_node = 2*x;
int right_node = 2*x+1;
pushdown(x);
if (r<=mid){
return query(left_node, l, r);
}else if( l>=mid+1){
return query(right_node, l,r);
}else{
return query(left_node, l,mid)+query(right_node, mid+1, r);
}
}
// 对于不同的题目,要求维护的区间信息不同,如区间最值,区间和等,修改以下两个函数
// 重点修改这个函数,这里是求和,且只有一个懒标记
public void update(int x, int value) {
tree[x].val += value*(tree[x].r-tree[x].l+1);
tree[x].lazy_tag += value;
}
public void pushdown(int x){
if(tree[x].lazy_tag == 0){
return;
}
int left_node = 2*x;
int right_node = 2*x+1;
update(left_node, tree[x].lazy_tag);
update(right_node, tree[x].lazy_tag);
tree[x].lazy_tag = 0;
}
在例题中使用线段树
板子题1:区间加
线段树的代码与前面完全一样,这里我主要写一下主函数的代码。
public static void main(String args[]) throws Exception {
int[] input = new int[]{
1,1,1};
int n = input.length;
SegmentTree ans = new SegmentTree(n, input);
// 编号从1,范围0,n-1
ans.build(1, 0, n-1);
// 第一个参数不变,永远是1,后面三个参数表示了对[0,2]这个区间上的全部数字+1;
ans.change(1, 0,2, 1);
// 返回答案,第一个参数不变,永远是1,后面表示了对[0,n-1]这个区间的区间和;
System.out.println(ans.query(1, 0,n-1));
板子题2:区间加和区间乘
这里就需要两个懒标记了,一个维护乘法一个维护加法的懒标记,每个区间其实应该等价于Y = Ax+B
。因此我们先乘乘法懒标记,再加上加法懒标记。
可以参考leectcode1622题
class Tree{
int l,r,lazy_tag1,val;
// 注意乘法的懒标记初始化是1.
int lazy_tag2 = 1;
}
class SegmentTree{
int mod = 1000000007;
Tree[] tree;
int[] input;
public SegmentTree(int N, int[] input){
tree = new Tree[N<<2];
for (int i = 0; i<(N<<2);i++){
tree[i] = new Tree();
}
this.input = input;
}
public void build(int x, int l, int r){
tree[x].l = l;
tree[x].r = r;
if (l == r){
tree[x].val = input[l];
return;
}
int mid = (tree[x].l+tree[x].r)/2;
int left_node = 2*x;
int right_node = 2*x+1;
build(left_node, l, mid);
build(right_node, mid+1, r);
tree[x].val = tree[left_node].val+tree[left_node].val;
}
public void change(int x, int l, int r, int val, int type){
if (tree[x].l == l && tree[x].r == r){
update(x,val, type);
return;
}
int mid = (tree[x].l+tree[x].r)/2;
int left_node = 2*x;
int right_node = 2*x+1;
pushdown(x);
if (l>=mid+1){
// 需要修改的部分在右侧
change(right_node, l, r, val, type);
}
else if (r<=mid){
change(left_node, l, r, val, type);
}
else {
change(left_node, l, mid,val, type);
change(right_node, mid+1, r,val, type);
}
tree[x].val = tree[left_node].val+tree[left_node].val;
}
public int query(int x, int l, int r){
if (tree[x].l == l && tree[x].r == r){
return tree[x].val;
}
int mid = (tree[x].l+tree[x].r)/2;
int left_node = 2*x;
int right_node = 2*x+1;
pushdown(x);
if (l>=mid+1){
return query(right_node, l, r);
}
else if (r<=mid){
return query(left_node, l, r);
}
else{
return query(left_node, l, mid)+query(right_node, mid+1, r);
}
}
// 前面的函数完全一样。下面的不同
public void update(int x, int val, int type){
if (type == 1){
// 表示更新加法懒标记
tree[x].val = (int)((1L*val*(tree[x].r-tree[x].l+1)+tree[x].val)%mod);
tree[x].lazy_tag1 = (int)((1L*tree[x].lazy_tag1+val)%mod);;
}
else{
// 更新乘法懒标记。
tree[x].val = (int)((1L*tree[x].val*val)%mod);
tree[x].lazy_tag2 = (int)((1L*tree[x].lazy_tag2*val)%mod);
// 注意这个地方加法懒标记也是受到影响的。也会扩大。
tree[x].lazy_tag1 = (int)((1L*tree[x].lazy_tag1*val)%mod);
}
}
public void pushdown(int x){
if (tree[x].lazy_tag1 == 0 && tree[x].lazy_tag2 == 1) return;
int mid = (tree[x].l+tree[x].r)/2;
int left_node = 2*x;
int right_node = 2*x+1;
// 一定要先update乘法,再update加法。
update(left_node, tree[x].lazy_tag2,2);
update(left_node, tree[x].lazy_tag1,1);
update(right_node, tree[x].lazy_tag2,2);
update(right_node, tree[x].lazy_tag1,1);
tree[x].lazy_tag1 = 0;
tree[x].lazy_tag2 = 1;
}
}
class Fancy {
int N = 100000;
int index = 0;
int[] a = new int[N];
SegmentTree st = new SegmentTree(N,a);
public Fancy() {
st.build(1, 0, N-1);
}
public void append(int val) {
st.change(1, index, index, val,1);
index++;
}
public void addAll(int inc) {
if (index == 0) return;
st.change(1, 0, index-1, inc, 1);
}
public void multAll(int m) {
st.change(1, 0, index-1, m, 2);
}
public int getIndex(int idx) {
if (idx >= index) return -1;
return st.query(1, idx, idx);
}
}