GSS 数据结构系列题目略解
前言
SPOJ GSS 系列题目以最大子段和为主,同时还有一些数据结构上的经典问题和解决方案,都比较模板。新手入门可能会有所难度,对于有一定数据结构基础的选手而言不失为一个很好的练手系列。
最大子段和系列
GSS 1
给定长为 \(n\) 的序列 \(a\),\(m\) 次询问,每次询问 \([l_i,r_i]\) 的最大子段和(不得为空)。
线段树。线段树上的每个节点记录当前线段的前缀最大值 \(lef\),后缀最大值 \(rit\) ,区间和 \(sum\),当前线段为区间的答案 \(ans\)。假设当前求得子区间 \(x,y\) 的信息(\(x,y\) 相连且 \(x\) 在 \(y\) 左)。可得到转移
方便实现,可以在线段树 query
的时候直接传完整的 \(lef,rit,ans\) 结构体,最后输出 \(ans\) 即可。
struct node{ll lef,rit,sum,ans;}t[N<<2];
inline node merge(node L,node R){
node tmp;
tmp.ans=max(max(L.ans,R.ans),L.rit+R.lef);
tmp.lef=max(L.lef,L.sum+R.lef);
tmp.rit=max(R.rit,R.sum+L.rit);
tmp.sum=L.sum+R.sum;
return tmp;
}
void build(int p,int l,int r){
if(l==r){
t[p].lef=t[p].rit=t[p].sum=t[p].ans=a[l];
return;
}
build(ls,l,mid);build(rs,mid+1,r);
t[p]=merge(t[ls],t[rs]);
}
node query(int p,int l,int r,int L,int R){
if(L<=l&&r<=R)return t[p];
if(R<=mid)return query(ls,l,mid,L,R);
else if(L>mid)return query(rs,mid+1,r,L,R);
return merge(query(ls,l,mid,L,mid),query(rs,mid+1,r,mid+1,R));
}
GSS 3
给定长为 \(n\) 的序列 \(a\),\(m\) 次操作。
操作
0 x y
把 \(a_x\) 修改成 \(y\)操作
1 l r
询问区间 \([l,r]\) 的最大子段和。
因为是单点修改,所以直接暴力递归下去就好了,复杂度是线段树深度,即单 \(\log\)。
void update(int p,int l,int v){
if(t[p].right==t[p].left){
t[p].sum=t[p].ans=t[p].lef=t[p].rit=v;
return;
}
if(l<=t[ls].right)update(ls,l,v);
else update(rs,l,v);
t[p]=merge(t[ls],t[rs]);
}
GSS 5
给定一个序列。查询左端点在 \([l_1, r_1]\) 之间,且右端点在 \([l_2, r_2]\) 之间的最大子段和,数据保证 \(l_1\leq l_2,r_1\leq r_2\),但是不保证端点所在的区间不重合。
分类讨论。记 \(lef_{l,r}\) 为 \([l,r]\) 区间的前缀最大值,\(rit,ans,sum\) 同理。
- \(r_1\leq l_2\) 即两区间不重合。此时答案为 \(rit_{l_1,r_1}+sum_{r_1,l_2}+lef_{l_2,r_2}-a_{r_1}-a_{l_2}\)
- \(l_2<r_1\) 即两区间重合。
- \(l\in[l_1,l_2),r\in[l_2,r_1]\),\(ans=rit_{l_1,l_2}+lef_{l_2,r_1}-a_{l_2}\)
- \(l\in[l_1,l_2),r\in(r_1,l_2)\),\(ans=rit_{l_1,r_1}+sum_{r_1,l_2}+lef_{l_2,r_2}-a_{r_1}-a_{l_2}\)
- \(l,r\in[l_2,r_1]\),\(ans=ans_{l_2,r_1}\)
- \(l\in[l_2,r_1],r\in(r_1,l_2]\),\(ans=rit_{l_2,r_1}+lef_{r_1,l_2}-a_{r_1}\)
int l1=read(),r1=read(),l2=read(),r2=read();
if(r1<=l2)printf("%lld\n",f(l1,r1).rit+f(r1,l2).sum+f(l2,r2).lef-a[r1]-a[l2]);
else{
ll tmp=max(f(l2,r1).ans,f(l1,l2).rit+f(l2,r1).sum+f(r1,r2).lef-a[l2]-a[r1]);
tmp=max(tmp,max(f(l1,l2).rit+f(l2,r1).lef-a[l2],f(l2,r1).rit+f(r1,r2).lef-a[r1]));
printf("%lld\n",tmp);
}
GSS 7
给定一棵树,有 \(N(N \le 100000)\) 个节点,每一个节点都有一个权值 \(x_i (|x_i| \le 10000)\)
你需要执行 \(Q (Q \le 100000)\) 次操作:
1 a b
查询(a,b)
这条链上的最大子段和,可以为空(即输出\(0\))2 a b c
将(a,b)
这条链上的所有点权变为c
\((|c| <= 10000)\)
一眼树剖,但是子段和可以为空了,并且还变成了区间覆盖,所以不能直接搬。
为空的时候就是 \(lef,rit,ans\) 为负数,于是将他们对 \(0\) 取 \(\max\) 即可,至于区间覆盖,其实也还是线段树的基本操作,覆盖区间的时候不仅要修改当前信息,还要打上当前区间已经覆盖的标记和被覆盖的值,并以后更新到子区间。
同时树剖统计答案的过程中 \(x,y\) 要统计分别跳到根的答案,具体见代码。
//省略树剖板子及变量
struct node{
int lef,rit,sum,ans,tag;bool cov;
node(){lef=rit=sum=ans=0;}
}t[N<<2];
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)>>1)
inline void modify(int p,int l,int r,int v){
t[p].sum=v*(r-l+1);
t[p].lef=t[p].rit=t[p].ans=max(t[p].sum,0);
t[p].tag=v,t[p].cov=1;
}
inline node merge(node L,node R){
//和上面一样,省略
}
inline void pushdown(int p,int l,int r){
if(!t[p].cov)return;
modify(ls,l,mid,t[p].tag);
modify(rs,mid+1,r,t[p].tag);
t[p].tag=t[p].cov=0;
}
void build(int p,int l,int r){
if(l==r){
t[p].sum=a[idx[l]];
t[p].lef=t[p].rit=t[p].ans=max(t[p].sum,0);
return;
}
build(ls,l,mid);build(rs,mid+1,r);
t[p]=merge(t[ls],t[rs]);
}
void update(int p,int l,int r,int L,int R,int v){
if(L<=l&&r<=R){
modify(p,l,r,v);
return;
}pushdown(p,l,r);
//...
}
node query(int p,int l,int r,int L,int R){
//...
}
inline void upline(int x,int y,int v){
//...
}
inline node qrline(int x,int y){
node lef,rit;
while(tps[x]!=tps[y]){
if(dfn[tps[x]]<dfn[tps[y]]){
rit=merge(query(1,1,n,dfn[tps[y]],dfn[y]),rit);
y=fat[tps[y]];
}else{
lef=merge(query(1,1,n,dfn[tps[x]],dfn[x]),lef);
x=fat[tps[x]];
}
}
if(dfn[x]>dfn[y])
lef=merge(query(1,1,n,dfn[y],dfn[x]),lef);
else rit=merge(query(1,1,n,dfn[x],dfn[y]),rit);
swap(lef.lef,lef.rit);
return merge(lef,rit);
}//不要想当然地以为应该写成 merge(lef,query())
//这里的lef/rit 实质上是从他们 lca 到各自节点的答案,所以 lef 最后要交换
//省略主函数
GSS 6
给出一个由 \(N\) 个整数组成的序列 \(A\),你需要应用 \(M\) 个操作:
I p x
在\(~p~\)处插入插入一个元素 \(x\)D p
删除\(~p~\)处的一个元素R p x
修改\(~p~\)处元素的值为\(~x~\)Q l r
查询一个区间 \(\left[l,r\right]\) 的最大子段和
使用平衡树。维护的时候仍然记录 \(lef,rit,sum,ans\) 四个信息,pushup
操作类似,除了左右儿子的信息之外,记得加上这个节点本身。
这里采用简洁的 FHQ treap
。
struct treap{
int lc,rc,siz,key;
ll val,sum,lef,rit,ans;
}t[N];
#define ls t[p].lc
#define rs t[p].rc
int tot,rt;
inline int nwnode(ll val){
t[++tot].siz=1,t[tot].key=rand();
t[tot].sum=t[tot].val=t[tot].ans=val;
t[tot].lef=t[tot].rit=max(0ll,val);
return tot;
}
inline void pushup(int p){
t[p].siz=t[ls].siz+t[rs].siz+1;
t[p].sum=t[ls].sum+t[rs].sum+t[p].val;
t[p].lef=max(t[ls].lef,t[ls].sum+t[p].val+t[rs].lef);
t[p].rit=max(t[rs].rit,t[rs].sum+t[p].val+t[ls].rit);
t[p].ans=max(max(t[ls].ans,t[rs].ans),t[ls].rit+t[p].val+t[rs].lef);
}
int merge(int x,int y){
if(!x||!y)return x+y;
if(t[x].key<t[y].key){
t[x].rc=merge(t[x].rc,y);
pushup(x);return x;
}else{
t[y].lc=merge(x,t[y].lc);
pushup(y);return y;
}return 114514;
}
void split(int p,int k,int &x,int &y){
if(!p){x=y=0;return;}
if(t[ls].siz<k){
x=p;
split(t[p].rc,k-t[ls].siz-1,t[p].rc,y);
}else{
y=p;
split(t[p].lc,k,x,t[p].lc);
}pushup(p);
}
int main(){
srand(114*514+1919-810);n=read();
t[rt].ans=-INF;
for(int i=1;i<=n;++i){
ll x=read();
rt=merge(rt,nwnode(x));
}
m=read();
char opt[10];
while(m--){
scanf("%s",opt);int a=read(),x=0,y=0,z=0;
if(opt[0]=='I'){
ll b=read();
split(rt,a-1,x,y);
rt=merge(x,merge(nwnode(b),y));
}else if(opt[0]=='D'){
split(rt,a-1,x,y);
split(y,1,y,z);
rt=merge(x,z);
}else if(opt[0]=='R'){
ll b=read();
split(rt,a-1,x,y);
split(y,1,y,z);
t[y].val=t[y].sum=t[y].ans=b;
t[y].lef=t[y].rit=max(0ll,b);
rt=merge(x,merge(y,z));
}else{
int b=read();
split(rt,b,y,z);
split(y,a-1,x,y);
printf("%lld\n",t[y].ans);
rt=merge(x,merge(y,z));
}
}
return 0;
}
GSS 2
给出 \(n\) 个数,\(q\) 次询问,求最大子段和,相同的数只算一次。
加了相同的数只算一次这个限制之后,难度有了显著提升。
最大子段和的核心就是一串数的和,对于一整个序列求最大字段和,我们常见的方法也是逐一加入一个数,并判断与前面的数组合能否使答案更优秀。
所以我们可以考虑将询问按右端点排序,使得右侧的数不断加入其中,同时对此时已经加入的每一个数维护一个后缀和。
也就是说,线段树上的每个节点维护区间里的每个数的后缀和的最大值。
但是答案区间的右端点并不一定是当前加入的数,所以我们还需要维护每个左侧端点的历史最大后缀和。那么查询的时候询问 \([1,r]\) 的历史最大值即可。
至于如何维护这两个信息,后缀和就是基础的操作;维护历史最大和,应当打上一个历史最大和的标记,表示在这个位置进行修改的最大值,然后用这个标记和区间最大值相加来更新历史区间最大值。这个标记只能由维护区间加的标记取 \(\max\) 得到,具体见代码。
现在考虑如何让相同的数只算一次。
假设现在加入的数为 \(a_r\),且 \(a_x=a_r(x<r)\),那么 \(a_x,a_r\) 不能同时统计到 \(sum\) 中,则只需要在线段树更新的时候对区间 \([x+1,r]\) 进行加法就行了。
struct querys{int l,r,id;}que[N];
inline bool cmp(querys x,querys y){return x.r<y.r;}
ll a[N],ans[N];int n,m,pre[N<<1],las[N<<1];
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)>>1)
ll maxv[N<<2],tag[N<<2],last[N<<2],lazy[N<<2];
inline void pushup(int p){
maxv[p]=max(maxv[ls],maxv[rs]);
last[p]=max(last[ls],last[rs]);
}
inline void pushdown(int p){
last[ls]=max(last[ls],maxv[ls]+lazy[p]);
last[rs]=max(last[rs],maxv[rs]+lazy[p]);
lazy[ls]=max(lazy[ls],tag[ls]+lazy[p]);
lazy[rs]=max(lazy[rs],tag[rs]+lazy[p]);
maxv[ls]+=tag[p],tag[ls]+=tag[p];
maxv[rs]+=tag[p],tag[rs]+=tag[p];
tag[p]=lazy[p]=0;
}
inline void update(int p,int l,int r,int L,int R,ll v){
if(L<=l&&r<=R){
maxv[p]+=v,tag[p]+=v;
last[p]=max(last[p],maxv[p]);
lazy[p]=max(lazy[p],tag[p]);
return;
}pushdown(p);
if(L<=mid)update(ls,l,mid,L,R,v);
if(R>mid)update(rs,mid+1,r,L,R,v);
pushup(p);
}
inline ll query(int p,int l,int r,int L,int R){
if(L<=l&&r<=R)return last[p];
pushdown(p);ll tmp=-INF;
if(L<=mid)tmp=max(tmp,query(ls,l,mid,L,R));
if(R>mid)tmp=max(tmp,query(rs,mid+1,r,L,R));
return tmp;
}
int main(){
n=read();
for(int i=1;i<=n;++i){
a[i]=read();
pre[i]=las[a[i]+BAS];
las[a[i]+BAS]=i;
}
m=read();
for(int i=1;i<=m;++i)
que[i].l=read(),que[i].r=read(),que[i].id=i;
sort(que+1,que+1+m,cmp);
for(int i=1,p=1;i<=n;++i){
update(1,1,n,pre[i]+1,i,a[i]);
while(p<=m&&que[p].r<=i){
ans[que[p].id]=query(1,1,n,que[p].l,que[p].r);
++p;
}
}
for(int i=1;i<=m;++i)
printf("%lld\n",ans[i]);
putchar('\n');
return 0;
}
其它
GSS 4
\(n\) 个数,和在\(10^{18}\) 范围内。
也就是 \(\sum~a_i~\leq~10^{18}\)
现在有「两种」操作
0 x y
把区间\([x,y]\) 内的每个数开方,下取整
1 x y
询问区间\([x,y]\) 的每个数的和
\(10^{18}\) 开六次根号 \(\approx 1.9\),所以对于每个数来说,对他的值有改变的修改次数不超过小常数次。
这样因为这对于任意一个区间,如果他里面的所有书都要开方,那么最多开 \(6\) 次里面的所有数都会变成 \(1\)。
那么我们记录一下每一个区间里的数时候都变成 \(1\),如果变成 \(1\) 了,那么跳过即可,否则暴力递归修改。时间复杂度 \(O(nk\log n)\)
实现的话比较基础。统计区间最大值和区间和即可。
GSS 8
给你一个序列,\(A[0], A[1]...A[N - 1]. (0 \le A[i] \lt 2^{32})\)
你需要支持 \(Q\) 次操作
I pos val
插入一个数字在第 \(pos\) 个位置之前,\(0 \le val \lt 2^{32}\)。D pos
删除第 \(pos\) 个元素R pos val
将第 \(pos\) 个元素变为 \(val(0 \le val \lt 2^{32})\)Q l r k
询问 \((\sum\limits_{i=l}^{r} A[i] * (i - l + 1) ^ k) \mod 2^{32}\),保证 \(0 \le k \le 10\)
平衡树,但是统计信息并不简单。
统计区间信息的关键在于对左右子区间信息的快速合并,所以我们需要找到一种方式使得左右子区间的信息能得到当前区间答案。注意到 \(k\) 很小,所以尝试统计 \(val_{u,k}\) 表示节点 \(u\) 及其子节点所构成的序列在询问为 \(k\) 的情况下的答案。
二项式定理的形式。
将原式中拆分出 \(mid\)
那么左右区间的信息即可合并,注意加上该节点本身的值。
组合数预处理一下就好了。
inline void pushup(int p){
siz[p]=siz[lc[p]]+siz[rc[p]]+1;
tmp[0]=1;
for(int i=1;i<=K;++i)tmp[i]=tmp[i-1]*(siz[lc[p]]+1);
for(int i=0;i<=K;++i){
val[p][i]=val[lc[p]][i]+arr[p]*tmp[i];
for(int j=0;j<=i;++j)
val[p][i]+=c[i][j]*tmp[i-j]*val[rc[p]][j];
}
}
后记
SPOJ 评测为啥这么慢啊。另外不能查看错误信息差评。GSS7 和 GSS8 都调了 114514 个小时。
本文作者:BigSmall_En
本文链接:https://www.cnblogs.com/BigSmall-En/p/16567427.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步