导航

线段树

Posted on 2022-06-07 15:52  wuqiu  阅读(45)  评论(0编辑  收藏  举报

线段树的定义

线段树是一颗二叉搜索树,它的每一个节点都有两个子节点,每一个节点存储着一个区间的有关信息(可以是区间和,也可以是区间的最大值等)
下图所示为 arr 数组区间和在线段树上的表示方法。

线段树的实现

使用数组存储二叉树的形式对线段树进行存储,在图中我们使用红色圆圈标记每一个节点的序号。即如果某个非叶结点的序号为 k ,那么它的左儿子序号为 2k ,右儿子序号为2k+1。

使用位运算来表示左右子树:
左子树: k<<1
右子树: k<<1|1

存储线段树所需要的空间:

结论:对于一个维护序列长度为n的线段树,使用堆结构存储的话,数组大小应该开4n,这样,在n为正整数的所有情况下都够用,且在最极限的情况下刚好够用。

证明:当n为2的正整数次幂时,设n = 2^k,此时线段树是一颗叶子数为n的满二叉树。若n再大一些但是不超过 2^(k+1)时,也是需要按照 n = 2^(k+1) 的情况开。
这启⽰我们,当2^k <n<=2^(k+1),即k<log n<=k+1时,线段树所开大小与叶子数为2^(k+1)的满二叉树大小相同,为 2^(k+2)-1 个
由 k < log n <= k+1 有 k + 1 = ceil(log n)
所以 size = 2^(k+2)-1 = 2 ^ (ceil(log 2n)) - 1
size < 2 ^ (log 2n + 1) = 4n 证毕

const int SIZE = 4 * N
int arr[N] , tree[SIZE];

//更新节点的值,这里每个节点表示所示区间的最大值
void reload(int index){
  tree[index] = max(tree[index<<1], tree[index<<1|1]);
}

void build(int index, int l,int r){
  if( l == r) //左端点等于右端点 为叶子节点,直接赋值就可以
    tree[index] = arr[l];
  else{
    int m = (l + r) >> 1;
    build(index << 1, l , m); // 构造左儿子节点
    build(index << 1|1, m+1 , r);// 构造右儿子节点
    reload(index); // 更新父节点
  }
}

线段树的基本操作

点的更新

// l 是区间的左端点   r 是区间的右端点
//target 是要修改的目标索引  value是要修改的值
//index 是该区间在线段树中的索引
void update(int target,int value,int l, int r, int index){
  if( l == r){ // 找到目标点,更新数组和线段树
    arr[index] += value;
    tree[index]  += value;
  }
  else{
    int m = ( l + r) >> 1; 
    if(target <= m)  update(target,value,l,m,index<<1);
    else  update(target,value,m+1,r,index<<1|1);
    reload(index);
  }
}

区间更新

每一个节点对于要搜寻的区间有三种情况 1. 真包含 2. 有交集 3. 毫无关系
延迟标记(懒标记):每个节点新增加一个标记,记录这个节点是否进行了某种修改(这种修改操作会影响其子节点),对于任意区间的修改,我们先按照区间查询的方式将其划分成线段树中的节点,然后修改这些节点的信息,并给这些节点标记上代表这种修改操作的标记。在修改和查询的时候,如果我们到了一个节点p,并且决定考虑其子节点,那么我们就要看节点p是否被标记,如果有,就要按照标记修改其子节点的信息,并且给子节点都标上相同的标记,同时消掉节点p的标记。
就是在更改一个节点的时候,先不对它的子节点进行递归修改,当我们决定要访问它的子节点的时候,再将它懒标记传递下去。

void pushDown(int index){
  if(lazy[index]){
    //处理懒标记
    tree[index << 1] += lazy[index];
    tree[index << 1 | 1] += lazy[index];
   
    //这一步是为了 子节点的子节点  我们为子节点打上懒标记  标志子节点被更新
    lazy[index << 1] += lazy[index];
    lazy[index << 1|1] += lazy[index];

    //懒标记清空
    lazy[index] = 0;
  }
}
void updateInterval(int targetL,int targetR,int l ,int r,int index,int value){
  if(l >  targetR || r <  targetL)  return ; // 毫无关系
  
  if(l >= targetL && targetR >= r){// 真包含
      tree[index] += value;
      lazy[index] += value;
  }
  pushDown(index); // //延迟标记向下传递
  int m = (l + r) >> 1;
  updateInterval(targetL,targetR,l,m,index<<1);
  updateInterval(targetL,targetR,m+1,r,index<<1|1);
  reload(index);  
}

线段树实战

732. 我的日程安排表 III

当 k 个日程安排有一些时间上的交叉时(例如 k 个日程安排都在同一时间内),就会产生 k 次预订。
给你一些日程安排 [start, end) ,请你在每个日程安排添加后,返回一个整数 k ,表示所有先前日程安排会产生的最大 k 次预订。
实现一个 MyCalendarThree 类来存放你的日程安排,你可以一直添加新的日程安排。

MyCalendarThree() 初始化对象。
int book(int start, int end) 返回一个整数 k ,表示日历中存在的 k 次预订的最大值。

示例

输入:
["MyCalendarThree", "book", "book", "book", "book", "book", "book"]
[[], [10, 20], [50, 60], [10, 40], [5, 15], [5, 10], [25, 55]]
输出:
[null, 1, 1, 2, 3, 3, 3]
解释:
MyCalendarThree myCalendarThree = new MyCalendarThree();
myCalendarThree.book(10, 20); // 返回 1 ,第一个日程安排可以预订并且不存在相交,所以最大 k 次预订是 1 次预订。
myCalendarThree.book(50, 60); // 返回 1 ,第二个日程安排可以预订并且不存在相交,所以最大 k 次预订是 1 次预订。
myCalendarThree.book(10, 40); // 返回 2 ,第三个日程安排 [10, 40) 与第一个日程安排相交,所以最大 k 次预订是 2 次预订。
myCalendarThree.book(5, 15); // 返回 3 ,剩下的日程安排的最大 k 次预订是 3 次预订。
myCalendarThree.book(5, 10); // 返回 3
myCalendarThree.book(25, 55); // 返回 3

class MyCalendarThree {
public:
    MyCalendarThree() {}
    map<int,pair<int,int>> tree;
    void reload(int index){
        if(index > 0)
            tree[index].first =  max(tree[index<<1].first, tree[index<<1|1].first);
    }
    void pushDown(int index){
        if(tree[index].second != 0 && index > 0){
            tree[index<<1].first += tree[index].second;
            tree[index<<1|1].first += tree[index].second;
            tree[index<<1].second += tree[index].second;
            tree[index<<1|1].second += tree[index].second;
            tree[index].second = 0;
        }
    }
    void addDate(int start,int end, int l,int r,int index){
        if(start > r || end < l) // 毫无关系
            return;
        if(l >= start && end >= r){//真包含
            tree[index].first++;
            tree[index].second++;                        
        }
        else{
            pushDown(index);
            int m = (l + r) >> 1;
            if(index > 0){
                addDate(start,end,l,m,index<<1); 
                addDate(start,end,m+1,r,index<<1|1); 
            }
            reload(index);
        }
    }
    int book(int start, int end) {
        addDate(start,end-1,0,1e9,1);
        return tree[1].first;
    }
};

/**
 * Your MyCalendarThree object will be instantiated and called as such:
 * MyCalendarThree* obj = new MyCalendarThree();
 * int param_1 = obj->book(start,end);
 */

要注意的是leetcode对数据范围的检查非常严格,对于左移(<<)右移(>>)等操作,必须要声明操作数非负,纵然index永远都不会小于零,但是也需要给出限制语句