山海经题解(线段树)
题目:
天立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"); }
}
此题应为一个初始化问题卡了一周,所以写下此篇题解来提醒自己以及记录此题。
完。
致谢。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具