• GSS系列题解——最大子段和系列


    开坑啦!

    2019 3/28 以前一直不知道怎么搞最大子段和,如今终于可以学习,其实真的很简单啊。

    2019 3/29 树链剖分上最大子段和也OK啦

    前置技能:线段树

    题目大意:询问区间[l,r]的最大字段和

    定义:

    struct kkk{
        int val;	//表示该区间的权值和
        int left;	//表示该区间的前缀最大和
        int right;	//表示该区间的后缀最大和
        int middle;	//表示该区间的最大子段和
        kkk(){val=left=right=middle=0;} //清0
    }tree[maxn];
    

    大家都应知道,线段树基本原理,那么最大子段和放在线段树上,其实就是两个区间的合并时怎么将区间关系,pushup区间的问题。

    下面给出两个区间合并的方式:合并的区间为res

    合并保证x是左区间,y是右区间,x,y相邻。

    首先是val的合并,很简单,区间x的val+区间y的val

    res.val=x.val+y.val;
    

    然后是left的合并,前缀最大和只有两种情况,要么是x区间的前缀最大和,要么是x的权值和+y的前缀最大和。结果是这两种情况的max值。证明:贪心。

    那么right的合并也差不多,要么是y区间的后缀最大和,要么是y的权值+x的后缀最大和。

    至于middle就分几种情况:

    1.x区间的middle
    2.y区间的middle
    3.x区间的后缀最大和+y区间的前缀最大和
    

    代码实现合并操作:

    kkk merge(kkk x,kkk y){
        kkk res;
        res.val=x.val+y.val;
        res.middle=max(
        		   max(x.middle,y.middle),
        		   x.right+y.left
        		   );
        res.left=max(x.left,x.val+y.left);
        res.right=max(y.right,y.val+x.right);
        return res;
    }
    

    那么pushup操作即是将左儿子和右儿子合并。

    查询操作

    我们查询时返回一个区间,这个区间在查询时会合并成我们想要查询的那个区间,那么那个区间的middle就是我们要求的答案。

    kkk query(int node,int begin,int end,int x,int y){
        if(begin>=x&&end<=y)return tree[node];		//包含该区间,直接返回
        int mid=(begin+end)>>1;
        kkk Left,Right,res;
        if(x<=mid) Left=query(L(node),begin,mid,x,y);	//查询左区间
        if(y>mid) Right=query(R(node),mid+1,end,x,y);	//查询右区间
        return merge(Left,Right);				//合并成一个区间
    }
    

    经过思考,我们会发现Left和Right决不是返回整个区间,而是我们要求的区间在Left或Right区间的部分。所以我们可以直接将Left和Right区间合并。

    最后输出的就是查询到区间的middle

    printf("%d
    ",query(1,1,n,x,y).middle);
    

    下面放完整代码:

    #include<bits/stdc++.h>
    #define maxn 1000001
    #define L(node) (node<<1)
    #define R(node) ((node<<1)|1)
    using namespace std;
    struct kkk{
        int val,left,right,middle;
        kkk(){val=left=right=middle=0;}
    }tree[maxn];
    int n,m,x,y,a[maxn];
    kkk merge(kkk x,kkk y){
        kkk res;
        res.val=x.val+y.val;
        res.middle=max(max(x.middle,y.middle),x.right+y.left);
        res.left=max(x.left,x.val+y.left);
        res.right=max(y.right,y.val+x.right);
        return res;
    }
    void build(int node,int begin,int end){
        if(begin==end){
            tree[node].val=tree[node].left=tree[node].right=tree[node].middle=a[begin];
            return ;
        }else{
            int mid=(begin+end)>>1;
            build(L(node),begin,mid);
            build(R(node),mid+1,end);
            tree[node]=merge(tree[L(node)],tree[R(node)]);
        }
    }
    kkk query(int node,int begin,int end,int x,int y){
        if(begin>=x&&end<=y)return tree[node];		//包含该区间,直接返回
        int mid=(begin+end)>>1;
        if(y<=mid) return query(L(node),begin,mid,x,y);
        if(x>mid)return query(R(node),mid+1,end,x,y);
        kkk Left,Right;
        if(x<=mid) Left=query(L(node),begin,mid,x,y);	//查询左区间
        if(y>mid) Right=query(R(node),mid+1,end,x,y);	//查询右区间
        return merge(Left,Right);				//合并成一个区间
    }
    int main(){
        scanf("%d",&n);
        for(int i=1;i<=n;i++)scanf("%d",&a[i]);
        build(1,1,n);
        scanf("%d",&m);
        for(int i=1;i<=m;i++){
            scanf("%d%d",&x,&y);
            printf("%d
    ",query(1,1,n,x,y).middle);
        }
    }
    

    修改操作

    修改操作和普通线段树没有什么区别,求最大子段和只和pushup和查询操作有直接关系,其他的操作几乎一样。当然,这里的修指的是区间赋值,如果是区间加的话可能要更加复杂。

    单点修代码:

    void update(int node,int begin,int end,int x,int val){
        if(begin==end){
            tree[node].val=tree[node].left=tree[node].right=tree[node].middle=val;
            return ;
        }else{
            int mid=(begin+end)>>1;
            if(x<=mid)update(L(node),begin,mid,x,val);
            if(x>mid) update(R(node),mid+1,end,x,val);
            pushup(node);
        }
    }
    

    区间修

    其实和线段树也是一模一样的,不信看代码:

    void update(int node,int begin,int end,int x,int y,int val){
        if(begin>=x&&end<=y){
            change(node,begin,end,val);
            return ;
        }else{
            pushdown(node,begin,end);
            int mid=(begin+end)>>1;
            if(x<=mid)update(L(node),begin,mid,x,y,val);
            if(y>mid) update(R(node),mid+1,end,x,y,val);
            tree[node]=merge(tree[L(node)],tree[R(node)]);
        }
    }
    

    容易发现,整体的结构和线段树的区间修是一模一样的,但是赋值变成了change,更新pushdown很迷,下面一起来看一下。

    change操作是赋值操作,也就是和线段树一模一样的。

    void change(int node,int begin,int end,int val){
        tree[node].val=(end-begin+1)*val;
        tree[node].left=tree[node].right=tree[node].middle=val;
        tree[node].tag=val;tree[node].flag=1;
    }
    

    可以发现,这里有个flag,表示的是该区间有没有被打tag标记。我们不可以直接让tag=0来判断标记,因为可能tag是赋值为0的,所以我们要用flag来记录有没有打tag。这是一个小细节。

    剩下就是pushdown操作了,就是注意一下用flag来判断就好。其他赋值都可以用change来代替,写起来方便很多。

    void pushdown(int node,int begin,int end){
        if(tree[node].flag==1){
            int mid=(begin+end)>>1;
            change(L(node),begin,mid,tree[node].tag);
            change(R(node),mid+1,end,tree[node].tag);
            tree[node].tag=0;tree[node].flag=0;
        }
    }
    

    那么区间修的代码就这些了,直接调用就可以了。

    区间修代码:

    void change(int node,int begin,int end,int val){
        tree[node].val=(end-begin+1)*val;
        tree[node].left=tree[node].right=tree[node].middle=val;
        tree[node].tag=val;tree[node].flag=1;
    }
    void pushdown(int node,int begin,int end){
        if(tree[node].flag==1){
            int mid=(begin+end)>>1;
            change(L(node),begin,mid,tree[node].tag);
            change(R(node),mid+1,end,tree[node].tag);
            tree[node].tag=0;tree[node].flag=0;
        }
    }
    void update(int node,int begin,int end,int x,int y,int val){
        if(begin>=x&&end<=y){
            change(node,begin,end,val);
            return ;
        }else{
            pushdown(node,begin,end);
            int mid=(begin+end)>>1;
            if(x<=mid)update(L(node),begin,mid,x,y,val);
            if(y>mid) update(R(node),mid+1,end,x,y,val);
            tree[node]=merge(tree[L(node)],tree[R(node)]);
        }
    }
    

    允许有空集

    前面的代码里都是不允许空集的,那么允许空集是怎样的呢?

    其实只需要改一点就可以了。赋值判断一下大于0还是小于0,如果小于0就在left,right,middle赋值为0。

    定义:

    struct kkk{
        int val,left,right,middle,tag,flag;
        kkk(){left=right=middle=val=0;}	//清0
    }tree[maxn];
    

    query:

    kkk query(int node,int begin,int end,int x,int y){
        if(begin>=x&&end<=y)return tree[node];
        int mid=(begin+end)>>1;
        pushdown(node,begin,end);
        kkk Left,Right;
        if(x<=mid) Left=query(L(node),begin,mid,x,y);
        if(y>mid) Right=query(R(node),mid+1,end,x,y);
        return merge(Left,Right);
    }
    

    update:

    void change(int node,int begin,int end,int val){
        tree[node].val=(end-begin+1)*val;
        tree[node].left=tree[node].right=tree[node].middle=max(val,0);	//负数不如0
        tree[node].tag=val;tree[node].flag=1;
    }
    void pushdown(int node,int begin,int end){
        if(tree[node].flag==1){
            int mid=(begin+end)>>1;
            change(L(node),begin,mid,tree[node].tag);
            change(R(node),mid+1,end,tree[node].tag);
            tree[node].tag=0;tree[node].flag=0;
        }
    }
    void update(int node,int begin,int end,int x,int y,int val){
        if(begin>=x&&end<=y){
            change(node,begin,end,val);
            return ;
        }else{
            pushdown(node,begin,end);
            int mid=(begin+end)>>1;
            if(x<=mid)update(L(node),begin,mid,x,y,val);
            if(y>mid) update(R(node),mid+1,end,x,y,val);
            tree[node]=merge(tree[L(node)],tree[R(node)]);
        }
    }
    

    build:

    void build(int node,int begin,int end){
        if(begin==end){
            tree[node].val=a[begin];tree[node].flag=0;
            tree[node].left=tree[node].right=tree[node].middle=max(0,a[begin]);		//负数不如0
            return ;
        }else{
            int mid=(begin+end)>>1;
            build(L(node),begin,mid);
            build(R(node),mid+1,end);
            tree[node]=merge(tree[L(node)],tree[R(node)]);
        }
    }
    

    树上最大子段和

    终于到树链剖分啦,传说什么区间问题都能搬到树上,最大子段和也不例外。

    先来看看GSS7,要支持两个操作。一查询,二修改。

    建树就不用说了吧。

    首先修改操作,和普通树链剖分一样,用上面的区间修代码加树链剖分就OK辽。

    树链剖分过程
    void linkadd(int x,int y,int z){
        int fx=top[x],fy=top[y];
        while(fx!=fy){
            if(dep[fx]<dep[fy])swap(x,y),swap(fx,fy);
            update(1,1,seg[0],seg[fx],seg[x],z);
            x=father[fx];fx=top[x];
        }
        if(dep[x]>dep[y])swap(x,y);
        update(1,1,seg[0],seg[x],seg[y],z);
    }
    

    重点是查询!

    查询

    我们知道树链剖分的核心就在于把树上的链化为序列。所以我们在树链剖分的过程中将链上的序列合并,最后得出的序列即为要求序列。

    那么怎么在跳重链的过程将序列合并呢?

    我们维护两个结构体L和R,分别表示左链上的序列和右链上的序列。什么意思呢?两个点x和y的最近公共祖先F只有一个。那么我们称F到x那一段为左链,F到y那一段为右链。

    跳重链的过程可能是一下跳到F到x那一段,一下跳到F到y那一段,所以如果在F到x那一段,我们将那段区间加入到L,不然加入到R。

    最后将L和R合并即是最后序列。小细节:要将L的left和right互换再合并。

    还是挺复杂的,看看代码注释比较有帮助:

    kkk linkquery(int x,int y){
    	kkk L,R;
    	int fx=top[x],fy=top[y];
    	while(fx!=fy){				//跳重链
    		if(dep[fx]<dep[fy]){
    			R=merge(query(1,1,n,seg[fy],seg[y]),R);     //跳到了F到y那边,和R合并
    			y=father[fy];fy=top[y];
    		}else{
    			L=merge(query(1,1,n,seg[fx],seg[x]),L);     //跳到了F到x那边,和L合并
    			x=father[fx];fx=top[x];
    		}
    	}
    	if(dep[x]>dep[y]){
    		L=merge(query(1,1,n,seg[y],seg[x]),L);      	//同上
    	}else{
    		R=merge(query(1,1,n,seg[x],seg[y]),R);
    	}
    	swap(L.left,L.right);				//交换
    	return merge(L,R);				//合并L,R
    }
    

    剩下也差不多,代码比较长,放剪切板里:GSS7代码

    To be continue……

  • 相关阅读:
    【CodeForces 438D 】The Child and Sequence
    【雅礼集训 2017 Day1】市场
    【POJ2528】Mayor's posters
    【NOIP模拟】图论题Graph
    【BZOJ2654】Tree
    【NOIP模拟】函数
    【NOIP模拟】箱子
    【CQOI2014】数三角形
    【USACO2009Feb】股票市场
    【APIO2009-3】抢掠计划
  • 原文地址:https://www.cnblogs.com/hyfhaha/p/10678349.html
Copyright © 2020-2023  润新知