序列分治学习笔记

0x01 前言

序列分治作为一种常见的解决序列问题的算法,有着许许多多的广泛应用。下至普及,上至 NOI,都能看见它的身影。

今年 S 组第一轮完善程序 T2 就考了序列分治,虽然对于那个问题来说分治并不是最优解,但是笔者从中学到了一种序列分治的写法。这也是本文的灵感来源。

本文主要介绍较为基础的序列分治,适合初学者使用。因为笔者也是初学者。

附题单

0x02 算法流程

序列分治通常用于解决⌈子区间贡献⌋一类问题,其基本思想是,假设要计算 [L,R] 范围内的子区间 [l,r] 的贡献,找到区间中点 mid=L+R2,将子区间分为三类:

  • [l,r][L,mid] 内。即 Llrmid

  • [l,r][mid+1,R] 内。即 mid+1lrR

  • [l,r] 跨过 mid,即 Llmid<rR

考虑一个递归的过程,先递归计算 [L,mid][mid+1,R] 的贡献,再计算第三种,即跨过中点的子区间的贡献。

边界:当 L=R 时,可以直接计算贡献,并返回。我们发现这个分治的结构和线段树十分相似。

时间复杂度的计算通常使用主定理,但是笔者主定理学的很烂,所以喜欢画递归树分析。

不妨通过一道经典的例题来理解这个过程:

SP32079 ADAGF - Ada and Greenflies

  • 给出一个序列 a1an,求:

l=1nr=lngcdi=lrai

  • n3×105,值域大小 |V|106

考虑对序列进行分治。设当前分治区间为 [L,R],中点 mid=L+R2。考虑如何计算跨过中点的贡献。

注意到固定右端点 r 后,区间的 gcd 只会有 O(log|V|) 种。因为当左端点左移 1 步时,gcd 要么不变,要么至少除以 2

一个跨过中点的区间一定是个左半区间的后缀拼上一个右半区间的前缀。因此考虑预处理左半区间所有后缀以及右半区间所有前缀的 gcd 的出现次数,使用 __gnu_pbds::gp_hash_table 维护 cl,cr 两个桶。

最后枚举左半区间后缀的 gl,右半区间前缀的 gr,根据乘法原理,贡献为 gcd(gl,gr)clglcrgr

时间复杂度可以这么计算:

  • 设当前分治区间长度为 N

  • 求左半区间后缀、右半区间前缀的 gcd 时,gcd 只变化 O(log|V|) 次,这 O(log|V|) 次辗转相除的时间复杂度为 O(log2|V|)。而剩下 O(N)gcd 不变的时候,根据辗转相除的具体实现容易得知,这些时候求 gcd 的时间复杂度为 O(1)

  • __gnu_pbds::gp_hash_table 的时间复杂度为 O(1),枚举左半区间后缀、右半区间前缀的 gcd 的时间复杂度为 O(log2|V|)

  • 容易发现瓶颈在于求 gcd,当前分治区间的时间复杂度为 O(N)

  • 根据主定理容易得知整个算法的时间复杂度 T(n)=2T(n2)+O(n)=O(nlogn)

  • 用另一种方式分析,分治一共会进行 logn 层,每一层的总时间复杂度为 O(N),容易发现对于任意一层 N=n,因此时间复杂度为 O(nlogn)

空间复杂度为 O(n)

提交记录 代码

再来看一道例题:

CF817D Imbalanced Array

双倍经验:SP10622

三倍经验(弱化版):AGC005B

  • 给出长度为 n 的序列 a1an,求:

l=1nr=ln(maxi=lraimini=lrai)

  • n106

这个问题就是今年 S1 完善程序 T2。它给出了一种分治求解⌈最值贡献/限制⌋的方法。

不妨将式子拆开:

l=1nr=ln(maxi=lraimini=lrai)=l=1nr=lnmaxi=lrail=1nr=lnmini=lrai

两个式子的计算是类似的,这里以最大值为例。

设当前分治区间为 [L,R],中点 mid=L+R2。考虑如何计算跨过中点的贡献。

考虑枚举左端点,求出以 i 为左端点、跨过 mid 的区间的总贡献,那么就是 [i,mid+1],[i,mid+2],,[i,r] 这些区间。

维护一个前缀最大值数组 prex=maxu=mid+1xau,再对 prex 维护前缀和 sumx=u=mid+1xpreu

考虑从右往左枚举左端点 i,设右端点为 j,再记一个 mx=maxu=imidau。不难发现可以找到一个分界点 k 使得当 j(mid,k) 时,区间 [i,j] 的最大值为 mx;当 j[k,r] 时,区间 [i,j] 的最大值为 prej。且随着 i 的减小,k 单调不降。

那么对于 (mid,k) 的区间,贡献为 (k1mid)mx;对于 [k,r] 的区间贡献为 u=krpreu=sumrsumk1

边界:当 L=R 时,贡献为 aL。最小值类似利用单调性做就行了。

时间复杂度为 O(nlogn),空间复杂度为 O(n)

提交记录(含代码)

0x03 习题

Ⅰ - JOISC2014H JOIOJI

  • 给出一个长度为 n 的字符串,仅由 J,O,I 三种字母组成,求一个最长的子串,使得其中三种字母出现次数相等。

  • n2×105

这题扫描线严格优于分治。

双倍经验(弱化版):CF873B Balanced Substring

考虑分治,设当前的区间为 [l,r],中点为 mid。枚举右端点 j,考虑怎样的左端点 i 能够与之组合。

uj=k=mid+1j[sk=J]k=mid+1j[sk=O]vj=k=mid+1j[sk=O]k=mid+1j[sk=I]ai=k=imid[sk=J]k=imid[sk=O]bi=k=imid[sk=O]k=imid[sk=I]

根据题意得知区间满足的条件为:

k=ij[sk=J]=k=ij[sk=O]=k=ij[sk=I]

整理得 ai+uj=0bi+vj=0

于是在左半区间用 map 维护每一个二元组 (ai,bi) 出现的最左位置(因为可能有相同的,而且要使得区间最长)。对于右半区间的 j,用 (uj,vj) 对应的位置与之匹配。

时间复杂度为 O(nlog2n),空间复杂度为 O(n)

提交记录(含代码)

Ⅱ - CF549F Yura and Developers

  • 给定数组 a1an 和常数 k,求有多少个区间 [l,r],满足:

    • rl+12

    • (i=lraimaxi=lrai)modk=0

  • n3×105k106

考虑分治。设当前分治区间左端点为 l,右端点为 r,中点为 mid,考虑如何计算跨过中点的贡献。

首先对于右半区间,维护 prep=maxu=mid+1pausump=(v=mid+1pav)modkdifp=(sumpprep)modk,即以 mid+1 为起点的前缀最大值、模 k 意义下的前缀和以及它们的差对 k 取模的值。

从右往左扫描跨过中点的区间的左端点 i,记 suf=maxv=imidavs=(u=imidau)modk,我们要找到一个位置 j,使得:

maxu=mid+1j1ausufw[j,r],maxu=mid+1wau>suf

说白了就是右端点取在 j 及其左边区间最大值位于 mid 及其左边,右端点取在 j 右边区间最大值位于 mid 右边。不难发现随着 i 递减,j 不降

分别计算以 j 为界的两部分的贡献,对于 j 及其左边,要找到这样的右端点 x,使得 (s+sumxsuf)modk=0,移项得 sumx=(ks+suf)modk;对于 j 右边,要找到这样的右端点 y,使得 (s+dify)modk=0,移项得 dify=(ks)modk

问题变成求 (mid,j) 中有多少 sum 值为 (ks+suf)modk[j,r] 中有多少 dif 值为 (ks)modk

考虑维护 b1,b2 两个桶,分别表示扫到当前的 j[j,r]dif 在每种值各出现了几次和 (mid,j)sum 在每种值各出现了几次。j 增加到 j+1 时,相当于 (mid,j) 比原来多包含了一个 j 位置,将 b2sumj 增加 1[j,r] 比原来少包含了一个 j 位置,将 b1difj 减去 1

这么一来,左端点 i 的贡献为 b2(ks+suf)modk+b1(ks)modk,注意边界、负数取模以及清空。

时间复杂度为 O(nlogn),空间复杂度为 O(n+k)

提交记录(含代码)

Ⅲ - ABC282Ex Min + Sum

  • 给出两个长为 n 的序列 a,b 和常数 S,求有多少个区间 [l,r](1lrn),满足:

    mini=lrai+j=lrbjS

  • n2×105

考虑分治。设当前分治区间为 [l,r],分治中点 mid=l+r2

考虑如何统计跨过中点的区间个数。枚举左端点 i,同时记录 s=x=imidbxminn=miny=imiday。求出有多少个合法的右端点 j

对于 (mid,r] 这部分区间的所有 j,预处理 sumj=u=mid+1jbuprej=minv=mid+1jav

把右端点 j 分成两种,分别统计:

  • prejminn,则符合条件的区间满足 s+sumj+minnS,那么统计有多少 j 满足 sumjSsminn

  • prej<minn,则符合条件的区间满足 s+sumj+prejS,那么统计有多少 j 满足 sumj+prejSs

考虑从右往左枚举左端点 i,不难发现可以找到一个分界点 k 使得当 j(mid,k) 时,区间满足第一种情况;当 j[k,r] 时,区间满足第二种情况。且随着 i 的减小,k 单调不降。

使用平衡树 t1,t2 分别维护 (mid,k)sumj 的权值集合和 [k,r]sumj+prej 的权值集合。分界点 k 右移时(接下来的 k 是右移前的 k),在 t1 中插入 sumk,在 t2 中删除 sumk+prek。可以完成⌈查询集合内有多少数不超过某个定值⌋这个操作。

有人可能会说平衡树小题大做,但是 __gnu_pbds::tree 真的很方便。

时间复杂度为 O(nlog2n),空间复杂度为 O(n)

提交记录(含代码)

Ⅳ - CF1156E Special Segments of Permutation

  • 给定一个长度为 n 的排列 p1pn,求有多少对 (l,r),满足:

    • lr

    • pl+pr=maxi=lrpi

  • n2×105

同样考虑分治。设当前分治区间为 [L,R],中点 mid=L+R2。考虑计算跨过中点的区间个数。

对于右半区间,记录 prex=maxu=mid+1xpx。用同样的套路,从右往左扫描左端点 i,记录 maxn=maxu=imidpu。维护一个单调不降的指针 k 使得当右端点 j(mid,k) 时,maxu=ij=maxn;当 j[k,R] 时,maxu=ij=prej

  • 对于 j(mid,k) 的区间,要统计 pi+pj=maxnj 的个数,变形成 pj=mxpi。故维护一个桶 mp1 表示 (mid,k)pj 每种取值的出现次数,贡献为 mp1maxnpi

  • 对于 j[k,R] 的区间,要统计 pi+pj=prejj 的个数,变形成 prejpj=pi。故维护一个桶 mp2 表示 [k,R]prejpj 每种取值的出现次数,贡献为 mp2pi

注意到 maxnpi,prejpj0,因此直接开数组维护桶就行了。k 右移时(下面的 k 是右移前的 k),将 mp1pkmp1pk+1mp2prekpkmp2prekpk1。原因和前几题也是类似的,你就考虑 k 这个位置被放到哪个区间了。

边界:当 L=R 时,返回 0

时间复杂度为 O(nlogn),空间复杂度为 O(n)

提交记录(含代码)

Ⅴ - P4755 Beautiful Pair

  • 给定长度为 n 的序列 a1an,求有多少个区间 [l,r] 满足 alarmaxi=lrai

  • n105

考虑分治,设当前分治区间为 [L,R],中点 mid=L+R2。考虑如何统计跨过中点的贡献。

考虑 midL 扫描左端点 i,统计怎样的右端点 j(j(mid,R]) 合法。

预处理数组 prex=maxu=mid+1x,在扫描的过程中同时记录 mx=maxu=imidau

维护一个单调的指针 k 使得当 j(mid,k) 是区间最大值为 mxj[k,R] 时区间最大值为 prej

j(mid,k) 时,条件可以改写成 ajmxai;当 j[k,R] 时,条件可以改写成 prejajai

那么就用两棵动态开点线段树维护 (mid,k)aj 每种权值的个数以及 [k,R]prejaj 每种权值的个数。k 移动时处理这个位置的变化量。

边界 L=R 时,答案为 1

注意清空。可以把所有用到的节点放到一个容器里面再单独拿出来清空。

设值域为 V,时间复杂度为 O(nlognlog|V|),空间复杂度为 O(nlog|V|)

提交记录 代码

Ⅵ - ABC248Ex Beautiful Subsequences

  • 给定长度为 n 的排列 a1an 以及整数 k,求有多少个区间 [l,r] 满足 :

maxi=lraimini=lrairl+k

  • n1.4×105k3

考虑分治,设当前分治区间为 [L,R],中点 mid=L+R2。考虑如何统计跨过中点的贡献。怎么全都是这句。

考虑 midL 扫描左端点 i,统计怎样的右端点 j(j(mid,R]) 合法。

预处理数组 pmxx=maxu=mid+1xau,pmn=minu=mid+1xau。在扫描的过程中同时记录 mx=maxu=imidau,mn=minu=imidau

维护两个指针 jmx,jmn,使得:

  • j(mid,jmx) 时,maxu=ijau=mx;当 j[jmx,R] 时,maxu=ijau=pmxj

  • j(mid,jmn) 时,minu=ijau=mn;当 j[jmn,R] 时,minu=ijau=pmnj

不难发现当 i 单调递减时,jmx,jmn 单调不降。

分两大种、六小种情况讨论(加粗的是结论):

  • jmnjmx 时:

    • j(mid,jmn) 时,满足的条件等价于 mxmnji+k,变形成 jmxmn+ik

      即统计有多少 j 满足 j(mid,jmn)jmxmn+ik

    • j[jmn,jmx) 时,满足的条件等价于 mxpmnjji+k,变形成 j+pmnji+mxk

      即统计有多少 j 满足 j[jmn,jmx)j+pmnji+mxk

    • j[jmx,R] 时,满足的条件等价于 pmxjpmnjji+k,变形成 jpmxj+pmnjik

      即统计有多少 j 满足 j[jmx,R]jpmxj+pmnjik

  • jmn>jmx 时:

    • j(mid,jmx) 时,满足的条件等价于 mxmnji+k,变形成 jmxmn+ik

      即统计有多少 j 满足 j(mid,jmx)jmxmn+ik

    • j[jmx,jmn) 时,满足的条件等价于 pmxjmnji+k,变形成 jpmxjimnk

      即统计有多少 j 满足 j[jmx,jmn)jpmxjimnk

    • j[jmn,R] 时,满足的条件等价于 pmxjpmnjji+k,变形成 jpmxj+pmnjik

      即统计有多少 j 满足 j[jmn,R]jpmxj+pmnjik

我们发现六种情况本质上是四类统计,每一类统计都是二维数点。

具体地,建立四棵主席树 t1,t2,t3,t4,分别对于每个前缀 (mid,p](p(mid,R]) 版本,在节点 [l,r] 内维护这个前缀中有多少个 j,j+pmnj,jpmxj,jpmxj+pmnj 的值在 [l,r] 这个范围内。

统计的时候,先看是哪一大类,再对于三种小类,运用结论得到要统计的权值区间,并将 j 所在的范围拆成两个前缀版本相减的形式去做区间求和即可。

边界:当 L=R 时,返回 1

时间复杂度为 O(nlog2n),空间复杂度为 O(nlogn)。据说二维数点的时候有更高明的桶做法,但是我太弱了不会。

提交记录(含代码)

posted @   lzyqwq  阅读(244)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 三行代码完成国际化适配,妙~啊~
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
点击右上角即可分享
微信分享提示