专题七 线段树(kuangbin专题)(还有扫描线的题待补充)

单点修改 + 区间查询: 1 2 9
区间修改 + 区间查询: 3 5 8 10 11 12 13
区间染色: 4 6
需要合并区间的区间查询: 7
扫描线计算面积: 16 15
扫描线计算周长: 14
扫面线计算体积: 17

1,敌兵布阵

题目链接:https://www.acwing.com/problem/content/4342/

这题就是一个没有懒标记的线段树的模板,维护一个区间总和

代码如下:

//线段树模板题
#include<iostream>
#include<algorithm>

using namespace std;

const int N=50010;

int n;
int a[N];
struct Node
{
    int l,r;
    int sum;
}tr[N*4];

void pushup(int u)
{
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void build(int u,int l,int r)
{
    if(l==r)tr[u]={l,r,a[l]};
    else
    {
        tr[u]={l,r};
        int mid=l+r>>1;
        build(u<<1,l,mid);
        build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void modify(int u,int x,int c)
{
    if(tr[u].l==x&&tr[u].r==x)tr[u].sum+=c;
    else
    {
        int mid=tr[u].l+tr[u].r>>1;
        if(x<=mid)modify(u<<1,x,c);
        else modify(u<<1|1,x,c);
        pushup(u);
    }
}
int query(int u,int l,int r)
{
    if(tr[u].l>=l&&tr[u].r<=r)return tr[u].sum;
    else
    {
        int res=0;
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)res=query(u<<1,l,r);
        if(r>mid)res+=query(u<<1|1,l,r);
        return res;
    }
}
int main()
{
    int t;
    scanf("%d",&t);
    for(int i=1;i<=t;i++)
    {
        printf("Case %d:\n",i);
        scanf("%d",&n);
        for(int i=1;i<=n;i++)scanf("%d",&a[i]);
        build(1,1,n);
        int x,y;
        char str[10];
        while(scanf("%s",str)!=EOF)
        {
            if(str[0]=='E')break;
            scanf("%d%d",&x,&y);
            
            if(str[0]=='A')modify(1,x,y);
            else if(str[0]=='S')modify(1,x,-y);
            else 
                printf("%d\n",query(1,x,y));
        }
    }
    
    return 0;
}

2,我讨厌它

题目链接:https://www.acwing.com/problem/content/4343/

这题也是一个没有懒标记的线段树模板,维护的是区间最大值

代码如下:

//线段树模板题
#include<iostream>
#include<algorithm>

using namespace std;

const int N=2e5+10;

int n,m;
int a[N];
struct Node
{
    int l,r;
    int maxv;
}tr[N*4];

void pushup(int u)
{
    tr[u].maxv=max(tr[u<<1].maxv,tr[u<<1|1].maxv);
}
void build(int u,int l,int r)
{
    if(l==r)tr[u]={l,r,a[l]};
    else
    {
        tr[u]={l,r};
        int mid=l+r>>1;
        build(u<<1,l,mid);
        build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void modify(int u,int x,int c)
{
    if(tr[u].l==x&&tr[u].r==x)tr[u].maxv=c;
    else
    {
        int mid=tr[u].l+tr[u].r>>1;
        if(x<=mid)modify(u<<1,x,c);
        else modify(u<<1|1,x,c);
        pushup(u);
    }
}
int query(int u,int l,int r)
{
    if(tr[u].l>=l&&tr[u].r<=r)return tr[u].maxv;
    else
    {
        int maxv=0;
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)maxv=query(u<<1,l,r);
        if(r>mid)maxv=max(maxv,query(u<<1|1,l,r));
        return maxv;
    }
}
int main()
{
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    
        build(1,1,n);
        char op[2];
        int x,y;
        while(m--)
        {
            scanf("%s%d%d",op,&x,&y);
            if(op[0]=='Q')
                printf("%d\n",query(1,x,y));
            else
                modify(1,x,y);
        }
    }
    
    return 0;
}

 3,一个简单的整数问题2

题目链接:https://www.acwing.com/problem/content/244/

这题涉及到区间修改,是一个带有懒标记,用于维护区间和的线段树模板题

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=1e5+10;

typedef long long ll;

int n,m;
int a[N];
struct Node
{
    int l,r;
    ll sum,add;
}tr[N*4];

void pushdown(int u)
{
    tr[u<<1].sum+=(tr[u<<1].r-tr[u<<1].l+1)*tr[u].add;
    tr[u<<1|1].sum+=(tr[u<<1|1].r-tr[u<<1|1].l+1)*tr[u].add;
    tr[u<<1].add+=tr[u].add;//注意这里是+=
    tr[u<<1|1].add+=tr[u].add;//注意这里是+=
    tr[u].add=0;
}
void pushup(int u)
{
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void build(int u,int l,int r)
{
    if(l==r)tr[u]={l,r,a[l],0};
    else
    {
        tr[u]={l,r,0,0};
        int mid=tr[u].l+tr[u].r>>1;
        build(u<<1,l,mid);
        build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void modify(int u,int l,int r,ll d)
{
    if(tr[u].l>=l&&tr[u].r<=r)
    {
        tr[u].sum+=(tr[u].r-tr[u].l+1)*d;
        tr[u].add+=d;//注意这里是+=!!!
    }
    else
    {
        pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)modify(u<<1,l,r,d);
        if(r>mid)modify(u<<1|1,l,r,d);
        pushup(u);
    }
}
ll query(int u,int l,int r)
{
    if(tr[u].l>=l&&tr[u].r<=r)return tr[u].sum;
    else
    {
        pushdown(u);
        ll sum=0;
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)sum=query(u<<1,l,r);
        if(r>mid)sum+=query(u<<1|1,l,r);
        return sum;
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    build(1,1,n);
    
    char op[2];
    int l,r;
    ll d;
    while(m--)
    {
        scanf("%s",op);
        if(op[0]=='Q')
        {
            scanf("%d%d",&l,&r);
            printf("%lld\n",query(1,l,r));
        }
        else
        {
            scanf("%d%d%lld",&l,&r,&d);
            modify(1,l,r,d);
        }
    }
    return 0;
}

4,市长海报

题目链接:https://www.acwing.com/problem/content/4344/

 这题有几个很坑的地方,首先,这题中,一个点代表一个长度为1的线段,如2到4就表示一个长度为3的线段,其次这题的数据范围很大,但是海报数量很少,因此我们要离散化,离散化有一个坑,比如有以下数据,(1,10),(1,4)(6,10)离散化后对应的下标为(1,4)(1,2)(2,3),如果这样做,依次给这三个区间贴海报,那么我们最终只能看到两张海报,但答案其实是三张海报,出错的原因在于原本数据中(4,6)这段区间的海报本不会被覆盖,但是离散化后被覆盖掉了,因为离散化的问题,因此我们可以在离散化的时候要在不相邻的两个数插入一个他们之间的任何一个数,例如在4和6之间插入一个5,但是如果数据是(4,5)就不需要插入了,原因是因为一个点代表一个线段,然后要注意,如果想要离散化后下标从1开始,那么就要插入一个极小值作为左边界。

接着讲一下对于这类区间染色的问题如何解决,离散化后我们给每张海报都编一个号,相当于区间修改操作(涉及到区间修改,所以要用到懒标记,但是我们维护的区间没有维护其他任何信息,所以不需要用到pushup操作),因此可以用线段树解决,最后我们在查询每个叶子结点的编号,就可以知道哪些海报的编号是出现过的

ps:以后做区间染色的问题一定要看清楚一个点代表的是一个点还是一个线段!!!!

代码如下:

#include<iostream>
#include<algorithm>
#include<vector>
#include<unordered_map>
#include<cstring>

using namespace std;

typedef pair<int,int>pii;

const int N=1e4+10;

int n,res,idx;//res记录答案,idx给出现过的编号编号
pii seg[N];//记录给定的海报区间,用于给海报编号
unordered_map<int,int>mp;//用于判断哪些编号出现过
vector<int>v;//用于离散化

struct Node
{
    int l,r;
    int id,lazy;//其实这里可以把懒标记当成id,只写一个变量,但为了结构清楚,这里多写一个变量
                //id表示当前区间的编号,lazy表示当前区间被哪个海报覆盖
}tr[N*2*2*4];//第一个2代表n个区间,总共2*n个点,第二个2表示需要对不相邻的数中插入一个数,
            //防止离散化后带来的问题,总共可能插入2*n个点,所以要*2
            //*4表示线段树需要开4倍区间

void pushdown(int u)//用父节点更新子节点的信息
{
    if(tr[u].lazy)//有懒标记才下传,因为初始时为0,防止错误下传导致答案出错
    {
        tr[u<<1].lazy=tr[u<<1|1].lazy=tr[u].lazy;//更新左右儿子的懒标记
        tr[u<<1].id=tr[u<<1|1].id=tr[u].id;//更新左右儿子的id
    }
    tr[u].lazy=0;//清空父节点的懒标记
}
void build(int u,int l,int r)//建立线段树
{
    if(l==r)tr[u]={l,r,0,0};//初始时id和lazy都为0
    else
    {
        tr[u]={l,r,0,0};//都为0
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        //因为并没有维护区间中的任何信息,所以不用pushup
    }
}
void modify(int u,int l,int r,int op)//区间修改
{
    if(tr[u].l>=l&&tr[u].r<=r)//更新当前区间的id和lazy
    {
        tr[u].id=op;
        tr[u].lazy=op;
    }
    else
    {
        pushdown(u);//因为涉及到区间修改,所以要用到pushdown
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)modify(u<<1,l,r,op);
        if(r>mid)modify(u<<1|1,l,r,op);
    }
}
void query(int u,int l,int r)//区间查询
{
    if(tr[u].l==tr[u].r)
    {
        if(!mp.count(tr[u].id)&&tr[u].id)//如果当前节点有编号并且该编号没记录过,说明看到了一张新海报
        {
            mp[tr[u].id]=++idx;//给出现过的编号编号
            res++;//答案数加1
        }
    }
    else
    {
        pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)query(u<<1,l,r);
        if(r>mid)query(u<<1|1,l,r);
    }
}
int find(int x)//二分查找大于等于x的第一个数的下标
{
    return lower_bound(v.begin(),v.end(),x)-v.begin();
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        res=0,idx=0;
        v.clear(),mp.clear();//多组测试数据,记得清空
        v.push_back(-1e9);//插入一个边界,使得离散化后下标从1开始,如果没有这步操作,bulid的区间为0~v.size()-1
        
        scanf("%d",&n);
        for(int i=0;i<n;i++)
        {
            int l,r;
            scanf("%d%d",&l,&r);
            seg[i]={l,r};//记录下来海报的区间
            v.push_back(l),v.push_back(r);//插入区间的左右端点,用于离散化
        }
        //离散化经典操作,排序去重
        sort(v.begin(),v.end());
        v.erase(unique(v.begin(),v.end()),v.end());
        
        int len=v.size();//排序去重后的序列长度
        for(int i=1;i<len;i++)
            if(v[i]-v[i-1]>1)//如果是不相邻的数要在他们中间插入一个数,防止答案错误
                v.push_back(v[i]-1);
        
        sort(v.begin(),v.end());
        
        build(1,1,v.size());//建立线段树
        
        for(int i=0;i<n;i++)
        {
            int l=seg[i].first,r=seg[i].second;
            modify(1,find(l),find(r),i+1);
            //给海报编号,下标从1开始会方便一些,因为后面查询时会判断该点是否有海报,0表示没有海报
        }
        
        query(1,1,v.size());//查询能看到的海报数
        
        printf("%d\n",res);
    }
    return 0;
}

5,就一勾子

题目链接:https://www.acwing.com/problem/content/4345/

这题也是一个涉及到区间修改,用到懒标记的线段数,注意一下这题的区间修改是赋值修改,不是添加数修改,所以修改时是=而不是+=

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=1e5+10;

int n,m;
struct Node
{
    int l,r;
    int sum,add;
}tr[N*4];

void pushdown(int u)
{
    if(tr[u].add)//如果有懒标记才下传
    {
        tr[u<<1].sum=(tr[u<<1].r-tr[u<<1].l+1)*tr[u].add;//注意这题应该是=而不是+=
        tr[u<<1|1].sum=(tr[u<<1|1].r-tr[u<<1|1].l+1)*tr[u].add;
        tr[u<<1].add=tr[u<<1|1].add=tr[u].add;
        tr[u].add=0;
    }
}
void pushup(int u)
{
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void build(int u,int l,int r)
{
    if(l==r)tr[u]={l,r,1,0};
    else
    {
        tr[u]={l,r,0,0};
        int mid=l+r>>1;
        build(u<<1,l,mid);
        build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void modify(int u,int l,int r,int op)
{
    if(tr[u].l>=l&&tr[u].r<=r)
    {
        tr[u].sum=(tr[u].r-tr[u].l+1)*op;//注意这题应该是=而不是+=
        tr[u].add=op;
    }
    else
    {
        pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)modify(u<<1,l,r,op);
        if(r>mid)modify(u<<1|1,l,r,op);
        pushup(u);
        
    }
}
int main()
{
    int t;
    scanf("%d",&t);
    for(int i=1;i<=t;i++)
    {
        scanf("%d%d",&n,&m);
        build(1,1,n);
        while(m--)
        {
            int l,r,op;
            scanf("%d%d%d",&l,&r,&op);
            modify(1,l,r,op);
        }
        printf("Case %d: The total value of the hook is %d.\n",i,tr[1].sum);
    }
    return 0;
}

6,数颜色

题目链接:https://www.acwing.com/problem/content/4346/

这题与第4题很像,都是区间染色问题,不同的是这里一个点表示的是一个点,不是一个线段,例如我们要将区间2~4染成1号色,等价于将2~3和3~4区间段染成1号色,所以我们可以用 i 来表示i-1~i这个区间段,对于给定的l,r传参的时候传l+1,r,这样做还有一个好处就是下标可以从1开始,然后给每个颜色编号等价于区间修改,因为颜色编号是从0开始,这里可以+1,从1开始,这样后面判断起来就会方便一些 

最后在便利每个点,找到该点的编号,该点的编号与上一个点不同的话就记录下来这个点的颜色编号出现的次数+1

代码如下:

#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

const int N=8010;

int n,l,r,c;
struct Node
{
    int l,r;
    int id,lazy;//id表示当前区间的颜色编号,lazy表示当前区间及子区间被哪个颜色覆盖
}tr[N*4];
int cnt[N];//颜色编号出现的次数

void pushdown(int u)//用父节点更新子节点的信息
{
    if(tr[u].lazy)
    {
        tr[u<<1].id=tr[u<<1|1].id=tr[u].id;
        tr[u<<1].lazy=tr[u<<1|1].lazy=tr[u].lazy;
    }
    tr[u].lazy=0;//清空父节点
}
void build(int u,int l,int r)//建立线段树
{
    if(l==r)tr[u]={l,r,0,0};
    else
    {
        tr[u]={l,r,0,0};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
    }
}
void modify(int u,int l,int r,int op)//区间修改
{
    if(l>r)return;//判断一下,如果l大于r就直接返回,因为我们传参传的是l+1
    if(tr[u].l>=l&&tr[u].r<=r)
    {
        tr[u].id=op;
        tr[u].lazy=op;
    }
    else
    {
        pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)modify(u<<1,l,r,op);
        if(r>mid)modify(u<<1|1,l,r,op);
        //没有pushup操作
    }
}
int query(int u,int x)//查询第x个节点的颜色编号
{
    if(tr[u].l==tr[u].r)return tr[u].id;
    else
    {
        pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(x<=mid)query(u<<1,x);
        else query(u<<1|1,x);
    }
}
int main()
{
    while(scanf("%d",&n)!=EOF)
    {
        memset(cnt,0,sizeof cnt);
        build(1,1,N);//建立线段树,1~N区间
        for(int i=1;i<=n;i++)
        {
            scanf("%d%d%d",&l,&r,&c);
            modify(1,l+1,r,c+1);//这里传l+1是为了让下标从1开始,且一个点i表示为i-1~i这个长度为1的线段
                                //c+1是为了方便,后面查询的时候会方便很多,让颜色下标从1开始
        }
        int last=-1;//记录上一个节点的颜色编号
        for(int i=1;i<=N;i++)
        {
            int id=query(1,i);//查询第i个节点的颜色编号
            if(id!=last&&id!=0)//如果当前节点的颜色编号与上一个节点的颜色编号不同且当前节点有被颜色覆盖的话
            {                   //说明第i-1~i这个区间段出现了一个新颜色
                cnt[id]++;//记录这个颜色出现的次数
                last=id;//更新last
            }
        }
        for(int i=1;i<=N;i++)
            if(cnt[i])//如果有颜色编号才输出
                printf("%d %d\n",i-1,cnt[i]);//这里要注意一下,因为我们输入的c+1,所以这里要i-1
        puts(" ");
    }
    return 0;
}

7,奶牛排队

题目链接:https://www.acwing.com/problem/content/1276/

这题只涉及到区间查询,因为不用懒标记就可以解决,我们要维护一个区间最大值和最小值,因此我们查询的时候可以直接返回节点,这样就不用写两个查询函数,而只用一个函数就可以解决问题了

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=5e4+10;

int n,m;
int a[N];
struct Node
{
    int l,r;
    int maxv,minv;
}tr[N*4];

void pushup(Node& root,Node& left,Node& right)
{
    root.maxv=max(left.maxv,right.maxv);
    root.minv=min(left.minv,right.minv);
}
void pushup(int u)
{
    pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r)
{
    if(l==r)tr[u]={l,r,a[l],a[l]};
    else
    {
        tr[u]={l,r};
        int mid=l+r>>1;
        build(u<<1,l,mid);
        build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
Node query(int u,int l,int r)//因为要返回一个节点中的两个信息,所以我们直接返回节点会方便很多,就可以用一个函数解决
{
    if(tr[u].l>=l&&tr[u].r<=r)return tr[u];//当前节点所在的区间被查询区间完全覆盖的话就直接返回
    else
    {
        int mid=tr[u].l+tr[u].r>>1;//找到当前区间的中点,用于查询左右儿子
        if(r<=mid)return query(u<<1,l,r);//如果查询区间在左儿子,直接返回左儿子即可
        else if(l>mid)return query(u<<1|1,l,r);//如果在右儿子,直接返回右儿子即可
        else//说明查询区间与左右儿子都有交集
        {
            Node left=query(u<<1,l,r);//先找到左儿子
            Node right=query(u<<1|1,l,r);//再找到右儿子
            Node root;
            pushup(root,left,right);//再用两个儿子的信息更新出其父节点的信息,返回其父节点的信息
            return root;
        }
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    build(1,1,n);
    
    int l,r;
    while(m--)
    {
        scanf("%d%d",&l,&r);
        Node ans=query(1,l,r);
        printf("%d\n",ans.maxv-ans.minv);
    }
    return 0;
}

8,你能回答这些问题吗

题目链接:https://www.acwing.com/problem/content/description/4347/

这题我们需要用到区间修改的操作,看似要用到懒标记,但是我们关系题目,需要维护的是区间求根,用懒标记很难进行处理,因为我们只能暴力修改每个点,但很显然,这样做必定是会TLE的,但是我们发现一个最大的longlong的数,只需要最多进行七八次就会变成1,因此这里我们可以加一个优化,也就是当前区间的长度等于值时,说明区间内的元素都是1,就可以直接返回,不再继续求根处理了

代码如下:

#include<iostream>
#include<algorithm>
#include<cmath>

using namespace std;

const int N=1e5+10;

typedef long long ll;

int n,m;
ll a[N];
struct Node
{
    int l,r;
    ll sum;
}tr[N*4];

void pushup(int u)
{
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void build(int u,int l,int r)
{
    if(l==r)tr[u]={l,r,a[l]};
    else
    {
        tr[u]={l,r};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void modify(int u,int l,int r)
{
    if(tr[u].r-tr[u].l+1==tr[u].sum)return;//优化,如果当前区间长度等于区间值时,说明区间内元素的值都为1,直接返回
    if(tr[u].l==tr[u].r&&tr[u].l>=l&&tr[u].r<=r)tr[u].sum=sqrt(tr[u].sum);//暴力单点修改
    else
    {
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)modify(u<<1,l,r);
        if(r>mid)modify(u<<1|1,l,r);
        pushup(u);
    }
}
ll query(int u,int l,int r)
{
    if(tr[u].l>=l&&tr[u].r<=r)return tr[u].sum;
    else
    {
        ll ans=0;
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)ans=query(u<<1,l,r);
        if(r>mid)ans+=query(u<<1|1,l,r);
        return ans;
    }
}
int main()
{
    int t=0;
    while(scanf("%d",&n)!=EOF)
    {
        printf("Case #%d:\n",++t);
        for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
        build(1,1,n);
        scanf("%d",&m);
        int op,l,r;
        while(m--)
        {
            scanf("%d%d%d",&op,&l,&r);
            if(l>=r)swap(l,r);//这里记得判断大小,保证l<=r
            if(op==0)modify(1,l,r);
            else printf("%lld\n",query(1,l,r));
        }
        puts(" ");
    }
    
    return 0;
}

9,地道战

题目链接:https://www.acwing.com/problem/content/description/4348/

这题与算法进阶指南里的你能回答这些问题吗很像,做法也类似,我们维护一个当前区间的最长连通长度,最长后缀连通长度,最长前缀连通长度,用一个栈记录下来被摧毁的村庄,便于我们后面修复,查询x点的最长连通长度时,只要判断其是否在左儿子的后缀与右儿子的前缀范围内即可

代码如下:

#include<iostream>
#include<algorithm>
#include<stack>

using namespace std;

const int N=50010;

int n,m;
struct Node
{
    int l,r;
    //sun表示最长连通长度,lmax表示以区间左端点开始的最长连通长度,rmax表示以区间右端点开始的最长连通长度
    int sum,lmax,rmax;
}tr[N*4];
stack<int>s;//用一个栈存储被摧毁的村庄

void pushup(Node& root,Node& left,Node& right)//函数重载,用子节点更新父节点的信息
{
    root.lmax=left.lmax;//根节点的前缀等于左儿子的前缀
    if(left.r-left.l+1==left.lmax)root.lmax+=right.lmax;//如果左儿子的前缀等于左儿子区间大小,还要加上右儿子的前缀
    
    root.rmax=right.rmax;//根节点的后缀等于右儿子的后缀
    if(right.r-right.l+1==right.rmax)root.rmax+=left.rmax;//如果右儿子的后缀等于右儿子区间大小,还要加上左儿子的后缀
    
    //根节点的最长连通长度在左儿子的连通长度,右儿子的连通长度,左儿子的后缀加右儿子的前缀三者取max
    root.sum=max(max(left.sum,right.sum),left.rmax+right.lmax);
}
void pushup(int u)//用子节点更新父节点的信息
{
    pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r)//建立线段树
{
    if(l==r)tr[u]={l,r,1,1,1};//初始时最长连通长度都为1
    else
    {
        tr[u]={l,r,0,0,0};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void modify(int u,int x,int c)//单点修改操作,将x位置的状态改成c
{
    if(tr[u].l==x&&tr[u].r==x)tr[u]={x,x,c,c,c};
    else
    {
        int mid=tr[u].l+tr[u].r>>1;
        if(x<=mid)modify(u<<1,x,c);
        else modify(u<<1|1,x,c);
        pushup(u);
    }
}
int query(int u,int x)//查询x所在的最长连通长度大小
{
    if(tr[u].l==tr[u].r)return tr[u].sum;//说明到边界了,就直接返回边界的大小
    else
    {
        int mid=tr[u].l+tr[u].r>>1;
        //判断当前x是否在u节点的左儿子的最长后缀和右儿子的最长前缀的这个区间上
        //如果在这个区间上,那么这个区间就是x点的最长连通长度
        if(mid-tr[u<<1].rmax+1<=x&&x<=mid+tr[u<<1|1].lmax)return tr[u<<1].rmax+tr[u<<1|1].lmax;
        if(x<=mid)return query(u<<1,x);//不是的话判断如果在左儿子区间上,就递归查找左儿子
        else return query(u<<1|1,x);//不是在左,就是在右,递归查找右儿子
    }
}
int main()
{
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        build(1,1,n);
        char op[2];
        int x;
        while(m--)
        {
            scanf("%s",op);
            if(op[0]=='D')
            {
                scanf("%d",&x);
                modify(1,x,0);//0表示当前村庄被摧毁,该点的最大连通长度为0
                s.push(x);
            }
            else if(op[0]=='Q')
            {
                scanf("%d",&x);
                printf("%d\n",query(1,x));
            }
            else
            {
                if(s.size())//要先判断栈是否为空
                {
                    modify(1,s.top(),1);//1表示当前村庄被修复,该点的最大连通长度至少为1
                    s.pop();
                }
            }
        }
    }
    return 0;
}

10,分配任务

题目链接:https://www.acwing.com/problem/content/description/4349/

11,转换

题目链接:https://www.acwing.com/problem/content/description/4350/

这题根算法提高课里面的维护序列那题非常像,在那个的基础上多了两个问题,首先都涉及到了区间乘和区间加,然后是需要区间的每个数变成每个数,这其实就等价于让乘的懒标记为0,加的懒标记为要变的那个数,最后还有查询的时候可能需要返回区间每个数的2次方的和或3次方的和,因此我们要多维护两个变量,一个区间2次方和,一个区间3次方和,更新的时候推一下公式就可以了用(a*b+c)^2和(a*b+c)^3展开后的式子来更新

记得,一定要开long long !!!!!!

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=1e5+10,mod=10007;

typedef long long ll;

int n,m;
struct Node
{
    int l,r;
    //sum1表示1次方和,sum2表示2次方和,sum3表示3次方和
    int sum1,sum2,sum3,mul,add;
}tr[N*4];

void eval(int u,int mul,int add)//修改当前区间信息,并给当前去加打上懒标记
{
    Node& t=tr[u];
    int len=t.r-t.l+1;//表示区间长度
    t.sum3=((ll)t.sum3 * mul % mod * mul * mul %mod +//推公式修改
            (ll)3*t.sum2 * mul % mod * mul * add % mod +
            (ll)3*t.sum1 * mul % mod * add * add % mod +
            (ll)len * add % mod * add * add % mod) % mod;
    t.sum2=((ll)t.sum2 *  mul * mul % mod +
            (ll)len * add * add % mod +
            (ll)2 * t.sum1 * mul * add % mod)%mod;
    t.sum1=((ll)t.sum1*mul+(ll)len*add)%mod;
    t.mul=(ll)t.mul*mul%mod;//更改懒标记
    t.add=((ll)t.add*mul+add)%mod;//更改懒标记
}
void pushdown(int u)//用父节点更新子节点信息
{
    eval(u<<1,tr[u].mul,tr[u].add);//更新左儿子
    eval(u<<1|1,tr[u].mul,tr[u].add);//更新右儿子
    tr[u].mul=1,tr[u].add=0;//删除父节点的懒标记
}
void pushup(int u)//用子节点更新父节点
{
    tr[u].sum1=(tr[u<<1].sum1+tr[u<<1|1].sum1)%mod;
    tr[u].sum2=(tr[u<<1].sum2+tr[u<<1|1].sum2)%mod;
    tr[u].sum3=(tr[u<<1].sum3+tr[u<<1|1].sum3)%mod;
}
void build(int u,int l,int r)//建立线段树
{
    if(l==r)tr[u]={l,r,0,0,0,1,0};
    else
    {
        tr[u]={l,r,0,0,0,1,0};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void modify(int u,int l,int r,int mul,int add)//区间修改
{
    if(tr[u].l>=l&&tr[u].r<=r)eval(u,mul,add);//如果当前区间被修改区间完全覆盖,修改当前区间,并打上懒标记
    else
    {
        pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)modify(u<<1,l,r,mul,add);
        if(r>mid)modify(u<<1|1,l,r,mul,add);
        pushup(u);
    }
}
int query(int u,int l,int r,int c)//区间查询
{
    if(tr[u].l>=l&&tr[u].r<=r)//看我们需要返回哪个值,返回对应的值
    {
        if(c==1)return tr[u].sum1;
        else if(c==2)return tr[u].sum2;
        else return tr[u].sum3;
    }
    else
    {
        pushdown(u);
        int ans=0;
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)ans=query(u<<1,l,r,c)%mod;
        if(r>mid)ans=(ans+query(u<<1|1,l,r,c))%mod;
        return ans;
    }
}
int main()
{
    while(scanf("%d%d",&n,&m),n||m)
    {
        build(1,1,n);
        int op,x,y,c;
        while(m--)
        {
            scanf("%d%d%d%d",&op,&x,&y,&c);
            if(op==1)modify(1,x,y,1,c);
            else if(op==2)modify(1,x,y,c,0);
            else if(op==3)modify(1,x,y,0,c);
            else printf("%d\n",query(1,x,y,c)%mod);
        }
    }
    return 0;
}

12,花瓶和鲜花

题目链接:https://www.acwing.com/problem/content/description/4351/

这题根算法进阶指南中旅馆那题很像 https://blog.csdn.net/m0_74911187/article/details/132828496?spm=1001.2014.3001.5501

我们维护区间中空花瓶的个数,插入了鲜花就当前区间的空花瓶个数清0,丢弃的鲜花就将当前区间的空花瓶个数变成区间长度大小,然后我们可以固定给定的左边界,二分右边界 来查找第一个插入的花瓶和最后一个插入的花瓶的位置,但是这样的花时间复杂度是O(NlogN^2),当数据范围大的时候会超时,首先看一下这个方法的代码

#include<iostream>
#include<algorithm>

using namespace std;

const int N=5e4+10;

int n,m;
struct Node
{
    int l,r;
    int sum,lazy;
}tr[N*4];

void pushdown(int u)
{
    if(tr[u].lazy!=-1)
    {
        tr[u<<1].sum=tr[u].lazy*(tr[u<<1].r-tr[u<<1].l+1);
        tr[u<<1|1].sum=tr[u].lazy*(tr[u<<1|1].r-tr[u<<1|1].l+1);
        tr[u<<1].lazy=tr[u<<1|1].lazy=tr[u].lazy;
        tr[u].lazy=-1;
    }
}
void pushup(int u)
{
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void build(int u,int l,int r)
{
    if(l==r)tr[u]={l,r,1,-1};
    else
    {
        tr[u]={l,r,0,-1};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void modify(int u,int l,int r,int op)
{
    if(tr[u].l>=l&&tr[u].r<=r)
    {
        tr[u].sum=op*(tr[u].r-tr[u].l+1);
        tr[u].lazy=op;
    }
    else
    {
        pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)modify(u<<1,l,r,op);
        if(r>mid)modify(u<<1|1,l,r,op);
        pushup(u);
    }
}
int query(int u,int l,int r)
{
    if(tr[u].l>=l&&tr[u].r<=r)return tr[u].sum;
    else
    {
        pushdown(u);
        int sum=0;
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)sum=query(u<<1,l,r);
        if(r>mid)sum+=query(u<<1|1,l,r);
        return sum;
    }
}
int find(int l,int r,int x)//l为给定的左区间边界,我们二分右边界,在区间中查找第x个空花瓶的位置
{
    int L=l;//固定左边界
    while(l<r)
    {
        int mid=l+r>>1;
        if(query(1,L,mid)>=x)r=mid;//如果区间L~mid中空花瓶的个数比x多,就缩小右边界
        else l=mid+1;//否则就扩大右边界
    }
    return r;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        build(1,1,n);
        int op,l,x;
        while(m--)
        {
            scanf("%d%d%d",&op,&l,&x);
            l++;
            if(op==1)
            {
                int sum=query(1,l,n);
                if(sum==0)puts("Can not put any one.");
                else
                {
                    int ansl=find(l,n,1);
                    int ansr=0;
                    if(sum>=x)ansr=find(l,n,x);//判断l~n中空花瓶的个数是否大于要插入的花的数量的个数
                    else ansr=find(l,n,sum);//如果要插入的花的个数比空花瓶的个数多的花,那么只能插入sum朵花
                    printf("%d %d\n",ansl-1,ansr-1);
                    modify(1,ansl,ansr,0);
                }
            }
            else
            {
                x++;
                printf("%d\n",(x-l+1)-query(1,l,x));
                modify(1,l,x,1);
            }
        }
        puts(" ");
    }
    return 0;
}

因为每次二分都要调用query函数,所以这里是logN^2的一个复杂度,其实不用每次都调用query,我们可以在线段树上二分,首先求出来1~l-1区间中空花瓶的个数num,那么我们第一个插入花的花瓶的位置在1~n区间中就是num+1,然后要注意以下,要插入的花的个数不能比空花瓶的个数要多,否则会越界,实现代码如下:时间复杂度为O(NlogN)

#include<iostream>
#include<algorithm>

using namespace std;

const int N=5e4+10;

int n,m;
struct Node
{
    int l,r;
    int sum,lazy;//sum表示当前节点表示的区间中空花瓶的个数
                //lazy为懒标记,为1表示当前节点表示的区间都为空花瓶,为0表示都为非空花瓶,-1表示没有懒标记
}tr[N*4];

void pushdown(int u)
{
    if(tr[u].lazy!=-1)//如果有懒标记才下传
    {
        tr[u<<1].sum=tr[u].lazy*(tr[u<<1].r-tr[u<<1].l+1);//更新左右儿子的区间空花瓶的个数
        tr[u<<1|1].sum=tr[u].lazy*(tr[u<<1|1].r-tr[u<<1|1].l+1);
        tr[u<<1].lazy=tr[u<<1|1].lazy=tr[u].lazy;
        tr[u].lazy=-1;
    }
}
void pushup(int u)//用子节点更新父节点的信息
{
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void build(int u,int l,int r)//建立线段树,记得初始化懒标记为-1
{
    if(l==r)tr[u]={l,r,1,-1};
    else
    {
        tr[u]={l,r,0,-1};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void modify(int u,int l,int r,int op)//区间修改
{
    if(tr[u].l>=l&&tr[u].r<=r)//当前区间被修改区间完全覆盖,修改当前区间空花瓶的个数,并在当前节点打上懒标记
    {
        tr[u].sum=op*(tr[u].r-tr[u].l+1);
        tr[u].lazy=op;
    }
    else
    {
        pushdown(u);//记得pushdown更新儿子节点的信息
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)modify(u<<1,l,r,op);
        if(r>mid)modify(u<<1|1,l,r,op);
        pushup(u);
    }
}
int query(int u,int l,int r)//区间查询操作,查询l~r区间中空花瓶的个数
{
    if(tr[u].l>=l&&tr[u].r<=r)return tr[u].sum;
    else
    {
        pushdown(u);
        int sum=0;
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)sum=query(u<<1,l,r);
        if(r>mid)sum+=query(u<<1|1,l,r);
        return sum;
    }
}
int find(int u,int x)//从根节点开始二分寻找第x个空花瓶
{
    if(tr[u].l==tr[u].r)return tr[u].l;//说明走到底了,即找到花瓶的位置了,返回花瓶的下标
    else
    {
        pushdown(u);//要记得更新子节点的信息,不然可能会查询错误
        if(x<=tr[u<<1].sum)return find(u<<1,x);//说明区间左边的个数要x多
        else return find(u<<1|1,x-tr[u<<1].sum);//说明区间左边的个数比x少,递归右儿子进行查找,同时要修改x
    }
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        build(1,1,n);
        int op,l,x;
        while(m--)
        {
            scanf("%d%d%d",&op,&l,&x);
            l++;//因为下标是从0开始的,所以要这里要++,后面求的答案要--
            if(op==1)
            {
                int sum=query(1,l,n);//首先查询l~n这个区间中空花瓶的个数
                if(sum==0)puts("Can not put any one.");//没有空花瓶说明不能插入
                else
                {
                    int num=0;//查询1~l-1这个区间中空花瓶的个数
                    if(l-1>=1)
                        num=query(1,1,l-1);
                        
                    int ansl=find(1,num+1);//查询从l开始能插入的第一个花瓶就是区间1~n中的第num+1个花瓶
                    int ansr=find(1,min(num+x,tr[1].sum));//这里同理,但是要判断一下需要插入的花的数量是否比当前所有空花瓶的数量大小
                    printf("%d %d\n",ansl-1,ansr-1);
                    modify(1,ansl,ansr,0);//当前区间都插了花,修改状态,表示当前区间都有花
                }
            }
            else
            {
                x++;
                printf("%d\n",(x-l+1)-query(1,l,x));//用区间长度减去这个区间中空花瓶的个数就是要丢弃的花的个数
                modify(1,l,x,1);//同时丢弃了花不要忘记修改当前区间的状态
            }
        }
        puts(" ");
    }
    return 0;
}

13,约会安排

题目链接:https://www.acwing.com/problem/content/4352/

这题要维护两个线段树,一个维护与屌丝的约会安排,另一个维护与女神的约会安排,对于与屌丝的约会,我们就只在与屌丝的线段树中查询即可,对于与女神的约会,我们判断女神的线段树中是否还有可约会的时间段(一定要先判断这个,再这里卡了一两天),如果有的话,先在与屌丝的线段树中查找,没有再从与女神的线段树中查找

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=1e5+10;

int n,m;
struct Node
{
    int l,r;
    int s,ls,rs,lazy;
    //s表示当前节点表示的最长连续可安排约会的空时间段
    //ls表示当前节点区间左端点开始的最长可安排约会的空时间段
    //rs表示当前节点区间右端点结尾的最长可安排约会的空时间段
    //lazy为懒标记,-1表示没有懒标记
    //1表示当前节点和子节点都是可安排约会的时间段,0表示当前节点和子节点都是不可安排约会的时间段
}trd[N*4],trn[N*4];
//trd维护跟屌丝的约会安排,trn维护跟女神的约会安排

void eval(Node& u,int op)//更新子节点的信息
{
    u.s=u.ls=u.rs=op*(u.r-u.l+1);
    u.lazy=op;//给子节点打上懒标记
}
void pushdown(Node tr[],int u)//用父节点更新子节点信息
{
    if(tr[u].lazy!=-1)//如果有懒标记才下传给子节点
    {
        eval(tr[u<<1],tr[u].lazy);
        eval(tr[u<<1|1],tr[u].lazy);
    }
    tr[u].lazy=-1;//情况父节点的懒标记
}
void pushup(Node& root,Node& left,Node& right)//用子节点更新父节点
{
    root.ls=left.ls;//当前节点的最长前缀等于左儿子的最长前缀
    if(left.ls==left.r-left.l+1)root.ls+=right.ls;//如果左儿子的最长前缀等于左儿子区间大小,就再加上右儿子的前缀
    root.rs=right.rs;//当前节点的最长后缀等于右儿子的最长后缀
    if(right.rs==right.r-right.l+1)root.rs+=left.rs;//如果右儿子的最长后缀等于右儿子的区间大小,就再加上左儿子的后缀
    root.s=max(max(left.s,right.s),left.rs+right.ls);//当前的最长连续空时间段为左儿子的和右儿子的还有左儿子加右儿子的
}
void pushup(Node tr[],int u)//用子节点更新父节点
{
    pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(Node tr[],int u,int l,int r)//建立线段树
{
    if(l==r)tr[u]={l,r,1,1,1,-1};
    else
    {
        tr[u]={l,r,r-l+1,r-l+1,r-l+1,-1};//最长可安排约会的区间都为区间长度大小
        int mid=l+r>>1;
        build(tr,u<<1,l,mid),build(tr,u<<1|1,mid+1,r);
        pushup(tr,u);
    }
}
void modify(Node tr[],int u,int l,int r,int op)//区间修改
{
    if(tr[u].l>=l&&tr[u].r<=r)eval(tr[u],op);
    else
    {
        pushdown(tr,u);
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid)modify(tr,u<<1,l,r,op);
        if(r>mid)modify(tr,u<<1|1,l,r,op);
        pushup(tr,u);
    }
}
int query(Node tr[],int u,int x)//查询区间长度为x的连续可安排时间的最靠前的一段区间长度
{
    if(tr[u].l==tr[u].r)return tr[u].l;//如果到边界了,就直接返回边界的下标
    pushdown(tr,u);//一定要记得pushdown
    if(tr[u<<1].s>=x)return query(tr,u<<1,x);//因为要找最靠前的区间段,因此先找左儿子
    else if(tr[u<<1].rs+tr[u<<1|1].ls>=x)return tr[u<<1].r-tr[u<<1].rs+1;//再找左儿子的后缀和右儿子的前缀
    else return query(tr,u<<1|1,x);//最后找右儿子
}
int main()
{
    int T;
    scanf("%d",&T);
    for(int t=1;t<=T;t++)
    {
        printf("Case %d:\n",t);
        scanf("%d%d",&n,&m);
        build(trd,1,1,n),build(trn,1,1,n);//建立线段树
        char op[10];
        int x,y;
        while(m--)
        {
            scanf("%s",op);
            if(op[0]=='S')//说明清空这段时间的约会安排
            {
                scanf("%d%d",&x,&y);
                puts("I am the hope of chinese chengxuyuan!!");
                modify(trd,1,x,y,1);//屌丝和女神的约会安排都要清空
                modify(trn,1,x,y,1);
            }
            else if(op[0]=='D')//查询屌丝的约会安排
            {
                scanf("%d",&x);
                if(trd[1].s<x)puts("fly with yourself");//表示没有可约会的时间段
                else
                {
                    int idx=query(trd,1,x);//查询可约会的时间段的最靠前的位置的下标
                    printf("%d,let's fly\n",idx);
                    modify(trd,1,idx,idx+x-1,0);//查询到了以后要将这段区间设置成不可安排
                }
            }
            else
            {
                scanf("%d",&x);
                if(trn[1].s<x)puts("wait for me");//看能否有安排女神的约会时间段,一定要先判断!!!!!
                else
                {
                    int idx=0;
                    if(trd[1].s>=x)idx=query(trd,1,x);//在屌丝中有可约会的时间段
                    else idx=query(trn,1,x);//在女神中有可约会的时间段
                    modify(trd,1,idx,idx+x-1,0);//女神和屌丝的这段约会时间都要修改成不可安排
                    modify(trn,1,idx,idx+x-1,0);
                    printf("%d,don't put my gezi\n", idx);
                }
            }
        }
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值