P1076 · OFFICIAL SOLUTION

P1076 潮声四界定音 官方题解

Gioush OJ · P1076 潮声四界定音

【部分分:测试点 1∼31\sim 31∼3】

  • 原题为 P2839 [国家集训队] middle
  • 对一次询问,枚举 l[a,b]l\in[a,b]r[c,d]r\in[c,d]。复制子段 xl,,xrx_l,\ldots,x_r 并排序,取上中位数。
  • 一共有 O(n2)\mathcal O(n^2) 组端点,每次排序需要 O(nlogn)\mathcal O(n\log n),单次询问时间复杂度为 O(n3logn)\mathcal O(n^3\log n)
  • 总时间复杂度为 O(qn3logn)\mathcal O(qn^3\log n)。这一步直接对应定义,也提示我们应先把“求最大中位数”改成判定问题。

【部分分:测试点 4∼74\sim 74∼7】

  • 固定一个候选答案 yy,把原序列转化为
bi={1,xiy,1,xi<y.b_i= \begin{cases} 1, & x_i\geq y,\\ -1, & x_i<y. \end{cases}
  • 对任意非空子段,其中位数不小于 yy,当且仅当子段内不小于 yy 的数不少于小于 yy 的数。

  • 这正好等价于转化后子段和不小于 00

  • 每个合法子段都由三部分组成:

[l,b],[b+1,c1],[c,r].[l,b],\qquad [b+1,c-1],\qquad [c,r].
  • 中间部分固定。左侧应选择 [a,b][a,b] 的最大后缀,右侧应选择 [c,d][c,d] 的最大前缀。
  • 因此候选答案 yy 可行,当且仅当
Suff(a,b)+Sum(b+1,c1)+Pre(c,d)0.\operatorname{Suff}(a,b) +\operatorname{Sum}(b+1,c-1) +\operatorname{Pre}(c,d)\geq0.
  • 直接扫描三个区间即可完成一次判定,再二分 yy。时间复杂度为 O(qnlogn)\mathcal O(qn\log n)

【部分分:测试点 8∼118\sim 118∼11】

  • 特殊性质给出 a=ba=bc=dc=d,左右端点都已经固定,询问变成求固定子段 [b,c][b,c] 的上中位数。
  • 对原序列建立值域可持久化权值线段树。第 ii 个前缀版本记录 x1,,xix_1,\ldots,x_i 中各离散值的出现次数。
  • 用版本 cc 减去版本 b1b-1,即可得到子段 [b,c][b,c] 的值域计数,再在线段树上寻找第
cb+12+1\left\lfloor\frac{c-b+1}{2}\right\rfloor+1

小的数。

  • 时间复杂度为 O((n+q)logn)\mathcal O((n+q)\log n)

【部分分:测试点 12∼1512\sim 1512∼15】

  • 设序列中不同的值为 v1<v2<<vVv_1<v_2<\cdots<v_V,本档有 V50V\leq50
  • 对每个候选值 vjv_j,建立对应的二值序列,并建立一棵普通线段树,维护区间和、最大非空前缀和与最大非空后缀和。
  • 对询问在 v1,,vVv_1,\ldots,v_V 中二分,使用对应线段树完成三段查询与可行性判断。
  • 预处理时间为 O(nV)\mathcal O(nV),询问时间为 O(logVlogn)\mathcal O(\log V\log n),空间复杂度为 O(nV)\mathcal O(nV)

【正解】

  • 线段树的每个结点保存
(Sum,LMax,RMax),(\operatorname{Sum},\operatorname{LMax},\operatorname{RMax}),

分别表示区间和、最大非空前缀和、最大非空后缀和。

  • 设左右儿子信息为 A,BA,B,合并方程为
Sum=A.Sum+B.Sum,LMax=max(A.LMax,A.Sum+B.LMax),RMax=max(B.RMax,B.Sum+A.RMax).\begin{aligned} \operatorname{Sum}&=A.\operatorname{Sum}+B.\operatorname{Sum},\\ \operatorname{LMax}&=\max(A.\operatorname{LMax}, A.\operatorname{Sum}+B.\operatorname{LMax}),\\ \operatorname{RMax}&=\max(B.\operatorname{RMax}, B.\operatorname{Sum}+A.\operatorname{RMax}). \end{aligned}
  • 若为每个候选值完整建立一棵线段树,空间会达到 O(nV)\mathcal O(nV)。相邻候选值对应的二值序列只在少数位置不同,可以共享其余结点。

  • 先建立阈值 v1v_1 的版本,此时所有位置均为 11

  • 阈值从 vjv_j 提高到 vj+1v_{j+1} 时,恰好把所有满足 xi=vjx_i=v_j 的位置从 11 改为 1-1

  • 每次单点修改只新建根到叶子的一条链,其余结点沿用前一版本,这就是可持久化线段树。

  • 全部阈值变化过程中,每个位置只会从 11 变成 1-1 一次,所以总修改次数为 nn

  • 建树与全部版本的总时间、空间均为 O(nlogn)\mathcal O(n\log n)

  • 对固定版本,分别查询 [a,b][a,b][b+1,c1][b+1,c-1][c,d][c,d],取左区间最大后缀、中间区间和、右区间最大前缀。

  • b+1>c1b+1>c-1,中间区间为空,其区间和按 00 处理。

  • 随着阈值增大,序列中的 11 只会变成 1-1,可行性单调不增。

  • 因此可以在离散值 v1,,vVv_1,\ldots,v_V 中二分最大的可行阈值。每次判定需要三次线段树区间查询,时间为 O(logn)\mathcal O(\log n)

  • 单次询问时间复杂度为 O(logVlogn)\mathcal O(\log V\log n)

  • 总时间复杂度为

O(nlogn+qlogVlogn),\mathcal O(n\log n+q\log V\log n),

最坏可写作 O((n+q)log2n)\mathcal O((n+q)\log^2 n)。空间复杂度为 O(nlogn)\mathcal O(n\log n)

  • 本题的完整链条是:最大中位数 \longrightarrow 二分阈值 \longrightarrow 正负序列 \longrightarrow 最大合法区间和。
  • 四个端点限制进一步把最大区间和拆成
左侧最大后缀+固定中段和+右侧最大前缀.\text{左侧最大后缀} +\text{固定中段和} +\text{右侧最大前缀}.
  • 线段树只负责区间信息合并,可持久化只负责共享相邻阈值下未改变的部分。
  • 上中位数的定义决定判定条件是区间和不小于 00。若改成下中位数,偶数长度时的判定边界也会随之改变。

【参考代码】

/*Author:EhundateghDate:2026/8/26Name:tonic.cppYou steal,I kill.*/#include <cstdio>#include <cstring>#include <algorithm>#define MAXN 100010#define MAXNODE 4000010using namespace std; int T,n,q,cnt,ValueCnt,Line[MAXN],Root[MAXN],Mapping[MAXN];pair<int,int> Ori[MAXN]; struct data{    int Len,Sum,Pre,Suff;}; struct node{    int ls,rs,Sum,Pre,Suff;}Node[MAXNODE]; data Merge(const data &A,const data &B){    if(!A.Len) return B;    if(!B.Len) return A;    return {A.Len+B.Len,A.Sum+B.Sum,max(A.Pre,A.Sum+B.Pre),max(B.Suff,B.Sum+A.Suff)};} void Update(int Now){    Node[Now].Sum=Node[Node[Now].ls].Sum+Node[Node[Now].rs].Sum;    Node[Now].Pre=max(Node[Node[Now].ls].Pre,Node[Node[Now].ls].Sum+Node[Node[Now].rs].Pre);    Node[Now].Suff=max(Node[Node[Now].rs].Suff,Node[Node[Now].rs].Sum+Node[Node[Now].ls].Suff);} int Build(int l,int r){    int Now=++cnt;    if(l==r){Node[Now]={0,0,1,1,1};return Now;}    int Mid=(l+r)>>1;    Node[Now].ls=Build(l,Mid);Node[Now].rs=Build(Mid+1,r);    Update(Now);return Now;} int Modify(int Last,int l,int r,int Pos){    int Now=++cnt;Node[Now]=Node[Last];    if(l==r){Node[Now].Sum=Node[Now].Pre=Node[Now].Suff=-1;return Now;}    int Mid=(l+r)>>1;    if(Pos<=Mid) Node[Now].ls=Modify(Node[Last].ls,l,Mid,Pos);    else Node[Now].rs=Modify(Node[Last].rs,Mid+1,r,Pos);    Update(Now);return Now;} data Query(int Now,int l,int r,int Left,int Right){    if(Left>r||Right<l) return {0,0,0,0};    if(Left<=l&&r<=Right) return {r-l+1,Node[Now].Sum,Node[Now].Pre,Node[Now].Suff};    int Mid=(l+r)>>1;    return Merge(Query(Node[Now].ls,l,Mid,Left,Right),Query(Node[Now].rs,Mid+1,r,Left,Right));} bool Judge(int Version,int a,int b,int c,int d){    data Left=Query(Root[Version],1,n,a,b);    data Right=Query(Root[Version],1,n,c,d);    int Sum=b+1<=c-1?Query(Root[Version],1,n,b+1,c-1).Sum:0;    return Left.Suff+Sum+Right.Pre>=0;} void Solve(){    int a,b,c,d;    scanf("%d%d",&n,&q);    for(int i=1;i<=n;i++){        scanf("%d",&Line[i]);        Ori[i]={Line[i],i};    }    sort(Ori+1,Ori+n+1);    cnt=ValueCnt=0;    int Now=Build(1,n);    for(int i=1;i<=n;){        int j=i;        Mapping[++ValueCnt]=Ori[i].first;        Root[ValueCnt]=Now;        while(j<=n&&Ori[j].first==Ori[i].first){Now=Modify(Now,1,n,Ori[j].second);j++;}        i=j;    }    while(q-->0){        scanf("%d%d%d%d",&a,&b,&c,&d);        int Left=1,Right=ValueCnt,Ans=1;        while(Left<=Right){            int Mid=(Left+Right)>>1;            if(Judge(Mid,a,b,c,d)) Ans=Mid,Left=Mid+1;            else Right=Mid-1;        }        printf("%d\n",Mapping[Ans]);    }} int main(){    int c;    scanf("%d%d",&c,&T);    while(T-->0) Solve();    return 0;}