山海经题解(线段树)

题目:

  天立oi-线段树基础-山海经

题解:

  分析一下,我们可知此题考察线段树

  首先,我们需要求任意一个区间的最大子串和。根据线段树的构建方法,每一个节点的值是由其左右两个儿子求得的,所以我们需要在每一个节点上存上能够用来给父节点更新的数据。

  一是基本的区间和(可以使用前缀和,因为本题不需要修改),二是区间的最大子串和。但这两项不可以满足更新的需要。于是我们再加上一个以左边边界为起始的子串的最大字串和,称为左隧道;和以右边边界为结尾的子串的最大字串和,称为右隧道。另外,还需要每一个值对应的左右边界。

  考虑如果在一个查询区间内(l,r)内有(l,m)和(m+1,r)两个区间,如果我们有(l,m)的右隧道和(m+1,r)的左隧道,那便可以拼出一个新的值作为下一个节点的最大子串和的备选值。(左隧道,右隧道是这个叫法是听说的,我也不知道为什么)

  所以我们可以定义以下结构体作为线段树的节点

struct Sum{
    int sum,l,r;
};//基本类型,(l,r)和为sum
struct S_Tree{ 
    Sum sum,maxn,lsum,rsum;//四个值,区间和,区间最大子串和,左隧道,右隧道
};

  接着是建树,几乎和普通的没什么区别

复制代码
void build(int p,int l,int r)
{
    t[p].sum=(Sum){sum[r]-sum[l-1],l,r};//用了一个前缀和求区间和,但其实不用
    if(l==r)//叶子
    {
        t[p].lsum=t[p].rsum=t[p].maxn=t[p].sum;//每一个叶子的最大子串和,左隧道,右隧道和区间和相等
        return ;
    }
    int m=(l+r)>>1;
    build(p<<1,l,m);
    build(p<<1|1,m+1,r);
    pushup(p);//向上更新
}
复制代码

  注意到build函数中的pushup函数,这个部分很重要,关系到建树的正确。

  对于每一个非叶子节点,需要更新最大子串和以及左隧道和右隧道。

  这里根据代码解释。

void pushup(int p)
{//S_max()是自定义的比较函数,下方有详细函数
    t[p].lsum=S_max(t[p<<1].lsum,(Sum){t[p<<1].sum.sum+t[p<<1|1].lsum.sum,t[p<<1].sum.l,t[p<<1|1].lsum.r});//左隧道的更新是左节点的左隧道,左节点的区间和与右节点的左隧道之和两值的更大值
    t[p].rsum=S_max(t[p<<1|1].rsum,(Sum){t[p<<1].rsum.sum+t[p<<1|1].sum.sum,t[p<<1].rsum.l,t[p<<1|1].sum.r});//右隧道的更新是右节点的右隧道,右节点的区间和与左节点的右隧道之和两值的更大值
    t[p].maxn=S_max(S_max(S_max(t[p<<1].maxn,t[p<<1|1].maxn),S_max(t[p].lsum,t[p].rsum)),S_max(t[p].sum,(Sum){t[p<<1].rsum.sum+t[p<<1|1].lsum.sum,t[p<<1].rsum.l,t[p<<1|1].lsum.r}));
}//区间最大子串和是左节点区间最大子串和,右节点区间最大子串和,当前区间左隧道,当前区间右隧道,当前区间区间和,以及左节点右隧道加上右节点左隧道之和的最大值

   以下是S_max函数,这个函数根据要求(对于每个查询,有a≤i≤j≤b((a,b)是查询区间,(i,j)是查询结果),如果有多组解,则输出i最小的,如果i也相等,则输出j最小的解)做特殊判断。

复制代码
Sum S_max(Sum a,Sum b)
{
    if(a.sum>b.sum) return a;//a的值大于b
    else if(a.sum<b.sum) return b;//a的值小于b
    else if(a.l<b.l) return a;//a的值等于b,则判断a与b的左边界。a的左边界小于b的左边界
    else if(a.l>b.l) return b;//a的左边界大于b的右边界
    else if(a.r<b.r) return a;//a的左边界与b的左边界相等,判断a与b的右边界。a的右边界小于b的右边界
    else return b;a的右边界小于等于b的右边界
}
复制代码

  建树完成后便是查询了。

  类比区间和的查询,我们也可以构造出对区间最大子串和的查询。

  这里也根据代码解释。

复制代码
S_Tree ask(int p,int l,int r)
{//每一个节点的区间和的值也是每个节点的所在区间
    if(l<=t[p].sum.l&&r>=t[p].sum.r) return t[p];//类似于区间和的查找,当前区间在查询区间内,便直接返回节点记录的值
    int m=(t[p].sum.l+t[p].sum.r)>>1,sum_=0,l_,r_;//sum_,l_,r_用来保存查询的区间和以及其左右边界,注意sum_的初始化很重要(我因为这个卡了一周)
    l_=max(m+1,l);//l_初始化为m+1和l的更大值,m+1是因为可能没有左子树,l是因为可能m+1不在区间内
r_
=min(m,r);//r_初始化为m和r的更小值,m是应为可能没有右子树,r可能是m不在区间内 S_Tree lc=W,rc=W,root=W;//root记录当前新算出的值,lc和rc分别记录左儿子和右儿子的值,W是根据上文中提到的对查询结果的要求赋的值,保证第一次更新一定成功 if(l<=m) lc=ask(p<<1,l,r),sum_+=lc.sum.sum,l_=lc.sum.l;//有左儿子便向左儿子查询,并更新sum_和l_的值 if(r>m) rc=ask(p<<1|1,l,r),sum_+=rc.sum.sum,r_=rc.sum.r;//有右儿子便向右儿子查询,并更新sum_和r_的值 root.sum=(Sum){sum_,l_,r_};//记录root中的sum(当前区间和)的值 if(l<=m) root.lsum=S_max(lc.lsum,(Sum){lc.sum.sum+rc.lsum.sum,lc.sum.l,rc.lsum.r});//有左儿子便更新左隧道 if(r>m) root.rsum=S_max(rc.rsum,(Sum){rc.sum.sum+lc.rsum.sum,lc.rsum.l,rc.sum.r});//有右儿子便更新右隧道 root.maxn=S_max(S_max((Sum){lc.rsum.sum+rc.lsum.sum,lc.rsum.l,rc.lsum.r},S_max(lc.maxn,rc.maxn)),S_max(root.lsum,root.rsum)); }//最后更新区间最大子串和,和pushup函数中的一样,只是去掉了当前区间的区间和(其实上面也不需要)
复制代码

  主要函数写完了,只剩下输入输出等操作,根据题目描述编写即可。

  以下为完整代码以及部分上面没有提到的代码注释

复制代码
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int MAXN=1e5+10;//最大的区间值
const int MINN=-1e9-10;//最小的区间和
int n,a[MAXN],sum[MAXN];//这里的sum保存前缀和
struct Sum{
    int sum,l,r;
};
struct S_Tree{ 
    Sum sum,maxn,lsum,rsum;
}t[MAXN<<2];
const S_Tree W=(S_Tree){(Sum){MINN,MAXN,MAXN},(Sum){MINN,MAXN,MAXN},(Sum){MINN,MAXN,MAXN},(Sum){MINN,MAXN,MAXN}};//W为用来初始化
Sum S_max(Sum a,Sum b)
{
    if(a.sum>b.sum) return a;
    else if(a.sum<b.sum) return b;
    else if(a.l<b.l) return a;
    else if(a.l>b.l) return b;
    else if(a.r<b.r) return a;
    else return b;
}
void pushup(int p)
{
    t[p].lsum=S_max(t[p<<1].lsum,(Sum){t[p<<1].sum.sum+t[p<<1|1].lsum.sum,t[p<<1].sum.l,t[p<<1|1].lsum.r});
    t[p].rsum=S_max(t[p<<1|1].rsum,(Sum){t[p<<1].rsum.sum+t[p<<1|1].sum.sum,t[p<<1].rsum.l,t[p<<1|1].sum.r});
    t[p].maxn=S_max(S_max(S_max(t[p<<1].maxn,t[p<<1|1].maxn),S_max(t[p].lsum,t[p].rsum)),S_max(t[p].sum,(Sum){t[p<<1].rsum.sum+t[p<<1|1].lsum.sum,t[p<<1].rsum.l,t[p<<1|1].lsum.r}));
}
void build(int p,int l,int r)
{
    t[p].sum=(Sum){sum[r]-sum[l-1],l,r};
    if(l==r)
    {
        t[p].lsum=t[p].rsum=t[p].maxn=t[p].sum;
        return ;
    }
    int m=(l+r)>>1;
    build(p<<1,l,m);
    build(p<<1|1,m+1,r);
    pushup(p);
}
S_Tree ask(int p,int l,int r)
{
   if(l<=t[p].sum.l&&r>=t[p].sum.r) return t[p]; int m=(t[p].sum.l+t[p].sum.r)>>1,sum_=0,l_,r_; l_=max(m+1,l); r_=min(m,r); S_Tree lc=W,rc=W,root=W; if(l<=m) lc=ask(p<<1,l,r),sum_+=lc.sum.sum,l_=lc.sum.l; if(r>m) rc=ask(p<<1|1,l,r),sum_+=rc.sum.sum,r_=rc.sum.r; root.sum=(Sum){sum_,l_,r_}; if(l<=m) root.lsum=S_max(lc.lsum,(Sum){lc.sum.sum+rc.lsum.sum,lc.sum.l,rc.lsum.r}); if(r>m) root.rsum=S_max(rc.rsum,(Sum){rc.sum.sum+lc.rsum.sum,lc.rsum.l,rc.sum.r}); root.maxn=S_max(S_max((Sum){lc.rsum.sum+rc.lsum.sum,lc.rsum.l,rc.lsum.r},S_max(lc.maxn,rc.maxn)),S_max(root.lsum,root.rsum)); } int main() { int m; scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]),sum[i]=sum[i-1]+a[i];//输入 build(1,1,n); for(int i=1;i<=m;i++) { int x,y; S_Tree ans; scanf("%d%d",&x,&y); ans=ask(1,x,y); printf("%d %d %d\n",ans.maxn.l,ans.maxn.r,ans.maxn.sum);//输出 } return 0; }
复制代码

  另外,在提交前,可以用简单的低效率高准确度的算法算出正确值用来检查。

  下面是测试代码。

复制代码
Sum ask2(int l,int r)//经典的最大子串和算法
{
    Sum ans=(Sum){MINN,MAXN,MAXN};
    int f[MAXN]={0},l_=l,r_=l;
    f[l-1]=MINN;
    for(int i=l;i<=r;i++)
    {
        f[i]=MINN;
        f[i]=max(f[i-1]+a[i],a[i]);
        if(f[i-1]>0)
        {
            f[i]=f[i-1]+a[i];
            r_=i;
        }
        else
        {
            f[i]=a[i];
            l_=r_=i;
        }
        ans=S_max(ans,(Sum){f[i],l_,r_});
    }
    return ans;
}    
for(int i=1;i<=n;i++)
{
    for(int j=i;j<=n;j++)//循环测试所有数据
    {
        S_Tree tmp=ask(1,i,j);
        Sum ans1=tmp.maxn;
        Sum ans2=ask2(i,j);
        printf("%2d %2d : ans1 = %2d,%2d %5d  ans2 = %2d,%2d %5d  ",i,j,ans1.l,ans1.r,ans1.sum,ans2.l,ans2.r,ans2.sum);//输出当前测试数据和结果
        if(ans1.l==ans2.l&&ans1.r==ans2.r&&ans1.sum==ans2.sum) printf("AC\n");//判断是否相等,相等则输出AC不相等则输出WA
        else printf("WA\n");
    }
}
复制代码

  此题应为一个初始化问题卡了一周,所以写下此篇题解来提醒自己以及记录此题。

  完。

  致谢。

posted @   zxr123_is_dd  阅读(132)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示