题解:P14638 [NOIP2025] 序列询问
前言
本文同步自洛谷专栏,题目传送门。
做法:具体地按 \(L\) 分块解决 \(R<2\times L\) 的情况。
当然,也可以认为我是来提供代码实现的。
分析
特殊情况
设当前询问范围为 \([L,R]\),采用按 \(L\) 分块解决 \(R<2\times L\) 的情况。
首先构建两个 ST 表,分别支持前缀和数组区间最小值、最大值。
假设当前的分块为 \([l,r]=[l,l+L)\),那么,所有与此块有交的区间可以分为三种(有重合,但不影响答案):
-
左端点在块内。
-
右端点在块内。
-
包含整块。
右端点在块内时,枚举块内的点作为右端点,利用 ST 表得出各点的答案(没有可行区间就设置为负无穷)。
然后可以发现:由于块长为 \(L\),\(x\) 作为右端点时,所得的答案对 \([l,x]\) 都成立,可以进行一个后缀 \(\max\) 完成统计,复杂度 \(O(L)\)。
左端点在块内时同理,注意前缀和数组的一些下标问题,统计用前缀 \(\max\)。
包含整块的情况,暴力向右枚举右端点,并规定左端点 \(\le l\),则长度一定 \(\ge L\),一直枚举到不存在 \(\le R\) 的情况为止,复杂度 \(O(R-L)=O(L)\)。
全局共 \(\frac{n}{L}\) 块,总复杂度就是 \(O(n)\)。
倍增预处理解决一般情况
按照值域倍增,预处理出 \([2^i,2^{i+1})\) 的答案,然后合并为区间,时间复杂度和空间复杂度都是 \(O(n\log^2{n})\)。
注:也可以再建一个 ST 表,但是复杂度上没有必要。
然后每次询问时,将询问拆成三部分:\([L,2^x),[2^x,2^y),[2^y,R]\)。
其中 \(2^{x-1}\le L<2^x,2^y\le R<2^{y+1}\)。
可以解决此问题,时间复杂度 \(O(nq)\),空间 \(O(n\log^2{n})\)。
$\red{\text{code}}$
#include<bits/stdc++.h>
using namespace std;
#define gc() getchar()
#define ll long long
#define N 50005
#define inf -1000000000000000ll
#define ull unsigned long long
int n,q,u;
ll ans[N],w[N],mx[20][20][N];
ll maxn[20][N],minn[20][N],res[N];
ull tot;
inline int read(){
int a=0,c=gc(),f=0;
for(;!isdigit(c);c=gc()) f|=c=='-';
for(;isdigit(c);c=gc()) a=10*a+c-'0';
return f?-a:a;
}
inline void cmax(ll &x,ll y){(x<y)&&(x=y);}
inline void cmin(ll &x,ll y){(x>y)&&(x=y);}
inline int LG(int x){return 31^__builtin_clz(x);}
inline ll get_mi(int l,int r){
int d=LG(r-l+1);
return min(minn[d][l],minn[d][r-(1<<d)+1]);
}
inline ll get_mx(int l,int r){
int d=LG(r-l+1);
return max(maxn[d][l],maxn[d][r-(1<<d)+1]);
}
void calc(int L,int R){//每次处理一个长为 l 的区间,答案为前后缀 max
for(int l=1,r=L;l<=r;l+=L,r=min(r+L,n)){
for(int i=l;i<=r;i++){
w[i]=i-L>=0?minn[0][i]-get_mi(max(0,i-R),i-L):inf;
}
for(int i=r-1;i>=l;i--) cmax(w[i],w[i+1]);
for(int i=l;i<=r;i++) ans[i]=w[i];//赋值,可以不用写清空
//分割线,上面在枚举右端点。下面的部分中,注意枚举左端点时,前缀和数组下标 -1
for(int i=r;i>=l;i--){
w[i]=i+L-1<=n?get_mx(i+L-1,min(n,i+R-1))-maxn[0][i-1]:inf;
}
for(int i=l+1;i<=r;i++) cmax(w[i],w[i-1]);
for(int i=l;i<=r;i++) cmax(ans[i],w[i]);
//统计包含这个块的答案
ll ret=inf;
for(int i=r+1;i<=n;i++){
if(i-R>l-1) break;
cmax(ret,minn[0][i]-get_mi(max(i-R,0),l-1));
}
for(int i=l;i<=r;i++) cmax(ans[i],ret);
}
}
int main(){
// freopen("query.in","r",stdin);
// freopen("query.out","w",stdout);
n=read(),u=LG(n);
for(int i=1;i<=n;i++){
maxn[0][i]=minn[0][i]=read()+maxn[0][i-1];
}
for(int i=1;i<=LG(n+1);i++){
for(int j=0,up=n-(1<<i)+1;j<=up;j++){
maxn[i][j]=maxn[i-1][j],cmax(maxn[i][j],maxn[i-1][j+(1<<i-1)]);
}
}
for(int i=1;i<=LG(n+1);i++){
for(int j=0,up=n-(1<<i)+1;j<=up;j++){
minn[i][j]=minn[i-1][j],cmin(minn[i][j],minn[i-1][j+(1<<i-1)]);
}
}
for(int i=0;i<=u;i++){
for(int j=0;j<=u;j++){
for(int k=1;k<=n;k++) mx[i][j][k]=inf;
}
}
for(int i=0;i<u;i++){
calc(1<<i,(1<<i+1)-1);
for(int j=1;j<=n;j++) mx[i][i][j]=ans[j];
}
for(int l=0;l<u;l++){//直接处理每个区间
for(int r=l+1;r<u;r++){
for(int i=1;i<=n;i++){
mx[l][r][i]=mx[l][r-1][i];
cmax(mx[l][r][i],mx[r][r][i]);
}
}
}
q=read();
for(int id=1,l,r;id<=q;id++){
l=read(),r=read(),tot=0;
if(r<2*l){
calc(l,r);
for(int i=1;i<=n;i++) tot^=(ull)(i*ans[i]);
printf("%llu\n",tot);continue;
}
int x=LG(l)+1,y=LG(r);
for(int i=1;i<=n;i++) res[i]=mx[x][y-1][i];
calc(l,(1<<x)-1);
for(int i=1;i<=n;i++) cmax(res[i],ans[i]);
calc(1<<y,r);
for(int i=1;i<=n;i++) cmax(res[i],ans[i]);
for(int i=1;i<=n;i++) tot^=(ull)(i*res[i]);
printf("%llu\n",tot);
}
return 0;
}
无需预处理的方法
为参考资料中的 bonus。
可以将询问简单拆为两部分:\([L,\frac{R}{2}],[\frac{R+1}{2},R]\)。
\([\frac{R+1}{2},R]\) 为特殊情况,容易解决。
\([L,\frac{R}{2}]\) 若应用特殊情况的算法,求解包含整块的情况时复杂度错误。
考虑特殊处理,可以利用这样一个特点:
对于一个块 \([l,r]\),若用 \([l+L-1,l+R-1]\) 中的最大值减去 \([r-R,r-L]\) 中的最小值,所得结果一定包括长度为 \([L,R]\) 的情况,
但是会错误地求解长度为 \(l+R-1-(r-R)=2\times R-(r-l+1)\le 2\times R-1\)。
$\red{\text{注}}$
\(r-l+1\) 未必是 \(L\),因为在最后一块处情况特殊。(代码实现时也要注意!)
但是在此处却不影响整体答案,问题得以解决。
时间复杂度 \(O(nq)\),空间 \(O(n\log{n})\)。
$\red{\text{code}}$
#include<bits/stdc++.h>
using namespace std;
#define gc() getchar()
#define ll long long
#define N 50005
#define inf -1000000000000000ll
#define ull unsigned long long
int n,q;
ll ans[N],w[N],maxn[20][N],minn[20][N];
ull tot;
inline int read(){
int a=0,c=gc(),f=0;
for(;!isdigit(c);c=gc()) f|=c=='-';
for(;isdigit(c);c=gc()) a=10*a+c-'0';
return f?-a:a;
}
inline void cmax(ll &x,ll y){(x<y)&&(x=y);}
inline void cmin(ll &x,ll y){(x>y)&&(x=y);}
inline int LG(int x){return 31^__builtin_clz(x);}
inline ll pmax(ll x,ll y){return x<y?y:x;}
inline ll pmin(ll x,ll y){return x>y?y:x;}
inline ll get_mi(int l,int r){
int d=LG(r-l+1);
return pmin(minn[d][l],minn[d][r-(1<<d)+1]);
}
inline ll get_mx(int l,int r){
int d=LG(r-l+1);
return pmax(maxn[d][l],maxn[d][r-(1<<d)+1]);
}
void calc(int L,int R,int o){//每次处理一个长为 l 的区间,答案为前后缀 max,R<=2L
for(int l=1,r=L;l<=r;l+=L,r=min(r+L,n)){
for(int i=l;i<=r;i++){
w[i]=i-L>=0?minn[0][i]-get_mi(max(0,i-R),i-L):inf;
}
for(int i=r-1;i>=l;i--) cmax(w[i],w[i+1]);
for(int i=l;i<=r;i++) cmax(ans[i],w[i]);
//分割线,上面在枚举右端点。下面的部分中,注意枚举左端点时,前缀和数组下标 -1
for(int i=r;i>=l;i--){
w[i]=i+L-1<=n?get_mx(i+L-1,min(n,i+R-1))-maxn[0][i-1]:inf;
}
for(int i=l+1;i<=r;i++) cmax(w[i],w[i-1]);
for(int i=l;i<=r;i++) cmax(ans[i],w[i]);
}
if(o==0) return;
for(int l=1,r=L;l<=r;l+=L,r=min(r+L,n)){//统计包含这个块的答案
ll ret=inf;
for(int i=r+1;i<=n;i++){//<=n 的限制要加入
if(i-R>l-1) break;
cmax(ret,minn[0][i]-get_mi(max(i-R,0),l-1));
}
for(int i=l;i<=r;i++) cmax(ans[i],ret);
}
}
void solve(int L,int R){//此处 R*2 <= 原来 R
for(int l=1,r=L;l<=r;l+=L,r=min(r+L,n)){//特别注意最后一块可能长度不足,r-L 不能写 l-1
ll ret=get_mx(r,min(n,l+R-1))-get_mi(max(0,r-R),r-L);
for(int i=l;i<=r;i++) cmax(ans[i],ret);
}
}
void init(){
for(int i=1;i<=n;i++){
maxn[0][i]=minn[0][i]=read()+maxn[0][i-1];
}
for(int i=1;i<=LG(n+1);i++){
for(int j=0,up=n-(1<<i)+1;j<=up;j++){
maxn[i][j]=pmax(maxn[i-1][j],maxn[i-1][j+(1<<i-1)]);
}
}
for(int i=1;i<=LG(n+1);i++){
for(int j=0,up=n-(1<<i)+1;j<=up;j++){
minn[i][j]=pmin(minn[i-1][j],minn[i-1][j+(1<<i-1)]);
}
}
}
int main(){
// freopen("query.in","r",stdin);
// freopen("query.out","w",stdout);
n=read(),init(),q=read();
for(int id=1,l,r;id<=q;id++){
l=read(),r=read(),tot=0;
for(int i=1;i<=n;i++) ans[i]=inf;
calc(max(l,(r+1)>>1),r,1);
if(l<((r+1)>>1)) calc(l,r>>1,0),solve(l,r>>1);
for(int i=1;i<=n;i++) tot^=(ull)(i*ans[i]);
printf("%llu\n",tot);
}
return 0;
}
后话
说一下蒟蒻赛时的一个 \(O(nq\log{nq})\) 的暴力(然而写挂了)。
把所有询问离线,\(L,R\) 放在一起后再塞入形如 \(2^i\) 的数,排序,每一段分别求解。
此时发现正着扫一遍(枚举左端点)和反着扫一遍(枚举右端点)可以更新到所有点,使用 zkw 线段树维护。
大抵是接近 \(R\le 2\times L\) 的特殊情况,但太弱了没有发现可以归结出来,也想不到按 \(L\) 分块,只能 \(O(n\log{n})\) 做。
赛时花了两个小时写这个做法,还写挂了……一分暴力都没有……
赛时算的空间 \(nq\) 恰好够,赛后想起来有个二倍常数……
警示后人,先写最基础的暴力(本来想写个高分暴力来着)。
听闻机房同学 T4 拼出了 45pts 的高分,膜。

浙公网安备 33010602011771号