• 线段树及Lazy-Tag


    一:线段树
    线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(log2N)。
    线段树的每个节点都表示一个区间[L, R],对于一个线段树的区间:
    若L < R,则必能被分为[L, M]和[M+1, R],其中M = (L + R) / 2。
    若L = R,则为叶子节点。
    实现方法:
    数组实现:节点T的左儿子是2T,代表[L, M]区间,右儿子是2T+1,代表[M+1,R]区间。
    结构体指针实现:左右子树为*l,*r。
    三个重点:
    1.线段树的构建

    int create_tree(int h,int x,int y)
    {  
        tree[h].l=x;tree[h].r=y;//当前节点的区间赋值为[x,y];
    if(x==y)//若当前节点为叶子节点,则更新该点权值,返回给父亲节点。
    {  
            tree[h].s=a[x];  
            return tree[h].s;  
        }
        int mid=(x+y)/2;//向下
        int x1=create_tree(h*2,x,mid);//更新当前节点; 
        int x2=create_tree(h*2+1,mid+1,y);  
    tree[h].s=max(x1,x2);//更新权值
        return tree[h].s;         
    } 

    2线段树的查询

    int query(int 当前节点,int L,int R)
    {
        如果[L,R]与当前节点区间无交集,则返回;
        若[L,R]包含当前节点区间,则返回所求值;
        搜索左右子树;
        返回值;
    }

    3.线段树的更新

    void update(int 当前节点,int L,int  R)
    {
        如果[L,R]与当前节点区间无交集,则返回;
        若[L,R]包含当前节点区间,则返回所求值,停止递归;
        搜索左右子树;
        重新计算本节点信息;
        返回;
    }

    下面有道例题:
    例1 I hate it(hdu 1754)
    题目描述:
    很多学校流行一种比较的习惯。老师们很喜欢询问,从某某到某某当中,分数最高的是多少。
    这让很多学生很反感。不管你喜不喜欢,现在需要你做的是,就是按照老师的要求,写一个程序,模拟老师的询问。当然,老师有时候需要更新某位同学的成绩。
    本题目包含多组测试
    在每个测试的第一行,有两个正整数 N 和 M ( 0~N<=200000,0~M<5000 ),分别代表学生的数目和操作的数目。学生ID编号分别从1编到N。第二行包含N个整数,代表这N个学生的初始成绩,其中第i个数代表ID为i的学生的成绩。接下来有M行。每一行有一个字符 C (只取’Q’或’U’) ,和两个正整数A,B。
    当C为’Q’的时候,表示这是一条询问操作,它询问ID从A到B(包括A,B)的学生当中,成绩最高的是多少。
    当C为’U’的时候,表示这是一条更新操作,要求把ID为A的学生的成绩更改为B。
    对于每一次询问操作,在一行里面输出最高成绩。

    输入
    本题目包含多组测试,请处理到文件结束。
    在每个测试的第一行,有两个正整数 N 和 M 分别代表学生的数目和操作的数目。
    学生ID编号分别从1编到N。
    第二行包含N个整数,代表这N个学生的初始成绩,其中第i个数代表ID为i的学生的成绩。
    接下来有M行。每一行有一个字符 C (只取’Q’或’U’) ,和两个正整数A,B。
    当C为’Q’的时候,表示这是一条询问操作,它询问ID从A到B(包括A,B)的学生当中,成绩最高的是多少。
    当C为’U’的时候,表示这是一条更新操作,要求把ID为A的学生的成绩更改为B。

    输出
    对于每一次询问操作,在一行里面输出最高成绩。

    样例输入
    5 6
    1 2 3 4 5
    Q 1 5
    U 3 6
    Q 3 4
    Q 4 5
    U 2 9
    Q 1 5

    样例输出
    5
    6
    5
    9

    分析
    最容易想到的算法是将成绩存到数组里,然后对于每一条查询,遍历数组的每一个元素。总时间复杂度是O(NM),实在是太大了。根据题目,我们可以用线段树来存储[x,y]区间中成绩的最大值,这样做的时间复杂度只有O(MlogN)。

    参考代码:

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    using namespace std;
    const int maxn=200000+10;
    struct node//定义线段树
    {
        int s;//权值
        int l,r;//左右子树权值
    };
    struct node tree[maxn*10];
    int a[maxn];
    int create_tree(int h,int x,int y)//建树(h为树编号)
    {
        tree[h].l=x;tree[h].r=y;//记录区间[l,r]
        if(x==y)//叶子结点
        {
            tree[h].s=a[x];//记录权值
            return tree[h].s;//返回权值
        }
        int mid=(x+y)/2;//取中点(int自动取整)
        int x1=create_tree(h*2,x,mid);//左子树权值
        int x2=create_tree(h*2+1,mid+1,y);//右子树权值
        tree[h].s=max(x1,x2);//取更大值
        return tree[h].s;//返回权值
    }
    int query(int h,int x,int y)//查询
    {
        if(y<tree[h].l||x>tree[h].r)//...x2---y2...l——r...x1---y1...
            return 0;
        if(x<=tree[h].l&&tree[h].r<=y)//达到范围...x---l——r---y...
            return tree[h].s;//返回权值
        int x1=query(2*h,x,y);//左子树
        int x2=query(2*h+1,x,y);//右子树
        return max(x1,x2);//返回权值
    }
    int update(int h,int x)//维护线段树
    {
        if(x<tree[h].l || x>tree[h].r)//超过范围...x1...l——r...x2...
            return tree[h].s;//返回权值
        if(tree[h].l==tree[h].r)//左右子树相同
        {   
            tree[h].s=a[tree[h].l];//改权值
            return tree[h].s;//返回权值 
        }
        int x1=update(2*h,x);//左子树
        int x2=update(2*h+1,x);//右子树
        tree[h].s=max(x1,x2);//改权值
        return tree[h].s;//返回权值
    }
    int main()
    {
        int i,j,k,m,n;int x,y;char c;
        scanf("%d%d",&n,&m);
        for(i=1;i<=n;i++)   scanf("%d",&a[i]);
        create_tree(1,1,n);
        for(i=1;i<=m;i++)
        {
            getchar();//过滤换行
            scanf("%c%d%d",&c,&x,&y);//取得指令
            if(c=='Q')
                {printf("%d
    ",query(1,x,y));}
            else
                {a[x]=y;update(1,x);}
        }
        return 0;
    }

    二:Lazy-Tag
    lazy-tag思想,记录每一个线段树节点的变化值,当这部分线段的一致性被破坏我们就将这个变化值传递给子区间,大大增加了线段树的效率。
    在此通俗的解释我理解的Lazy意思:
    现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行更新操作;如果刚好执行到一个rt节点,而且tree[rt].l == a && tree[rt].r == b,这时我们就应该一步更新此时rt节点的sum[rt]的值(sum[rt]+=c* (tree[rt].r - tree[rt].l + 1))。
    关键来了,如果此时按照常规的线段树的update操作,这时候还应该更新rt子节点的sum[]值,而Lazy思想恰恰是暂时不更新rt子节点的sum[]值,而是在这里打一个tag,直接return。直到下次需要用到rt子节点的值的时候才去更新,这样避免许多可能无用的操作,从而节省时间 。
    另外我们经常在树里面用到位运算,简单介绍一下:

    (i<<n)==(i*2n)  (i>>n)==(⌊i/2n⌋)
    在找子树的时候,若父亲结点编号为i,则左右子结点分别表示为2i2i+1,而树中就直接写为i<<1i<<1|1(“|”详细自行百度),而寻找子节点可以表示为i>>1;

    申请结构体的时候,要开到四倍长度空间,直接表示为i<<2;
    这里再说明一下为什么要开四倍空间

    假设我们用一个数组来头轻脚重地存储一个线段树,根节点是1,孩子节点分别是2n, 2n+1, 那么,设线段长为L(即[1..L+1))
    设树的高度为H,对H,有:H(L)={1,1+H(⌈L2⌉)L>=1;
    这是一个很简单的递归式,并用公式逐次代换,就等到
    H(L)=k+H(⌈L2k⌉),其中 k 是满足2k≥L的最小值,所以H(L)=⌈lgL⌉+1.
    所以显然所需空间为
    2^H−1=2^(⌈lgL⌉+1)−1
          =2×2^(⌈lgL⌉)−1
      =2×2(L1)−1
      =4L−5,L2

    来看一道题:
    例2:一个简单的问题与整数 [POJ 3468]
    题目描述
    你有N个整数,A1,A2,…,AN。 你需要处理两种操作。 一种类型的操作是在给定间隔中向每个数字添加一些给定数目。 另一个是要求给定间隔内的数字之和。

    输入
    第一行包含两个数字N和Q (1≤N,Q≤100000)
    第二行包含N个数字,即A1,A2,…,AN的初始值。(-1000000000≤Ai≤1000000000)。
    接下来的Q行中的每一行表示操作。
    “C a b c”意味着把Aa,Aa+1,…,Ab中的每一个都加上C(-10000≤c≤10000)。
    “Q a b”表示查询Aa,Aa+1,…,Ab的和。

    输出
    按顺序回答所有的“Q”命令。 一行中有一个答案。

    样例输入
    10 5
    1 2 3 4 5 6 7 8 9 10
    Q 4 4
    Q 1 10
    Q 2 4
    C 3 6 3
    Q 2 4

    样例输出
    4
    55
    9
    15
    *提示:可能超出int范围

    参考代码

    #include<cstdio>  
    using namespace std;  
    #define maxn 100000+10  
    typedef long long LL;  
    struct node{  
        int l,r,m;//左右中点 
        LL sum,mark;//权值、tag 
    }T[maxn<<2];  
    int a[maxn];  
    void build(int id,int l,int r){  
        T[id].l=l;//左端点 
        T[id].r=r;//右端点 
        T[id].m=(l+r)>>1;//中点 
        T[id].mark=0;//初始化标记 
        if(l==r)//达到端点 
            {T[id].sum=a[l];return;}//标记和,停止递归并返回 
        build(id<<1,l,T[id].m);//递归左子树 
        build(id<<1|1,T[id].m+1,r);//递归右子树 
        T[id].sum=(T[id<<1].sum+T[id<<1|1].sum);//记录和  
    }  
    void update(int id,int l,int r,int val){
         if(T[id].l==l&&T[id].r==r)//确定是这一段了 
         {T[id].mark+=val;return;}//不必递归到叶子结点,打tag 
         T[id].sum+=(LL)val*(r-l+1);//更新权值 
         if(T[id].m>=r)//只要更新左子树 
              update(id<<1,l,r,val);  
         else if(T[id].m<l)  
              update((id<<1)+1,l,r,val);//只要更新右子树 
         else
         {  
              update(id<<1,l,T[id].m,val);//更新左右子树 
              update(id<<1|1,T[id].m+1,r,val);  
         }  
    }  
    LL query(int id,int l,int r){  
        if(T[id].l==l&&T[id].r==r)//找到结点 
            return T[id].sum+T[id].mark*(LL)(r-l+1);//权值+tag 
        if(T[id].mark)//原来更新到这里的时候没有继续更新下去了(有tag) 
        {  
            T[id<<1].mark+=T[id].mark;//tag下传 
            T[id<<1|1].mark+=T[id].mark;
            T[id].sum+=(LL)(T[id].r-T[id].l+1)*T[id].mark;//把tag加回sum 
            T[id].mark=0;//去掉tag  
        }  
        if(T[id].m>=r){  
              return query(id<<1,l,r);//只有左子树 
        }  
        else if(T[id].m<l){  
              return query(id<<1|1,l,r);//只有左子树
        }  
        else{  
              return query(id<<1,l,T[id].m)+query((id<<1)+1,T[id].m+1,r);//左右子树都有 
        }
    }  
    int main(){  
        int n,Q;
        char str[8];   
        int b,c,d;  
        scanf("%d%d",&n,&Q);
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]); 
        build(1,1,n);//建树 
        for(int i=0;i<Q;i++)
        {  
            scanf("%s",str);  
            if(str[0]=='Q')
            {  
                scanf("%d%d",&b,&c);  
                printf("%lld
    ",query(1,b,c));//查询 
            }  
            else
            {  
                scanf("%d%d%d",&b,&c,&d); 
                update(1,b,c,d);//更新
            }  
        }  
        return 0;  
    }

    更大的挑战:
    例3 Count color[POJ 2777]
    题目描述
    有一个非常长的板,长度L厘米,L是一个正整数,所以我们可以均匀地划分为L段,他们从左到右标记为1,2,… L,每个是1厘米长。现在我们必须着色板 - 一段只有一种颜色。我们可以在板上进行以下两个操作:
    1.“C A B C”使板材从板材A到板材C着色C.
    2.“P A B”输出在段A和段B(包括)之间绘制的不同颜色的数量。
    在我们的日常生活中,我们有很少的词来描述一种颜色(红色,绿色,蓝色,黄色…),所以你可以假设不同颜色T的总数是非常小的。为了简单起见,我们将颜色的名称表示为颜色1,颜色2,…颜色T.在开始时,板子以颜色1绘制。现在剩下的问题留给你。

    输入
    第一行输入包含L(1≤L≤100000),T(1≤T≤30)和O(1≤O≤100000)。这里O表示操作的数量。在O行之后,每个包含“C A B C”或“P A B”(这里A,B,C是整数,A可以大于B)作为先前定义的操作。

    输出
    输出结果按顺序输出操作,每行包含一个数字。

    样例输入
    2 2 4
    C 1 1 2
    P 1 2
    C 2 2 2
    P 1 2

    样例输出
    2
    1

    分析
    根据题目的数据规模,暴力求解显然超时。所以就考虑用线段树做。
    说明
    本题运用了线段树中“区间修改”的思想,只修改目标区间而不再继续修改其子节点(lazy)

    参考代码:

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    using namespace std;
    const int N=100010;
    #define L(rt) (rt<<1)
    #define R(rt) (rt<<1|1)
    struct Tree{
        int l,r;
        int col;    //用一个32位的int型,每一位对应一种颜色,用位运算代替bool col[32]
        bool cover; //表示这个区间都被涂上同一种颜色提高效率 
    }tree[N<<2];
    
    void build(int L,int R,int rt){
        tree[rt].l=L;//左区间 
        tree[rt].r=R;//右区间 
        tree[rt].col=1; //开始时都为涂有颜色1
        tree[rt].cover=1;//当然只有一种颜色 
        if(tree[rt].l==tree[rt].r)
            return ;//叶节点直接返回 
        int mid=(L+R)>>1;//取中点 
        build(L,mid,L(rt));//建左子树 
        build(mid+1,R,R(rt));//建右子数 
    }
    
    void PushDown(int rt){//下推
        tree[L(rt)].col=tree[rt].col;
        tree[L(rt)].cover=1;
        tree[R(rt)].col=tree[rt].col;
        tree[R(rt)].cover=1;
        tree[rt].cover=0;//标记取消 
    }
    
    void PushUp(int rt){//最后递归回来再更改父节点的颜色
        tree[rt].col=tree[L(rt)].col | tree[R(rt)].col;//相加 
    }
    
    void update(int val,int L,int R,int rt){
        if(L<=tree[rt].l && R>=tree[rt].r){//区间在要求范围内 
            tree[rt].col=val;//刷颜色 
            tree[rt].cover=1;//打标记 
            return;//不需要更新子树了 
        }
        if(tree[rt].col==val)//剪枝
            return;//不需要更新子树了 
        if(tree[rt].cover)//这里面只有一种颜色 
        PushDown(rt);//下推 
        int mid=(tree[rt].l+tree[rt].r)>>1;
        if(R<=mid)
            update(val,L,R,L(rt));
        else if(L>=mid+1)
            update(val,L,R,R(rt));
        else{
            update(val,L,mid,L(rt));
            update(val,mid+1,R,R(rt));
        }
        PushUp(rt); //上载 
    }
    
    int sum;
    
    void query(int L,int R,int rt)
    {
        if(L<=tree[rt].l && R>=tree[rt].r){
            sum |= tree[rt].col;//把颜色加进和里 
            return;
        }
        if(tree[rt].cover){//这个区间全部为1种颜色,就没有继续分割区间的必要了
            sum |= tree[rt].col;//颜色种类相加的位运算代码
            return;
        }
        int mid=(tree[rt].l+tree[rt].r)>>1;
        if(R<=mid)
            query(L,R,L(rt));
        else if(L>=mid+1)
            query(L,R,R(rt));
        else
        {
            query(L,mid,L(rt));
            query(mid+1,R,R(rt));
        }
    }
    
    int solve()//位运算 
    {
        int ans=0;
        while(sum)
        {
            if(sum&1)
                ans++;
            sum>>=1;
        }
        return ans;
    }
    
    void swap(int &a,int &b)
    {
        int tmp=a;a=b;b=tmp;
    }
    
    int main()
    {
        int n,t,m;
        scanf("%d%d%d",&n,&t,&m);
        build(1,n,1);//建树 
        char op[3];
        int a,b,c;
        while(m--)
        {
            scanf("%s",op);
            if(op[0]=='C')
            {
                scanf("%d%d%d",&a,&b,&c);
                if(a>b)
                    swap(a,b);
                update(1<<(c-1),a,b,1); // int型的右起第c位变为1,即2的c-1次方。
            }
            else
            {
                scanf("%d%d",&a,&b);
                if(a>b)
                    swap(a,b);
                sum=0;
                query(a,b,1);
                printf("%d
    ",solve());
            }
        }
        return 0;
    }
  • 相关阅读:
    jQuery Timer 实现的新邮件提醒
    在 jquery repeater 中添加设置日期,下拉,复选框等控件
    jquery repeater 完成 QQ 邮箱邮件分组显示功能
    Ajax 与 WebService 之间日期等数据类型的转换
    通过 Parameter 对象添加 Ajax 请求时的参数
    在 jQuery Repeater 进行验证更新等操作时提示消息
    jquery repeater 模仿 Google 展开页面预览子视图
    在 jQuery Repeater 中检索过滤数据
    功能完善的 jquery validator 完成用户注册的验证
    在 Repeater 中绑定转化 JSON 格式的字段
  • 原文地址:https://www.cnblogs.com/ibilllee/p/7651975.html
Copyright © 2020-2023  润新知