P1076 潮声四界定音 官方题解
Gioush OJ · P1076 潮声四界定音
【部分分:测试点 1∼31\sim 31∼3】
- 原题为 P2839 [国家集训队] middle。
- 对一次询问,枚举 与 。复制子段 并排序,取上中位数。
- 一共有 组端点,每次排序需要 ,单次询问时间复杂度为 。
- 总时间复杂度为 。这一步直接对应定义,也提示我们应先把“求最大中位数”改成判定问题。
【部分分:测试点 4∼74\sim 74∼7】
- 固定一个候选答案 ,把原序列转化为
-
对任意非空子段,其中位数不小于 ,当且仅当子段内不小于 的数不少于小于 的数。
-
这正好等价于转化后子段和不小于 。
-
每个合法子段都由三部分组成:
- 中间部分固定。左侧应选择 的最大后缀,右侧应选择 的最大前缀。
- 因此候选答案 可行,当且仅当
- 直接扫描三个区间即可完成一次判定,再二分 。时间复杂度为 。
【部分分:测试点 8∼118\sim 118∼11】
- 特殊性质给出 且 ,左右端点都已经固定,询问变成求固定子段 的上中位数。
- 对原序列建立值域可持久化权值线段树。第 个前缀版本记录 中各离散值的出现次数。
- 用版本 减去版本 ,即可得到子段 的值域计数,再在线段树上寻找第
小的数。
- 时间复杂度为 。
【部分分:测试点 12∼1512\sim 1512∼15】
- 设序列中不同的值为 ,本档有 。
- 对每个候选值 ,建立对应的二值序列,并建立一棵普通线段树,维护区间和、最大非空前缀和与最大非空后缀和。
- 对询问在 中二分,使用对应线段树完成三段查询与可行性判断。
- 预处理时间为 ,询问时间为 ,空间复杂度为 。
【正解】
- 线段树的每个结点保存
分别表示区间和、最大非空前缀和、最大非空后缀和。
- 设左右儿子信息为 ,合并方程为
-
若为每个候选值完整建立一棵线段树,空间会达到 。相邻候选值对应的二值序列只在少数位置不同,可以共享其余结点。
-
先建立阈值 的版本,此时所有位置均为 。
-
阈值从 提高到 时,恰好把所有满足 的位置从 改为 。
-
每次单点修改只新建根到叶子的一条链,其余结点沿用前一版本,这就是可持久化线段树。
-
全部阈值变化过程中,每个位置只会从 变成 一次,所以总修改次数为 。
-
建树与全部版本的总时间、空间均为 。
-
对固定版本,分别查询 、、,取左区间最大后缀、中间区间和、右区间最大前缀。
-
若 ,中间区间为空,其区间和按 处理。
-
随着阈值增大,序列中的 只会变成 ,可行性单调不增。
-
因此可以在离散值 中二分最大的可行阈值。每次判定需要三次线段树区间查询,时间为 。
-
单次询问时间复杂度为 。
-
总时间复杂度为
最坏可写作 。空间复杂度为 。
- 本题的完整链条是:最大中位数 二分阈值 正负序列 最大合法区间和。
- 四个端点限制进一步把最大区间和拆成
- 线段树只负责区间信息合并,可持久化只负责共享相邻阈值下未改变的部分。
- 上中位数的定义决定判定条件是区间和不小于 。若改成下中位数,偶数长度时的判定边界也会随之改变。
【参考代码】
/*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;}