【线段树】基本写法,区间极值,区间延迟更新,多延迟标签

线段树的特性:

开多大数组来存线段树?

线段树表示的 区间长度为k, 则叶节点个数k,线段树作为一棵 完全二叉树,它的总节点个数为 n=2*k-1。 
并且:  线段树的左右子树 叶节点个数相差不超过1
假定线段树的深度为h。则深度h对应的满二叉树节点个数为2^h-1.
下面证明:  2^h - 1 >= n > (2^(h-1) -1), 可以用反证法证明
首先,证明引理1:随着线段树的节点个数 2*k-1 增大,  深度不会出现反而减小的情况。 归纳法即可: k=1: h = 1, k= 2: h=2. .... : 
然后,有 当线段树节点个数 n= 2^(h-1)-1时,深度为h-1; 当 n= 2^h-1时,深度为h。
现在给定深度为h,则根据引理,必有 2^h - 1 >= n > (2^(h-1) -1)
那么线段树如果用数组来存,数组实际上存的是一棵满二叉树。那么数组的容量 C=2^h-1<=2*n-1= 4*k-3   
于是, 数组存线段树,它的容量为 4*k - 3, k 为区间容量。

更新: 

延迟更新(不一下子更新到叶节点,而是先更新到尽量少的几个线段节点)

查询:

  • 每次查询到一个节点,它祖宗不能有延迟的更新未传播下来。否则当前节点读到的就是一个无效(未完全更新)数据了。
  • 访问(查询或更新)一个点之前,如果该点存在未下达的更新,就要先下达更新。(down)。 

         这样每到一个点,路径上面的祖宗点的更新都已经下达了下来。


struct node{
  int value;//统计的量
  int delta;//更新的量
}
struct tree{
  node nodes[MAXN];
  bool uTag;//
  void down(int ndx, int l, int r){
        node &d=nodes[ndx];
        if(!d.delta) return; //不需要向下更新
        if(l==r)  doSomething; //L==r是叶节点了
        nodes[2*ndx+1], nodes[2*n+2]   <=   d.delta //更新子节点
        d.delta=0; //不再更新
   }
   void set(int ndx, int l, int r, int delta, int s, int t){
        down(ndx, l, r)

        node &d=nodes[ndx];
        if(s<=l && t>=r)  	{ d.value+=delta, d.delta=delta; return;}

        //拆分[l, r]
        int mid=( l+r ) /2
        set(2*ndx+1, l, mid, delta, s, mid)  set(2*ndx+2, mid+1, r, delta, mid+1, t)
        
        //merge....
   }
   void get(int idx, int l, int r, int s, int t){
        down(idx, l, r)

        node &d=nodes[idx];
        if(s<=l && t>=r)  return xxx(d.value)

        //拆分[l, r]
        int mid=( l+r ) /2
        s1=get(2*idx+1, l, mid, d, s, mid)
        s2=get(2*idx+2, mid+1, r, d, mid+1, t)
        return xxx(s1, s2)
   }
};


例如进行

区间极值1

(关键在于更新的方向: 对于区间最小值这种, 先找到某个叶子节点,然后自底向上更新。而对于一次更新一个区间的求和性线段树,则自顶向下更新。)

 
 
另一个模板:

区间极值2

每个测试点(输入文件)有且仅有一组测试数据。

每组测试数据的第1行为一个整数N,意义如前文所述。

每组测试数据的第2行为N个整数,分别描述每种商品的重量,其中第i个整数表示标号为i的商品的重量weight_i。

每组测试数据的第3行为一个整数Q,表示小Hi总共询问的次数与商品的重量被更改的次数之和。

每组测试数据的第N+4~N+Q+3行,每行分别描述一次操作,每行的开头均为一个属于0或1的数字,分别表示该行描述一个询问和描述一次商品的重量的更改两种情况。对于第N+i+3行,如果该行描述一个询问,则接下来为两个整数Li, Ri,表示小Hi询问的一个区间[Li, Ri];如果该行描述一次商品的重量的更改,则接下来为两个整数Pi,Wi,表示位置编号为Pi的商品的重量变更为Wi

对于100%的数据,满足N<=10^6,Q<=10^6, 1<=Li<=Ri<=N,1<=Pi<=N, 0<weight_i, Wi<=10^4。

 

#include <iostream>
#include <stdio.h>
using namespace std;
int tree[3000000];
int data[1000000];
void init(int l, int r, int idx){
    if(l==r) tree[idx]=data[l];
    else{
        int mid=(l+r)/2;
        init(l, mid, idx*2+1);
        init(mid+1, r, idx*2+2);
        tree[idx]=min(tree[idx*2+1], tree[idx*2+2]);
    }
}
int get(int l, int r, int idx, int a, int b){
    if(a<=l && b>=r) return tree[idx];
    int mid=(l+r)/2;
    if(a<=mid && b>mid){
        int s = get(l, mid, idx*2+1, a, mid);
        int t = get(mid+1, r, idx*2+2, mid+1, b);
        return min(s, t);
    }
    else if(a>mid){
        return get(mid+1, r, idx*2+2, a, b);
    }
    else return get(l, mid, idx*2+1, a, b);
}
// 更新单点
int set(int l, int r, int idx, int a, int b){
    while(l!=r){
        int mid = (l+r)/2;
        if( a<=mid ) r=mid, idx=idx*2+1;
        else l=mid+1, idx=idx*2+2;
    }
    tree[idx] = b; ///@error lost this. update leaf node 
    while( idx>0 ){
        int p = (idx-1)/2, s = 4*p + 3 - idx; //@error: sb le, s = 2*p + 3 - idx ???
        tree[p]=min(tree[idx], tree[s]);
        idx = p;
    }
}
int main(){
    int n; scanf("%d", &n);
    for(int i=0; i<n; i++) scanf("%d", &data[i]);
    init(0, n-1, 0);
    int m; scanf("%d", &m);
    int c, a, b;
    for(int i=0; i<m; i++) {///@error: i<n
        scanf("%d %d %d", &c, &a, &b);
        if(c==0)  printf("%d\n", get(0, n-1, 0, a-1, b-1));
        else set(0, n-1, 0, a-1, b);
    }
    return 0;
}


区间更新问题

区间更新问题用上文所述, 

延迟更新(对线段的更新,只更新到对应的节点),访问先下达更新( 每读写一个节点前,先更新下达,从而保证访问任何节点时祖宗节点没有未下达的更新)

#include <iostream>
#include <vector>
#include <map>
#include <stdio.h>
using namespace std;
typedef pair<int, int> Pair;
Pair tree[800000];
int init(int l, int r, int idx, const vector<int>& a){
    tree[idx].second=0;
    if(l==r) return tree[idx].first=a[l];
    
    int mid=(l+r)/2;
    return tree[idx].first= init(l, mid, idx*2+1, a) + init(mid+1, r, idx*2+2, a);
}
void down(int l, int r, int idx){
    int d = tree[idx].second;
    if(d && l<r){
        int mid = (l+r)/2;
        tree[idx*2+1].first=d*(mid-l+1),tree[idx*2+1].second=d;
        tree[idx*2+2].first=d*(r-mid),  tree[idx*2+2].second=d;
        tree[idx].second=0;
    }
}
void set(int l, int r, int idx, int s, int t, int delta){
    down(l, r, idx);
    
    if(s<=l && t>=r) {
        tree[idx].first = delta*(r-l+1), 
        tree[idx].second= delta;
        return;
    }
  
    int mid = (l + r)/2;
    if(t<=mid) set(l, mid, idx*2+1, s, t, delta);
    else if(s>mid) set(mid+1, r, idx*2+2, s, t, delta);
    else{
        set(l, mid, idx*2+1, s, mid, delta);
        set(mid+1, r, idx*2+2, mid+1, t, delta);
    }
    
    // back
    tree[idx].first = tree[idx*2+1].first + tree[idx*2+2].first;
}
int get(int l, int r, int idx, int s, int t){
    down(l, r, idx);
    
    if(s<=l && t>=r) return tree[idx].first;
    
    int mid = ( l + r )/2;
    if(s>mid)  return get(mid+1, r, idx*2+2, s, t);
    else if(t<=mid) return get(l, mid, idx*2+1, s, t);
    else{
        return get(l, mid, idx*2+1, s, mid)+
                get(mid+1, r, idx*2+2, mid+1, t);
    }
}


int main(){
    int n; cin>>n;
    vector<int> a(n, 0);
    for(int i=0; i<n; i++){ cin>>a[i]; }
    init(0, n-1, 0, a);
    int m; cin>>m;
    char buf[1000];
    for(int i=0; i<m; i++){
        int op, a, b, c;
        cin>>op;
        if(op==0) {
            cin>>a>>b;
            cout<<get(0, n-1, 0, a-1, b-1)<<endl;
        }
        else {
            cin>>a>>b>>c;
            set(0, n-1, 0, a-1, b-1, c);
        }
    }
    return 0;
}

多延迟标签的线段树

延迟读写的正确性保证

线段树的set/get/add等读写操作,都是从根节点一直向下的顺序访问;而且每访问到一个节点,会及时把节点上的懒惰标签向下更新

这样做的目的是  保证每访问到一个节点的时候,它的祖宗的更新都已经传递下去,即祖宗节点中不再存在延迟标签(从而保证该节点上的各项值的正确性),这是很关键的。

延迟标签的时间先后性

而线段树的延迟存在这个规律:

祖宗节点和子节点同时存在延迟标签的时候,以祖宗节点为准,祖宗节点的延迟更新会覆盖子节点的

不难看出祖宗结点上的延迟标签 来自于 后来的更新,而子节点上的延迟是来自早期的还没来得及再往下传递的更新。后来的更新肯定是覆盖之前的更新的。

多个延迟标签的问题

线段树上如果有多个懒惰标签会出现什么状况?比如set标签和add标签同时出现?

谁能覆盖谁:

一般后来的set会覆盖早期的add。而后来的add则不会覆盖早期的set。


举例

描述

小Hi和小Ho都是游戏迷,“模拟都市”是他们非常喜欢的一个游戏,在这个游戏里面他们可以化身上帝模式,买卖房产。

在这个游戏里,会不断的发生如下两种事件:一种是房屋自发的涨价或者降价,而另一种是政府有关部门针对房价的硬性调控。房价的变化自然影响到小Hi和小Ho的决策,所以他们希望能够知道任意时刻某个街道中所有房屋的房价总和是多少——但是很不幸的,游戏本身并不提供这样的计算。不过这难不倒小Hi和小Ho,他们将这个问题抽象了一下,成为了这样的问题:

小Hi和小Ho所关注的街道的长度为N米,从一端开始每隔1米就有一栋房屋,依次编号为0..N,在游戏的最开始,每栋房屋都有一个初始价格,其中编号为i的房屋的初始价格为p_i,之后共计发生了M次事件,所有的事件都是对于编号连续的一些房屋发生的,其中第i次事件如果是房屋自发的涨价或者降价,则被描述为三元组(L_i, R_i, D_i),表示编号在[L_i, R_i]范围内的房屋的价格的增量(即正数为涨价,负数为降价)为D_i;如果是政府有关部门针对房价的硬性调控,则被描述为三元组(L_i, R_i, V_i),表示编号在[L_i, R_i]范围内的房屋的价格全部变为V_i。而小Hi和小Ho希望知道的是——每次事件发生之后,这个街道中所有房屋的房价总和是多少。

下面是一个多标签的实现(还有其他方法)

【线段树】基本写法,区间极值,区间延迟更新,多延迟标签_第1张图片

代码

#include <iostream>
#include <vector>
using namespace std;

struct node{
    int sum;
    int add;// 0 or not
    int set;//-1 or not 
};
node tree[4000000];
void down(int l, int r, int idx){
    if(tree[idx].set!=-1){
        int tmp =  tree[idx*2+1].set = tree[idx*2+2].set = tree[idx].set + tree[idx].add;//@error:
        tree[idx].add= tree[idx*2+1].add = tree[idx*2+2].add = 0;
        tree[idx].sum = (r-l+1)*tmp;
        tree[idx].set = -1;
    }    
    else if(tree[idx].add){
        tree[idx].sum += (r-l+1)*tree[idx].add;
        tree[idx*2+1].add += tree[idx].add;    
        tree[idx*2+2].add += tree[idx].add;    
        tree[idx].add = 0;
    }
}
void set(int l, int r, int idx, int a, int b, int d){
    if(a<=l && b>=r){
        tree[idx].set = d, tree[idx].add = 0;     
        return;    
    }
    down(l, r, idx);
    int mid = (l+r)/2;
    if(a<=mid) set(l, mid, idx*2+1, a, min(mid, b), d);
    if(b>mid) set(mid+1, r, idx*2+2, max(a, mid+1), b, d);
    down(l, mid, idx*2+1);
    down(mid+1, r, idx*2+2);
    tree[idx].sum = tree[idx*2+1].sum + tree[idx*2+2].sum;        
    //cout<<"set:"<<l<<","<<r<<":"<<tree[idx].sum<<endl;
}
void add(int l, int r, int idx, int a, int b, int d){
    if(a<=l && b>=r){
        tree[idx].add += d;        
        return;    
    }
    down(l, r, idx);
    int mid = (l+r)/2;
    if(a<=mid) add(l, mid, idx*2+1, a, min(mid, b), d);
    if(b>mid) add(mid+1, r, idx*2+2, max(a, mid+1), b, d);
    down(l, mid, idx*2+1);
    down(mid+1, r, idx*2+2);//@error: r not mid
    tree[idx].sum = tree[idx*2+1].sum + tree[idx*2+2].sum;        
    //cout<<"add:"<<l<<","<<r<<":"<<tree[idx].sum<<endl;
}
void init(int l, int r, int idx, vector<int> &p){
    if(l==r) {
        tree[idx].sum = p[l];
        tree[idx].set = -1, tree[idx].add = 0;
        return;
    }
    int mid = (l+r)/2;
    init(l, mid, idx*2+1, p);
    init(mid+1, r, idx*2+2, p);
    tree[idx].sum = tree[idx*2+1].sum + tree[idx*2+2].sum;
    tree[idx].set = -1, tree[idx].add = 0;
}
int get(int l, int r, int idx, int a, int b){
    down(l, r, idx);
    if(a<=l && b>=r){
        return tree[idx].sum;
    }
    int mid = (l+r)/2;
    int x = 0;
    if(a<=mid) x += get(l, mid, idx*2+1, a, min(mid, b));
    if(b>mid) x += get(mid+1, r, idx*2+2, max(a, mid+1), b);
    return x;
}
int main(){
    int n, m; cin>>n>>m; n++;
    vector<int> price(n);
    for(int i=0; i<n; i++) cin>>price[i];
    init(0, n-1, 0, price);

    for(int i=0; i<m; i++) {
        int o, a, b, d;
        cin>>o>>a>>b>>d;
        //cout<<o<<" "<<a<<" "<<b<<" "<<d<<endl;
        if(o==0) add(0, n-1, 0, a, b, d); 
        else set(0, n-1, 0, a, b, d);
        //for(int i =0; i<n; i++) cout<<get(0, n-1, 0, i, i)<<" ";cout<<endl;
        cout<<get(0, n-1, 0, 0, n-1)<<endl;
    }
    return 0;
}


你可能感兴趣的:(【线段树】基本写法,区间极值,区间延迟更新,多延迟标签)