线段树及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\n",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\n",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\n",solve());
        }
    }
    return 0;
}

转载于:https://www.cnblogs.com/ibilllee/p/7651975.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值