一、减少约束条件 / 从特殊情况入手(对于明显堆砌约束条件的题目较为有效)
像这道题目,对于重建的这个约束太多。
Consider the case without any “accidents”, i.e., when there are no additional restrictions on the reconstructed streets.
不过,悲观地而言,只应用这一技巧想把这道题目做出来没什么用啊。
题目大意
给定一个 n×m 的网格图,包含交汇点 (1,1) 到 (n,m)。网格中的相邻交汇点之间存在道路:
点 (i,j) 与 (i+1,j) 之间为垂直道路,其权值为 wi,j。
点 (i,j) 与 (i,j+1) 之间为水平道路,其权值为 vi,j。
由于一些事故,部分道路无法被重建(由输入状态 0 表示),其余道路可选择是否重建(由输入状态 1 表示)。
对于网格中的任意一个交汇点,如果与其相连且最终被重建的道路数量为偶数,则称该交汇点是“优雅的”。
如果所有的交汇点都是“优雅的”,则称当前的整个道路重建方案是“完美的”。
美丽值计算方式
对于一个“完美的”重建方案,其“美丽值”的计算方式如下:
初始美丽值为 0。
对于每一行 i (1≤i≤n−1),找出所有在这一行向下延伸(即连接 (i,j) 与 (i+1,j))且被重建的垂直道路。设它们所在的列号从小到大依次为 c1<c2<c3<…,则将 wi,c1−wi,c2+wi,c3−wi,c4+… 累加到美丽值中。
对于每一列 j (1≤j≤m−1),找出所有在这一列向右延伸(即连接 (i,j) 与 (i,j+1))且被重建的水平道路。设它们所在的行号从小到大依次为 r1<r2<r3<…,则将 vr1,j−vr2,j+vr3,j−vr4,j+… 累加到美丽值中。
(简单来说,就是分别对“每行向下的重建道路”和“每列向右的重建道路”按坐标顺序交替加减其权值)
目标:在所有“完美的”重建方案中,求出最大的“美丽值”。

输入格式
第一行包含测试用例数 t (1≤t≤5⋅104)。
每个测试用例的第一行包含 n 和 m (2≤n,m≤2⋅105,∑n⋅m≤4⋅105)。
接下来 n−1 行,每行 m 个整数,表示垂直道路的权值 wi,j。
接下来 n 行,每行 m−1 个整数,表示水平道路的权值 vi,j。
接下来 n−1 行,每行一个长度为 m 的 01 字符串,表示各条垂直道路是否可以被重建。
接下来 n 行,每行一个长度为 m−1 的 01 字符串,表示各条水平道路是否可以被重建。
输出格式
对于每个测试用例,输出一个整数,表示在所有完美重建方案中能获得的最大美丽值。
样例数据
1 | 4 |
样例解释
以第一个测试用例为例(n=3,m=4,且所有道路均允许重建),在使得所有交汇点相连重建道路数均为偶数的前提下,取得最大美丽值的方案如下:
垂直道路(向下的边)的选择:
第 1 行向下重建位于列 1、列 3 的道路,权值分别为 2,−2,贡献值为:2−(−2)
第 2 行向下重建位于列 2、列 4 的道路,权值分别为 9,−4,贡献值为:9−(−4)
水平道路(向右的边)的选择:
第 1 列向右重建位于行 1、行 2 的道路,权值分别为 3,−9,贡献值为:3−(−9)
第 2 列向右重建位于行 1、行 3 的道路,权值分别为 4,−1,贡献值为:4−(−1)
第 3 列向右重建位于行 2、行 3 的道路,权值分别为 1,−3,贡献值为:1−(−3)
该方案不仅满足所有交汇点度数皆为偶数,且其总美丽值为:
(2−(−2))+(9−(−4))+(3−(−9))+(4−(−1))+(1−(−3))
=4+13+12+5+4=38。
在第二个测试用例中,受限于无法重建的道路限制,唯一的完美重建方案是“不重建任何道路”,因此最大美丽值为 0。
像这道题目,对于重建的这个约束太多。
Consider the case without any “accidents”, i.e., when there are no additional restrictions on the reconstructed streets.
我们可以先把所有对于重建边操作的限制,就是有一些边不能进行重建操作,给删除,我们会做了吗?(注意这里不能够把这个偶数条边的这个结果也给删了)
其实也没有那么简单,你首先要注意到,对于一维问题,成对的±,相当于一系列差分的和。那么,还需要特意去选吗?直接选全部为正的差分值不就好了?(连续选了两个或更多差分值,有重叠也没关系,可以使用我们下图的逆运算)

但是这个只能解决成对的 ± 问题呀,要是 ±+,以 + 结尾的不成对串如何解决呢?
来,我们仔细看看。只需要在末尾加一个 0,然后采用同样的贪心即可。

好,下面我们要正式解决这个问题了。
问题来到了 2D,我们发现有一个这个约束,就是,点的偶数边重建约束,这个还是有点烦的,怎么样去解决这个问题?
不难想到,如果一个点的边是否重建受到约束,我们能不能找到一个东西,其无论怎么样取,都符合约束呢?这样无论是贪心还是 dp,都会好解决的多。
Codeforces Round 1026 (Div. 2)-CF-1026-E. Melody
Codeforces Round 1081 (Div. 2)-CF-1081-E. Swap to Rearrange(按值进行图论建模)(欧拉回路)
通过这些题目,我们知道,如果说所有的点的度数都是偶数,组成的一定是一个欧拉回路(在无向联通图中)。
那么在这道题目中,题目所给我们的图,实际上是一个对偶图(就是边不相交的平面图,显然,网格图是对偶图),欧拉回路在对偶图中,实际上是一个割(这个是不难得出的,说白了,你在平面上一笔画出一条回路,当然能把平面划分为两部分,特别还要求你的这个路径是不会自相交的,因为所有边都互不相交)。注意,可以确定的是,这些欧拉回路
那既然是一个割,把一个面分割成了两部分,我们就想到了黑白染色。
这个时候求解为什么能想到小的单位面元?从更高的观点来看,其实这个是离散型二维格林公式。
在高等数学里,格林公式极其优美,它建立了一个桥梁:
∮C(Pdx+Qdy)=∬D(∂x∂Q−∂y∂P)dxdy
题目定义的“美丽值”计算方式非常奇怪:
“在每一行 i 中,选中的列 c1,c2,c3,c4… 交替加减:+wi,c1−wi,c2+wi,c3…”
我们用几何的眼光来看这个式子。
题目要求所有十字路口的联边数必须是偶数。在图论里,这意味着选出来的边,必定构成若干个闭合的环(多边形)。
假设我们选出了一个大矩形环(左边界在 c1,右边界在 c2,上边界在 r1,下边界在 r2)。
按照题目的计算规则:
既然题目的美丽值求的是一个大环的“线积分(环量)”。
那么根据格林公式的思想,我们完全可以把这个大环,撕碎成无数个 1×1 的“无穷小面元(Basis / 小方块)”!
我们取网格中第 i 行、第 j 列的那个 1×1 的小方块。
如果我们把题目的线积分规则(左+,右-,上+,下-)强制套用到这个最微小的方块上,它就会产生一个属于它自己的“旋度”(也就是内在权值):
Weighti,j=wi,j左边界 (+) −wi,j+1右边界 (-) +vi,j上边界 (+) 下边界 (-) −vi+1,j
现在,我们见证格林公式在离散网格上发威的瞬间。
如果我们选定了某几个连续的小方块(构成了一个区域 D)。
我们直接把这几个小方块的 Weight(旋度)全部加起来。
会发生什么?
比如方块 (i,j) 和右边的方块 (i,j+1) 被同时选中。
∬D(小方块权值)=∮∂D(题目要求的美丽值交替求和)
格林公式告诉我们:算复杂的边界太难了,算简单的内部面元再相加就容易了。
题目要求我们在无数种复杂的“偶数闭合边界”中找最大值,这极其困难,因为边界的形状千变万化,且互相牵制。
但格林公式赋予了我们降维打击的武器:
我们根本不去管什么边界和交替相加!我们直接算好每一个 1×1 小方块的固定内在权值(旋度),然后只要权值是正的,我就把它拿走(贪心选这个面元)。
你拿走的这堆正权值面元,它们的外边缘自然而然就生成了题目要求的最优闭合边界,且美丽值自动最大化!
这就是这道算法题背后最深沉、最优美的数学灵魂。

具体而言,如下图所示:

现在问题在于如何处理这个有些边不能进行重建操作?

那么上图已经讲的很清楚了,不能重建的边,就像是胶水,直接把这两块给粘起来,这条边就不会再操作了。唯一一个特殊点就是最边上的边就是和外部联通了,这里我们需要设置一个外部节点 out_node,把他和 out 连接在一起即可。
AC
https://codeforces.com/contest/2205/submission/365321534
1 | /** |

在并查集中,直接改动节点的值是非常幼稚的,因为你不知道他是不是根节点,当然你可以 find 后改根节点。

AC
https://codeforces.com/contest/2205/submission/365204709
AI 代码确实是没什么问题。
题目描述
给定一个长度为 n 的目标数组 T。
现有一个与 T 等长的初始数组 S,对 S 进行恰好一次如下操作:
选择一个整数 k,并将数组 S 划分为 k 个连续且不重叠的子段。假设这 k 个子段的下标区间依次为 [l1,r1],[l2,r2],…,[lk,rk],满足 l1=1,rk=n,且对于所有的 1≤i≤k−1,都有 ri+1=li+1。
然后,对这 k 个子段分别独立地进行翻转操作。
即,选择的区间形如:

请求出有多少种不同的初始数组 S,可以通过上述操作变为目标数组 T。由于答案可能很大,请输出其对 998244353 取模后的结果。
输入格式
第一行包含一个整数 t(1≤t≤8000),表示测试用例的数量。
每组测试用例的第一行包含一个整数 n(1≤n≤8000),表示数组的长度。
第二行包含 n 个整数 T1,T2,…,Tn(1≤Ti≤8000),表示目标数组 T 的元素。
保证所有测试用例中 n 的总和不超过 8000。
输出格式
对于每组测试用例,输出一个整数,表示满足条件的数组 S 的数量对 998244353 取模后的结果。
样例输入
1 | 5 |
样例输出
1 | 4 |
样例解释
在第一组测试用例中,T=[1,1,2,1],以下 4 种数组 S 可以被转换为 T:
对于 S=[2,1,1,1],可以选择区间 [1,3] 和 [4,4] 进行翻转。翻转后数组变为 [1,1,2,1],等于 T。
对于 S=[1,2,1,1],可以选择区间 [1,4] 进行翻转。
对于 S=[1,1,2,1],可以选择区间 [1,2]、[3,3] 和 [4,4] 进行翻转。
对于 S=[1,1,1,2],可以选择区间 [1,2] 和 [3,4] 进行翻转。
在第二组测试用例中,T=[1,2,3,1],以下 7 种数组 S 可以被转换为 T:
对于 S=[1,1,3,2],可以选择区间 [1,1] 和 [2,4] 进行翻转。
对于 S=[1,2,1,3],可以选择区间 [1,1]、[2,2] 和 [3,4] 进行翻转。
对于 S=[1,2,3,1],可以选择区间 [1,1]、[2,2]、[3,3] 和 [4,4] 进行翻转。
对于 S=[1,3,2,1],可以选择区间 [1,4] 进行翻转。
对于 S=[2,1,1,3],可以选择区间 [1,2] 和 [3,4] 进行翻转。
对于 S=[2,1,3,1],可以选择区间 [1,2]、[3,3] 和 [4,4] 进行翻转。
对于 S=[3,2,1,1],可以选择区间 [1,3] 和 [4,4] 进行翻转。
分析 rev 操作的一个重要公式是:
rev(XY)=rev(Y)rev(X)
可以继续拓展:


所以,我们要求转移时前缀相同,只采用这一后缀,即可得到独特的计数。
具体来讲,我们对这一后缀的要求是(在前缀相同的情况下)这一后缀无论如何划分,都达不到整体的效果。
1 | rev(S) != rev(任意对S的划分方法) |
比如说 [1, 2, 3, 4] 任何其他划分方式都达不到 rev([1, 2, 3, 4]) 的效果。




不过上面,opus4.6 给出的解释实际上不是很令人满意,因为带有非常明显的知道答案,凑出证明的痕迹。我们采用一种更加好的方法?(可能没有那么严谨,但是实际上是严谨的)

P4391 [BalticOI 2009] Radio Transmission 无线传输
我们不难发现,可以这种所谓的查找目标 C=C′,其实就是查找周期,其实和查找周期的哈希实现一模一样(详见上面一道题目)。

相信看了我的这个解释,应该都能看出来这个东西。
其实,形如 rev(S)=rev(ABA),其实是唯一一种形式,可以保证分段反转等于整体反转。
不过快速检查一个字符串是否有真前后缀相同,显然是使用 kmp(前缀数组,前缀数组就是这么定义的,能不好吗?) 还是比较好的,使用字符串哈希时间复杂度不是很优秀。
好,现在问题就来到了,我们怎么样求 s[l;r] 是否有这个真前后缀匹配情况?

哈哈,其实解决这个问题的方法也很简单,那么就是跑 n 遍 kmp 就可以了,这道题目不是 O(n2) 的嘛。绷不住了。
1 | vector<ll> kmp(ll N,const vector<ll> &A){ // 返回总前缀数组pi |
最后的这个 dp 代码:
1 | for (int l=1;l<=N;++l) { |
AC
https://codeforces.com/contest/2205/submission/365177121
这个代码跑的好快啊,竟然
1 | /** |
要好好想清楚这个 dp 是从哪里转移过来的,是从 l-1 转移过来的。
1 | for (int l=1;l<=N;++l) { |
下面是废掉的 AI 提示
题意转化
题目要求:给定目标数组 T,有多少个不同的初始数组 S,使得存在某种将 S 切成若干连续段后分别翻转能得到 T。
由于"分段翻转"是自逆操作(对同一分段再做一次翻转就还原),S 能变成 T 当且仅当 T 用同样的分段翻转能变成 S。因此问题等价于:
把 T 切成若干连续段并分别翻转,能得到多少种不同的数组?
核心观察:何时两种切法产生同一结果
设切法 1 的最后一段是 [j1+1,i],切法 2 的最后一段是 [j2+1,i](j1<j2)。经过推导可知,它们在位置 j2+1 到 i 上的值相同当且仅当:
T[q]=T[q+d],j1+1≤q≤i−d,d=j2−j1
即子串 T[j1+1..i] 具有周期 d。
关键引理:非本原段可以缩短
若 T[j+1..i] 具有周期 d<i−j(即该子串不是"本原的"),则所有以 [j+1,i] 为最后一段能产生的结果,都能通过以 [j+d+1,i] 为最后一段来实现。
证明思路:将最后一段 [j+1,i] 的前 d 个字符"吸收"到前缀中——由于周期性,这前 d 个翻转后的值恰好等于对 T[j+1..j+d] 的某种合法分段翻转结果(具体地,用切点 j+r 把它分成两段,其中 r=(i−j)modd)。
推论:本原段之间不产生重复
若两个最后一段 [j1+1,i] 和 [j2+1,i] 都是本原的(最小周期等于自身长度),则 j1=j2 时它们产生的结果集合完全不交。因为若有交集,较长的那个子串就会有周期 ∣j2−j1∣,与本原性矛盾。
DP 公式
设 dp[i] 为 T[1..i] 能产生的不同结果数。由以上分析:
dp[i]=j=0T[j+1..i] 是本原的∑i−1dp[j]
其中 dp[0]=1。每一项互不重叠,无需容斥。
如何判断本原性
子串 T[j+1..i] 是本原的当且仅当它的最小周期等于其长度。利用 KMP 的失配函数(failure function):长度为 L 的字符串的最小周期为 L−fail[L],因此本原等价于 fail[L]=0。
对于固定的终点 i,我们考虑反串 Ri=T[i],T[i−1],…,T[1],则子串 T[j+1..i] 对应 Ri 的前缀 Ri[1..i−j]。对 Ri 跑一次 KMP 即可得到所有前缀长度 L 处的 fail[L],从而判断哪些 j 对应本原段。
复杂度
对每个 i,KMP 和求和各 O(i),总复杂度 O(n2)。n≤8000,完全可行。
这是个好问题,我来讲清楚。
证明"非本原段可被更短段覆盖"时,我们说:
切法 1(最后一段 [j1+1,i])和切法 2(最后一段 [j2+1,i])给出同一个结果 R。
你的疑问是:“这要求两个切法的前缀部分产生相同的值,但 dp 本身在数不同的结果,这不矛盾吗?”
关键:R1′ 和 R2′ 不是同一个东西。
T=[1,1,2,1],i=4,j1=0,j2=3,d=3。
切法 1:最后一段 [1,4],前缀为空。
R=rev(T[1..4])=[1,2,1,1]
切法 2:最后一段 [4,4],前缀是 T[1..3] 的某个结果。
要得到同一个 R=[1,2,1,1]:
整个推理没有循环:
回顾算法核心
i=1: R1=[1]
| L | R1[1..L] | fail | 本原? | 对应子串 |
|---|---|---|---|---|
| 1 | [1] | 0 | 是 | T[1..1]=[1] |
dp1=dp0=1
i=2: R2=[1,1]
| L | R2[1..L] | fail | 本原? | 对应子串 |
|---|---|---|---|---|
| 1 | [1] | 0 | 是 | T[2..2]=[1] |
| 2 | [1,1] | 1 | 否 | T[1..2]=[1,1],周期 1 |
| L=2 时 [1,1] 有周期 1(两个字符相同),非本原,跳过。 |
dp2=dp1=1
含义:T[1..2]=[1,1] 无论怎么切(切或不切),翻转后都是 [1,1],只有 1 种结果。
i=3: R3=[2,1,1]
| L | R3[1..L] | fail | 本原? | 对应子串 |
|---|---|---|---|---|
| 1 | [2] | 0 | 是 | T[3..3]=[2] |
| 2 | [2,1] | 0 | 是 | T[2..3]=[1,2] |
| 3 | [2,1,1] | 0 | 是 | T[1..3]=[1,1,2] |
| 三段都是本原的! |
dp3=dp2+dp1+dp0=1+1+1=3
含义:T[1..3]=[1,1,2] 的三种不同结果是:
dp4=dp3+dp2=3+1=4
验证:4 种不同的 S 分别是:
i=1: R1=[1],fail=[0]。dp1=1。
i=2: R2=[2,1],fail=[0,0]。两段都本原。
dp2=dp1+dp0=1+1=2
(T[1..2]=[1,2]:切或不切给出 [1,2] 和 [2,1],2 种结果)
i=3: R3=[3,2,1],fail=[0,0,0]。三段都本原(所有字符互不相同)。
dp3=dp2+dp1+dp0=2+1+1=4
i=4: R4=[1,3,2,1]
| L | R4[1..L] | fail | 本原? | 对应子串 |
|---|---|---|---|---|
| 1 | [1] | 0 | 是 | T[4..4]=[1] |
| 2 | [1,3] | 0 | 是 | T[3..4]=[3,1] |
| 3 | [1,3,2] | 0 | 是 | T[2..4]=[2,3,1] |
| 4 | [1,3,2,1] | 1 | 否 | T[1..4]=[1,2,3,1],周期 3(T[1]=T[4]=1) |
| 只有 L=4 被排除(整段 T[1..4] 有周期 3)。 |
dp4=dp3+dp2+dp1=4+2+1=7
验证:7 种 S 的来源:
更快速地走一遍:
| i | Ri | fail 数组 | 本原的 L 值 | DP 转移 | dpi |
|---|---|---|---|---|---|
| 0 | — | — | — | — | 1 |
| 1 | [1] | [0] | 1 | dp0 | 1 |
| 2 | [3,1] | [0,0] | 1, 2 | dp1+dp0 | 2 |
| 3 | [2,3,1] | [0,0,0] | 1, 2, 3 | dp2+dp1+dp0 | 4 |
| 4 | [3,2,3,1] | [0,0,1,0] | 1, 2, 4 | dp3+dp2+dp0 | 7 |
| 5 | [3,3,2,3,1] | [0,1,0,1,0] | 1, 3, 5 | dp4+dp2+dp0 | 10 |
| 6 | [2,3,3,2,3,1] | [0,0,0,1,2,0] | 1, 2, 3, 6 | dp5+dp4+dp3+dp0 | 22 |
| 最后一行详细解释: | |||||
| R6=[2,3,3,2,3,1],KMP 失配函数: |
dp6=dp5+dp4+dp3+dp0=10+7+4+1=22
直觉总结
每一步的逻辑可以这样理解:
子串 T[j+1..i] 是本原的,就是说它不存在比自身长度更短的周期。
具体几个例子(都来自样例 1):
我们在计算 dp4 时,考虑最后一段 [2,4](L=3,j=1)。
用 [2,4] 做最后一段时:
用 [1,4] 做最后一段(整段翻转):
用 [3,4] 做最后一段(L=2,j=2):
对于最后一段 T[j+1..i](长度 L),考虑"更短一截"的最后一段 T[j+d+1..i](长度 L−d):
两者在位置 j+d+1 到 i 的重叠区域上:
T[i+j+1−p]=T[i+j+d+1−p]即T[q]=T[q+d]
这恰好就是周期 d 的定义!
所以:
这个问题有一个非常优雅的数学观察。
设在 T[l..r] 中,切点在 m。令:
A=rev(T[l..m]),B=rev(T[m+1..r])
那么:
A∘B=B∘A?
即:两个字符串是否在拼接运算下交换。
有一个经典结论:
两个非空字符串 A 和 B 满足 AB=BA,当且仅当存在一个字符串 Z,使得 A=Zp,B=Zq(p,q≥1)。
也就是说,A 和 B 必须都是同一个基本串的若干次重复。
例子 1:T[1..4]=[1,2,1,2],切点 m=2。
A=rev([1,2])=[2,1],B=rev([1,2])=[2,1]
A=B=[2,1],显然 AB=BA。(Z=[2,1],p=q=1)
验证:分段翻转 =[2,1,2,1],整体翻转 =rev([1,2,1,2])=[2,1,2,1]。相等 ✓
例子 2:T[2..4]=[1,2,1](样例 1 中),切点 m=3。
A=rev([1,2])=[2,1],B=rev([1])=[1]
AB=[2,1,1],BA=[1,2,1]。AB=BA。
A=[2,1] 和 B=[1] 不是同一个基本串的幂次(因为长度 2 和 1 的公共基本串只能长度 1,那就要求 A=[c,c],但 A=[2,1]),所以不交换 ✓
例子 3:T=[1,2,1,2,1,2],切点 m=2。
A=rev([1,2])=[2,1],B=rev([1,2,1,2])=[2,1,2,1]
Z=[2,1],A=Z1,B=Z2。所以 AB=BA。
验证:AB=[2,1,2,1,2,1],BA=[2,1,2,1,2,1]。相等 ✓
AB=BA 也可以翻译成周期语言:
设 a=∣A∣=m−l+1,b=∣B∣=r−m。则:
AB=BA⟺T[l..r] 有周期 gcd(a,b)
这是因为"两块能交换"等价于整个串由一个长度为 gcd(a,b) 的基本单元循环构成。
在代码中,判断"分段 rev = 整体 rev"最实用的方式是字符串哈希:
对 T 预处理多项式哈希(前缀哈希 + 逆序前缀哈希),就可以 O(1) 比较任意子串、翻转子串。具体地:
总结:分段翻转和整体翻转是否相同,本质上就是问两个翻转后的块能否交换顺序,即 AB=?BA。这当且仅当 A 和 B 是同一个基本串的幂次,也等价于整个子串有周期 gcd(∣A∣,∣B∣)。
P4391 [BalticOI 2009] Radio Transmission 无线传输
计算这个串的周期所使用的这个哈希做法。
首先呢,计数问题,还是要选择 dp 解决。
问题转化
由于翻转操作是对称的,将初始数组 S 划分为若干段并分别翻转得到目标数组 T,完全等价于将 T 按照完全相同的划分方式进行分别翻转,从而得到 S。因此,原问题可以转化为:对于给定的数组 T,将其划分为任意个连续子段并分别翻转,能够产生多少种不同的数组 S。
分析重复情况与核心结论
如果我们随意对 T 进行划分并计算结果,会遇到“不同的划分方式产生相同数组 S”的情况。
例如,设当前段为 [1, 2, 3, 1],如果将其作为一整段翻转,会得到 [1, 3, 2, 1];但如果我们将其划分为 [1]、[2, 3]、[1] 三段分别翻转,拼起来的结果依然是 [1, 3, 2, 1]。
发生这种等价替换的根本原因在于:当某一段序列存在一个“前缀等于后缀”(在字符串理论中被称为 Border)时,对其整体翻转的结果,必定等价于将其拆分为更小块分别翻转的结果。
可以证明,为了避免重复计数,我们只需要在所有的划分方案中,只统计那些所有子段都“无 Border”的划分。无 Border 的定义是:对于长度为 L 的子段,不存在一个长度介于 1 到 L−1 之间的前缀,与相同长度的后缀完全相等。
任意一个能够通过翻转生成的合法数组 S,都存在且唯一存在一种完全由“无 Border”子段构成的划分方案。因此,所求的合法数组 S 的数量,恰好就等于将数组 T 划分为若干个“无 Border”子段的合法方案数。
动态规划设计
基于上述结论,我们可以使用一维动态规划来解决。
定义 dp[i] 表示将数组 T 的前 i 个元素划分为若干个无 Border 子段的方案数。
初始化时 dp[0] = 1,其余为 0。
当我们要从状态 dp[i] 向后转移时,我们需要寻找所有合法的右端点 j。条件就是:从 i+1 到 j 构成的子段必须是无 Border 的。如果满足,则执行状态转移:
dp[j] = (dp[j] + dp[i]) % 998244353
借助 KMP 算法加速判断
如何快速判断区间是否存在 Border?这恰好是 KMP 算法中求解前缀函数 pi 数组的核心作用。
pi[k] 存储的是前缀子串中最长的“真前缀等于真后缀”的长度。如果 pi[k] == 0,就意味着当前这一段子串不存在任何 Border。
因此,我们可以这样做:
不过总体上来说,我们对 border 的概念不是很熟。
确实,这道题目的难点就在于,对于不同的划分,如果有相同的这个答案怎么办?这个时候就需要引入字符串算法。
Border 的定义
在序列(如字符串或数组)中,如果该序列的一个非平凡前缀(即长度大于 0 且小于序列总长度的前缀)和它的一个非平凡后缀完全相同,那么这个相同的前缀或后缀就被称为该序列的一个 Border。
举个具体的例子:考虑数组 [1, 2, 4, 5, 1, 2]。
这个数组长度为 6。取它的前缀 [1, 2],再取它的后缀 [1, 2],两者完全相同。因此,[1, 2] 就是这个数组的一个 Border。
Border 对计数产生重复的具体机制
题目要求我们计算能生成多少种不同的初始数组 S。如果我们在动态规划中不加任何限制,直接枚举所有可能的划分方式,就会把“产生相同最终数组的不同划分方式”重复计算。
我们继续以数组 [1, 2, 4, 5, 1, 2] 为例。假设它是目标数组 T 里的某一个连续子段。现在我们要对这一段进行划分并分别翻转。观察以下两种截然不同的划分方式:
划分方案一:作为一个整体翻转
我们将 [1, 2, 4, 5, 1, 2] 视为单独的一大段,整体进行一次翻转操作。
翻转后的结果是:[2, 1, 5, 4, 2, 1]。
划分方案二:沿着 Border 将其拆分后分别翻转
我们把这段数组拆分成三小段,分别是:前缀 Border、中间剩余的部分、后缀 Border,即拆分为 [1, 2]、[4, 5]、[1, 2]。
我们对这三个小段分别独立进行翻转:
第一段 [1, 2] 翻转得到 [2, 1];
第二段 [4, 5] 翻转得到 [5, 4];
第三段 [1, 2] 翻转得到 [2, 1]。
最后,把这三段翻转后的结果原样拼接起来,得到的结果依然是:[2, 1, 5, 4, 2, 1]。
可以看出,不同的划分方式(整体分 1 段对比拆分成 3 段)最终产生了一模一样的生成数组。如果不加干预,这两种划分方案都会被统计入总方案数中,对于同一个合法的数组 S,我们就进行了重复计数。
将现象抽象化
为什么只要存在 Border 就会发生这种情况?
我们可以把任意存在 Border 的序列抽象地表示为 B+M+B(其中 B 是 Border 代表的子段,M 是中间夹着的子段,+ 表示拼接操作)。
如果我们把它作为整体翻转,得到的是 (B+M+B)R。根据翻转操作的数学性质,把一个拼接序列整体翻转,等价于把原序列从后往前依次翻转拼接,即结果为 BR+MR+BR。
如果我们把它拆分成三段分别翻转并拼接,结果显然也是 BR+MR+BR。
这就推导出了一个核心结论:只要某一段序列内部存在 Border,那么把这段序列“整体翻转”所产生的效果,总是可以被“沿着 Border 切分成更小的块并分别翻转”的效果完全替代。
消除重复的最终策略
既然“存在 Border 的大段”总是可以被等价替换为“没有 Border 的小段组合”,那么为了做到不重不漏,我们需要为每一种生成的最终数组 S 寻找一个唯一代表它的划分方式。
我们的强制规定就是:在所有能产生同一个结果 S 的划分方案中,我们只统计那种“被拆分得最细”的方案。
什么样的方案是拆得最细的?就是划分出来的每一小段序列,都不存在任何 Border。
因为一旦某个子段没有 Border,它就绝对无法再被拆解为类似于 B+M+B 的形式,也就意味着它“作为一个整体翻转产生的结果”,绝对无法通过“拆分成更小的几段分别翻转”来伪造。
可以从数学上严谨证明,任意一个能够通过翻转操作生成的合法数组 S,都存在且仅存在一种完全由“无 Border 子段”构成的划分方式。因此,只要在动态规划转移时,严格限制划分出来的每一段都必须没有 Border,我们计算出的方案数就恰好等于最终生成的不同数组 S 的数量,从而达到了去重目的的不同数组 S 的数量。
6 \\n 1 3 2 3 3 2 进行详细的算法模拟。dp[i] 表示将 T 的前 i 个元素划分为若干个无 Border 子段的方案数。dp[0] = 1,其余 dp[1...6] = 0。当前 dp[0] = 1。我们需要考察从下标 1 开始的后缀 T[1...6]=[1,3,2,3,3,2]。
对此后缀运行 KMP 求 pi 数组(注:为方便描述,这里子串内索引用 1 开始,对应原数组 T 的前缀长度 k):
| 长度 k | 当前子串 | KMP pi[k] |
pi[k] == 0? |
DP 转移操作 |
|---|---|---|---|---|
| 1 | [1] |
0 | 是 | dp[1] += dp[0] → dp[1] = 1 |
| 2 | [1, 3] |
0 | 是 | dp[2] += dp[0] → dp[2] = 1 |
| 3 | [1, 3, 2] |
0 | 是 | dp[3] += dp[0] → dp[3] = 1 |
| 4 | [1, 3, 2, 3] |
0 | 是 | dp[4] += dp[0] → dp[4] = 1 |
| 5 | [1, 3, 2, 3, 3] |
0 | 是 | dp[5] += dp[0] → dp[5] = 1 |
| 6 | [1, 3, 2, 3, 3, 2] |
0 | 是 | dp[6] += dp[0] → dp[6] = 1 |
本轮结束状态:dp = [1, 1, 1, 1, 1, 1, 1] |
当前 dp[1] = 1。考察从下标 2 开始的后缀 T[2...6]=[3,2,3,3,2]。
| 长度 k | 当前子串 | KMP pi[k] |
pi[k] == 0? |
DP 转移操作 |
|---|---|---|---|---|
| 1 | [3] |
0 | 是 | dp[1+1] += dp[1] → dp[2] = 2 |
| 2 | [3, 2] |
0 | 是 | dp[1+2] += dp[1] → dp[3] = 2 |
| 3 | [3, 2, 3] |
1 (Border:[3]) |
否 | 无操作 |
| 4 | [3, 2, 3, 3] |
1 (Border:[3]) |
否 | 无操作 |
| 5 | [3, 2, 3, 3, 2] |
2 (Border:[3, 2]) |
否 | 无操作 |
本轮结束状态:dp = [1, 1, 2, 2, 1, 1, 1] |
当前 dp[2] = 2。考察从下标 3 开始的后缀 T[3...6]=[2,3,3,2]。
| 长度 k | 当前子串 | KMP pi[k] |
pi[k] == 0? |
DP 转移操作 |
|---|---|---|---|---|
| 1 | [2] |
0 | 是 | dp[2+1] += dp[2] → dp[3] = 4 |
| 2 | [2, 3] |
0 | 是 | dp[2+2] += dp[2] → dp[4] = 3 |
| 3 | [2, 3, 3] |
0 | 是 | dp[2+3] += dp[2] → dp[5] = 3 |
| 4 | [2, 3, 3, 2] |
1 (Border:[2]) |
否 | 无操作 |
本轮结束状态:dp = [1, 1, 2, 4, 3, 3, 1] |
当前 dp[3] = 4。考察从下标 4 开始的后缀 T[4...6]=[3,3,2]。
| 长度 k | 当前子串 | KMP pi[k] |
pi[k] == 0? |
DP 转移操作 |
|---|---|---|---|---|
| 1 | [3] |
0 | 是 | dp[3+1] += dp[3] → dp[4] = 7 |
| 2 | [3, 3] |
1 (Border:[3]) |
否 | 无操作 |
| 3 | [3, 3, 2] |
0 | 是 | dp[3+3] += dp[3] → dp[6] = 5 |
本轮结束状态:dp = [1, 1, 2, 4, 7, 3, 5] |
当前 dp[4] = 7。考察从下标 5 开始的后缀 T[5...6]=[3,2]。
| 长度 k | 当前子串 | KMP pi[k] |
pi[k] == 0? |
DP 转移操作 |
|---|---|---|---|---|
| 1 | [3] |
0 | 是 | dp[4+1] += dp[4] → dp[5] = 10 |
| 2 | [3, 2] |
0 | 是 | dp[4+2] += dp[4] → dp[6] = 12 |
本轮结束状态:dp = [1, 1, 2, 4, 7, 10, 12] |
当前 dp[5] = 10。考察从下标 6 开始的后缀 T[6...6]=[2]。
| 长度 k | 当前子串 | KMP pi[k] |
pi[k] == 0? |
DP 转移操作 |
|---|---|---|---|---|
| 1 | [2] |
0 | 是 | dp[5+1] += dp[5] → dp[6] = 22 |
最终状态:dp = [1, 1, 2, 4, 7, 10, 22] |
遍历结束后,我们将整个数组 T(长度为 6)划分为若干个无 Border 子段的总方案数,即为 dp[6]。
模拟得出的 dp[6] 的值为 22。
对比题目样例:
1 | 6 |
样例输出恰好为 22。算法模拟过程严丝合缝地吻合了正确答案。
众所周知,计数题目,必须要找到一种方法,一种特征,保证符合这一特征的划分方式/方案之间是独立,互不相同的。这道题目的特征如下:(注意,我们认为单一字符符合这个要求,毕竟递归需要有一终止条件嘛)

只要有前后缀相同情况,那么一定会有更加细粒度的切分方法,实现相同效果。
不过,如果只是需要无前后缀相同情况的话,那么直接判断一下前后缀第一个一不一样不就行了?事实上,我们要求,我们所选择的串,其所有子串(长度 ≥ 2)的前后缀都必须不同。
题目描述
有 n 篇博客,第 i 篇博客按顺序提到了 li 个用户,用数组 ai=[ai,1,ai,2,…,ai,li] 表示。
你需要将这 n 篇博客全部发布。维护一个序列 Q(初始为空)来记录最近提到的用户列表。你需要选择一个所有 n 篇博客的发布顺序进行发布,每次发布一篇博客 i 时,会对每一个 1≤j≤li 依次执行以下操作:
如果 ai,j 已经存在于 Q 中,则将 ai,j 移动到 Q 的开头。
否则,将 ai,j 插入到 Q 的开头。
求在发布所有 n 篇博客后,所能得到的字典序最小的序列 Q。
输入格式
第一行包含一个整数 t(1≤t≤1000),表示测试用例的数量。
每个测试用例第一行包含一个整数 n(1≤n≤3000),表示博客数量。
接下来 n 行,每行首先包含一个整数 li(1≤li≤3000),表示第 i 篇博客提到的用户数量,随后跟着 li 个整数 ai,1,ai,2,…,ai,li(1≤ai,j≤106),表示提到的用户列表。
保证所有测试用例中 n 的总和不超过 3000,∑li 的总和不超过 3000。
输出格式
对于每个测试用例,输出一行包含若干个整数,表示能够得到的字典序最小的 Q。
样例数据
1 | Input |
样例解释
在第一个测试用例中,可以按如下顺序发布博客:
发布第一篇博客,Q 变为 [6,4,3,2,1]。
发布第三篇博客,Q 变为 [3,2,9,1,6,4]。
发布第二篇博客,Q 变为 [1,5,2,3,9,6,4]。
这里存在其他的发布方式,例如:
发布第三篇博客,Q 变为 [3,2,9,1]。
发布第一篇博客,Q 变为 [6,4,3,2,1,9]。
发布第二篇博客,Q 变为 [1,5,2,6,4,3,9]。
可以看到 [1,5,2,3,9,6,4] 的字典序比其他结果更小。如果我们不把第二篇博客放在最后发布,序列的第一个元素将不会是 1。因此 [1,5,2,3,9,6,4] 就是能得到的字典序最小的数组 Q。
在第二个测试用例中,可以按如下顺序发布博客:
发布第一篇博客,Q 变为 [6,1]。
发布第二篇博客,Q 保持自身为 [6,1]。
在第三个测试用例中,只能发布唯一的一篇博客,Q 会变为 [1,6]。
这道题有一个比较干净的做法,不需要动态排序和复杂删除。核心在于把"逆序操作"的想法进一步转化成一个简单的贪心。
关键观察:如何理解最终的 Q
设发布顺序为 π1,π2,…,πn。最终的 Q 可以这样等价地构造:
按逆序发布顺序(即 πn,πn−1,…,π1)依次处理每篇博客,对每篇博客从右往左扫描,遇到还没出现过的用户就追加到 Q 末尾。
这是因为最后发布的博客决定了 Q 最前面的部分(最近被移到前面的用户),倒数第二篇决定了下一段,以此类推。
预处理:有效序列
对每篇博客 i,从右往左扫描,保留每个用户的首次出现,得到"有效序列" Ri。例如博客 [1, 2, 1, 3] 的有效序列为 [3, 1, 2]。
贪心选择
维护一个集合 used 记录已经放入 Q 的元素。每一步:
used 里的元素跳过,得到"剩余有效序列"。used。[6, 4, 3, 2, 1][1, 5, 2][3, 2, 9, 1][1, 5, 2] 字典序最小(以 1 开头),选它。Q=[1,5,2]。[6, 4, 3],博客 3 剩余 [3, 9]。选博客 3(3 < 6)。Q=[1,5,2,3,9]。[6, 4]。Q=[1,5,2,3,9,6,4]。与期望答案完全一致。哎,其实这个思路和我一模一样,这个思路其实和我一模一样,我说白了你不会更加有更加简单的做法,实际上你就是这个做法,绷不住了。
也看了各路大神的代码,确实是没有更加简单的做法了。
1 | while (!g.empty()) { |
AC
http://codeforces.com/contest/2205/submission/365031780
1 | /** |
题目描述
定义一个长度为 m 的数组 b 是“好的”,当且仅当不存在任何索引 i(1<i<m)满足 bi=max({bi−1,bi,bi+1})。
给定一个长度为 n 的排列 a。如果数组不是“好的”,你可以反复执行以下操作:
选择一个满足 ai=max({ai−1,ai,ai+1}) 的索引 i(1<i<n)。然后,你可以从数组中删除 ai−1 或 ai+1。删除后,数组的左右两部分会重新拼接在一起。
求出使初始排列 a 变为“好的”所需的最小操作次数。
输入格式
第一行包含一个整数 t(1≤t≤5⋅104),表示测试用例的数量。
对于每个测试用例:
第一行包含一个整数 n(3≤n≤5⋅105),表示排列的长度。
第二行包含 n 个互不相同的整数 a1,a2,…,an(1≤ai≤n),表示初始的排列。
保证所有测试用例中 n 的总和不超过 5⋅105。
输出格式
对于每个测试用例,输出一个整数,表示使数组变为“好的”所需的最小操作次数。
样例
输入
1 | 5 |
输出
1 | 0 |
样例解释
在第一组测试用例中,数组初始时已经是“好的”,因此不能执行任何操作,答案为 0。
在第二组测试用例中,可以进行如下操作:
选择索引 3,此时满足 a3=max({a2,a3,a4})(即 3=max({1,3,2}))。接着从数组中移除 a2(元素 1)。数组变为 [4,3,2,5]。
此时数组已经是“好的”,因此答案为 1。
在第三组测试用例中,可以进行如下操作:
选择索引 2。从数组中移除 a1。数组变为 [5,3,6,2,1]。
选择索引 3。从数组中移除 a2。数组变为 [5,6,2,1]。
选择索引 2。从数组中移除 a1。数组变为 [6,2,1]。
通过上述 3 次操作,数组变为“好的”。

官方解法是这个笛卡尔树,不过我不太会这个东西,我们来看看其他做法吧。
https://www.luogu.com.cn/article/h4sddubz
这个 luogu 题解抓住了最大值一定不会被删除这个特性。(并不是删除最大值不优秀,而是重新阅读题面,你会发现)
题解:CF2205D Simons and Beating Peaks
注意到,数组的最大值必须置于两端。否则,我们必须将其左侧或右侧的所有元素全部删除,然后得到一个新的子数组。每次操作都会导致区间的缩小,因而可以递归求解。
具体地,对于区间 [l,r],如果 ax=maxl≤i≤rai,那么我们有两种选择:
fl,r=min(r−x+fl,x−1,x−l+fx+1,r)
如果暴力求解区间最大值,那么平均复杂度为 O(nlogn),但若原数组单调,则会被卡成 O(n2)。
故考虑用 ST 表维护区间最大值,递归过程可优化至 O(n)。
时间复杂度为 O(∑nlogn),瓶颈在 ST 表的初始化。
1 | #include <bits/stdc++.h> |
笛卡尔树是一种二叉树,每一个节点由一个键值二元组 (k,w) 构成。要求 k 满足二叉搜索树(BST)的性质,而 w 满足堆的性质。如果笛卡尔树的 k,w 键值确定,且 k 互不相同,w 也互不相同,那么这棵笛卡尔树的结构是唯一的。如下图:

(图源自维基百科)
上面这棵笛卡尔树相当于把数组元素值当作键值 w,而把数组下标当作键值 k。可以发现,这棵树的键值 k 满足 BST 的性质,而键值 w 满足小根堆的性质。同时根据二叉搜索树的性质,可以发现这种特殊的笛卡尔树满足一棵子树内的下标是一个连续区间。
竞赛中使用笛卡尔树时,常用数组下标作为二元组的键值 k,数组下标 k 满足 BST 性质。
下文使用 k,w 时,默认 k 满足 BST 性质,w 满足堆的性质。
我们考虑将元素按 k 升序依次插入到当前的笛卡尔树中。
对于一棵笛卡尔树,定义「右链」为从根节点开始一直走右儿子,走到叶节点形成的链。则插入节点后,这个节点一定在右链上。因为是按照满足 BST 性质的 k 升序插入,那么这个新插入的节点必然在树的最右端。这个节点不可能是一个左儿子,也没有右儿子。
于是我们执行这样一个过程,从下往上比较右链节点与当前节点 u 的 w,如果找到了一个右链上的节点 x 满足 wx<wu,就把 u 接到 x 的右儿子上,而 x 原本的右子树就变成 u 的左子树。
图中红框部分就是我们始终维护的右链:

显然每个数最多进出右链一次(或者说每个点在右链中存在的是一段连续的时间)。这个过程可以用单调栈维护,栈中维护当前笛卡尔树的右链上的节点。一个点不在右链上了就把它弹掉。这样每个点最多进出一次,复杂度 O(n)。
实际上,Treap 是笛卡尔树的一种,只不过 Treap 中 w 的值完全随机。Treap 有线性的构建算法,如果提前将键值 k 排好序,是可以使用上述单调栈算法完成构建过程的,只不过很少会这么用。
1 | // stk 维护笛卡尔树中节点对应到序列中的下标 |
笛卡尔树(Cartesian Tree)是一种由序列构造出的二叉树,同时满足两个性质:
对于序列 a1,a2,…,an:
[4, 5, 3, 6, 2, 1] 为例:1 | 6(pos=4) |
[4, 5, 3] 的最大值是 5(位置 2),成为左子树根[2, 1] 的最大值是 2(位置 5),成为右子树根“cool"数组要求没有内部峰值(即没有任何内部元素同时大于左右邻居)。由于元素互不相同,一个没有内部峰值的数组只能是先递减、再递增的"V 形”:
ai1>ai2>⋯>aik<aik+1<⋯<aim
(递减或递增部分可以为空,即纯递增、纯递减也合法。)
如果序列中间出现了一个"先升后降"的拐点,那个拐点就是一个峰值,违反 cool 条件。
每次操作是"选一个峰 ai,删掉 ai−1 或 ai+1"。被删除的只能是峰的邻居,而不是峰本身。全局最大值 n 永远不可能是某个峰的邻居(因为没有比它更大的元素),所以 n 永远不会被删除。
既然 n 一定留在最终数组中,而最终数组是 V 形的,那么 n 作为最大值如果出现在内部,它一定是峰值——矛盾。所以 n 必须在最终数组的左端点或右端点。
假设 n 在位置 p:
f(v)=1+max(f(左子树),f(右子树))
其中 f(v) 是以 v 为根的子树中,能保留的最多元素数(即最长合法 V 形子序列的长度)。
答案=n−h
其中 h 是大根笛卡尔树的高度。
以 [4, 5, 3, 6, 2, 1] 为例,笛卡尔树如上图所示:
[6, 2, 1](6>2>1,纯递减,合法 V 形)。你可能会问:把根 v 接到子树的 V 形子序列前面/后面,结果还是 V 形吗?
是的。因为 v 是子树中的最大值,一定大于子序列的所有元素:
| 概念 | 对应关系 |
|---|---|
| 笛卡尔树的根 | 区间最大值,必须放在最终数组端点 |
| 选左/右子树 | 决定保留最大值的哪一侧 |
| 笛卡尔树高度 h | 最多能保留的元素数 |
| 答案 n−h | 最少删除次数 |
| 笛卡尔树精确刻画了"每一级的最大值都必须在端点"这一层层递归的约束,这就是它与本题的本质联系。 |
我们最终采用的呢,是递归式求解做法啊,也就是分治做法,配合使用的那个ST表。
这个是利用了最大值一定不会被删除的这个特性,而且还利用了一个特性,就是只要这个最大值它不在L或者不在R,一定要把L旁边的或者R旁边的全部给删掉。
1 | auto solve=[&](auto && self,ll l,ll r) -> ll { |
AC
https://codeforces.com/contest/2205/submission/365029208
1 | /** |
C++ 14下,因为必须传入模板参数,嗯,可以这样子写其模板类型。
1 | ST<ll,decltype(max_i)> st(N,vec_idx,max_i); |
嗯,我们可以看到,嗯,这里应该是减L,不应该是减1。
1 | auto solve=[&](auto && self,ll l,ll r) -> ll { |