P1052 · OFFICIAL SOLUTION

P1052 序列询问 官方题解

Gioush OJ · P1052 序列询问

序列询问

【题意简述】

给定一个二进制序列。若当前元素与栈顶相同,则弹出栈顶,否则压入当前元素,由此定义序列的消除结果。

对每次询问 [l,r][l,r],枚举分割点 lm<rl\leq m<r,分别消除左右两段,再把两个消除结果拼接并继续消除。求所有分割点在拼接以后新消除的元素对数之和。

【数据点 1∼31\sim 31∼3】

【题目描述】

数据范围允许按照定义直接模拟。

【Hint】

对每个分割点分别用栈处理左右两段,再拼接两个栈中的序列。

【解法】

记录拼接后新弹出的元素对数,并对全部分割点求和。时间复杂度为 O(qn2)\mathcal{O}(qn^2)

【数据点 4∼94\sim 94∼9】

【题目描述】

需要把单个分割点的计算降至常数时间。

【Hint】

消除结束后的序列中不存在相邻且相同的元素,因此它一定是一个 0,10,1 交替序列。

【解法】

对每次询问,从左向右维护所有前缀的消除长度,从右向左维护所有后缀的消除长度。加入一个元素时,消除结果的长度只会增加 11 或减少 11

预处理完成后,可以在 O(1)\mathcal{O}(1) 时间计算一个分割点,单次询问复杂度为 O(n)\mathcal{O}(n),总时间复杂度为 O(qn)\mathcal{O}(qn)

【数据点 10∼1510\sim 1510∼15】

【题目描述】

需要用一个代数量描述区间消除后的长度。

【Hint】

定义

si={1,aii(mod2),1,ai≢i(mod2),Pi=j=1isj.s_i= \begin{cases} 1,&a_i\equiv i\pmod 2,\\ -1,&a_i\not\equiv i\pmod 2, \end{cases} \qquad P_i=\sum_{j=1}^{i}s_j.

【解法】

先证明下标奇偶性。设当前被消去的两个元素来自原序列中的位置 x<yx<y。它们在当前序列中相邻,说明原来位于二者之间的 yx1y-x-1 个元素已经全部被成对消去。因此 yx1y-x-1 为偶数,x,yx,y 的奇偶性相反。

被消去的两个元素数值相同、下标奇偶性相反,所以对应的 sx,sys_x,s_y 互为相反数。每次消除都不会改变区间内 sis_i 的总和。

再考虑最终剩余序列中相邻的两个元素。二者之间的原序列元素同样已经全部被成对消去,所以它们的原下标奇偶性相反;而最终序列不存在相邻且相同的元素,所以它们的数值也相反。数值与下标奇偶性同时改变以后,“二者是否相等”这一关系不变,因此相邻剩余元素对应的 sis_i 相同。由此,全部剩余元素对应的 sis_i 均同号。

消除前后 sis_i 的总和不变,而消除结束后每一项都等于 11 或都等于 1-1,故剩余长度恰好等于这个总和的绝对值:

red(al,,ar)=PrPl1.\left|\operatorname{red}(a_l,\ldots,a_r)\right| = \left|P_r-P_{l-1}\right|.

枚举分割点即可做到 O(qn)\mathcal{O}(qn)

【正解】

【题目描述】

需要对一个询问中的全部分割点同时求和。

【Hint】

对询问 [l,r][l,r],记

A=Pl1,B=Pr,X=Pm.A=P_{l-1},\qquad B=P_r,\qquad X=P_m.

把分割点 mm 的贡献写成关于 A,B,XA,B,X 的绝对值表达式。

【公式】

还需要说明分段消除不会改变最终结果。对任意两个序列 U,VU,V,先处理 UU 后,栈中恰好留下 red(U)\operatorname{red}(U);继续把 VV 依次送入同一个栈,与把 UVU\circ V 从头送入栈完全相同。因此

red(red(U)red(V))=red(UV).\operatorname{red}\left( \operatorname{red}(U)\circ\operatorname{red}(V) \right) = \operatorname{red}(U\circ V).

左右两段消除后的长度分别为 XA\lvert X-A\rvertBX\lvert B-X\rvert,拼接并继续消除后的长度因而等于整个区间的消除长度 BA\lvert B-A\rvert。每次新消除恰好删去两个元素,所以

f(m)=XA+BXBA2.f(m) = \dfrac{ \lvert X-A\rvert+\lvert B-X\rvert-\lvert B-A\rvert }{2}.

定义

G(L,R,x)=i=LRPix.G(L,R,x)=\sum_{i=L}^{R}\lvert P_i-x\rvert.

lm<rl\leq m<r 求和,得到

Ans(l,r)=G(l,r1,Pl1)+G(l,r1,Pr)(rl)PrPl12.\operatorname{Ans}(l,r) = \dfrac{ G(l,r-1,P_{l-1})+G(l,r-1,P_r) -(r-l)\lvert P_r-P_{l-1}\rvert }{2}.

【离线计算】

每个原询问被拆成两个形如 G(L,R,x)G(L,R,x) 的离线询问。

将位置 ii 按照 PiP_i 从小到大排序,将全部离线询问按照 xx 从小到大排序。扫描询问时,把所有满足 PixP_i\leq x 的位置加入数据结构。使用两棵树状数组,分别维护这些位置的数量与 PiP_i 之和。

记区间内 PixP_i\leq x 的数量与和为 C,SC_{\leq},S_{\leq},全部元素的数量与和为 C,SC,S,则

G(L,R,x)=xCS+(SS)x(CC).\begin{aligned} G(L,R,x) ={}&xC_{\leq}-S_{\leq}\\ &+(S-S_{\leq})-x(C-C_{\leq}). \end{aligned}

前两项统计不大于 xx 的部分,后两项统计大于 xx 的部分。全部元素的和 SS 可以由 PiP_i 的普通前缀和求出。

【复杂度分析】

单组测试数据的时间复杂度为 O((n+q)logn)\mathcal{O}((n+q)\log n),空间复杂度为 O(n+q)\mathcal{O}(n+q)

【参考代码】

/*Author:EhundateghDate:2026/7/24Name:query.cppYou steal,I kill.*/#include <cstdio>#include <cstring>#include <algorithm>#define MAXN 500010#define MAXQ 1000010using namespace std;int n,q,Line[MAXN],Pre[MAXN],Position[MAXN],Left[MAXN],Right[MAXN];long long IndexSum[MAXN],Cnt[MAXN],Sum[MAXN],Part[MAXQ],Ans[MAXN];struct ask{    int Left,Right,Value,Id;}Ask[MAXQ];bool cmpa(int x,int y){return Pre[x]<Pre[y];}bool cmpb(ask x,ask y){return x.Value<y.Value;}int lowbit(int x){return x&-x;}void Modify(int x,long long Val,long long C[]){    for(;x<=n;x+=lowbit(x)) C[x]+=Val;}long long Query(int x,long long C[]){    long long Ret=0;    for(;x;x-=lowbit(x)) Ret+=C[x];    return Ret;}void Solve(){    static int LastN=0;    scanf("%d%d",&n,&q);    int ClearN=max(n,LastN);    memset(Cnt,0,sizeof(long long)*(ClearN+1));    memset(Sum,0,sizeof(long long)*(ClearN+1));    Pre[0]=IndexSum[0]=0;    LastN=n;    for(int i=1;i<=n;i++){        scanf("%d",&Line[i]);        Pre[i]=Pre[i-1]+(Line[i]==(i&1)?1:-1);        IndexSum[i]=IndexSum[i-1]+Pre[i];        Position[i]=i;    }    int Total=0;    for(int i=1;i<=q;i++){        scanf("%d%d",&Left[i],&Right[i]);        Ask[++Total]={Left[i],Right[i]-1,Pre[Left[i]-1],i*2-1};        Ask[++Total]={Left[i],Right[i]-1,Pre[Right[i]],i*2};    }    sort(Position+1,Position+n+1,cmpa);    sort(Ask+1,Ask+Total+1,cmpb);    int Now=1;    for(int i=1;i<=Total;i++){        while(Now<=n&&Pre[Position[Now]]<=Ask[i].Value){            Modify(Position[Now],1,Cnt);            Modify(Position[Now],Pre[Position[Now]],Sum);            Now++;        }        int l=Ask[i].Left,r=Ask[i].Right;        long long CntLow=Query(r,Cnt)-Query(l-1,Cnt);        long long SumLow=Query(r,Sum)-Query(l-1,Sum);        long long Count=r-l+1,SumAll=IndexSum[r]-IndexSum[l-1];        Part[Ask[i].Id]=1ll*Ask[i].Value*CntLow-SumLow+                        SumAll-SumLow-1ll*Ask[i].Value*(Count-CntLow);    }    for(int i=1;i<=q;i++){        Ans[i]=(Part[i*2-1]+Part[i*2]-                1ll*(Right[i]-Left[i])*abs(Pre[Right[i]]-Pre[Left[i]-1]))/2;        printf("%lld\n",Ans[i]);    }}int main(){    int c,T;    scanf("%d%d",&c,&T);    while(T-->0) Solve();    return 0;}