题解:P14638 [NOIP2025] 序列询问 / query(民间数据)

· · 题解

前言

参考资料:题解,作者 Doqe 大佬。

做法:具体地按 L 分块解决 R<2\times L 的情况。

当然,也可以认为我是来提供代码实现的。

分析

特殊情况

设当前询问范围为 [L,R],采用按 L 分块解决 R<2\times L 的情况。

首先构建两个 ST 表,分别支持前缀和数组区间最小值、最大值。

::::info[关于单调队列]

部分情况下可以改为单调队列,但是 ST 表使用起来会更加方便。

::::

假设当前的分块为 [l,r]=[l,l+L),那么,所有与此块有交的区间可以分为三种(有重合,但不影响答案):

右端点在块内时,枚举块内的点作为右端点,利用 ST 表得出各点的答案(没有可行区间就设置为负无穷)。

然后可以发现:由于块长为 Lx 作为右端点时,所得的答案对 [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})

::::info[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]

$[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$。 ::::info[注] $r-l+1$ 未必是 $L$,因为在最后一块处情况特殊。(代码实现时也要注意!) :::: 但是在此处却不影响整体答案,问题得以解决。 时间复杂度 $O(nq)$,空间 $O(n\log{n})$。 ::::info[code] ```cpp #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 的高分,膜。