2024 杂题记录

1. 线段树维护 gcd

查线段树势能提到的一个例题。线段树势能里面强调线段树维护区间 gcd 的时间复杂度 为 遍历数组的复杂度 + 总 gcd 的时间复杂度,即 O(n + log C),均摊到每一个操作上就是 O(1 + log C / n) = O(1),所以我们可以 O(nlogn) 解决线段树维护区间 gcd。

不过网上做法好像和我想象的不一样(?)考虑由 gcd(a,b)=gcd(b-a,a) 得到 gcd(a,b,c) = gcd(gcd(a,b-a),gcd(b,c-b)) = gcd(a,b-a,c-b)。所以 gcd(a[l]...a[r])=gcd(a[l],gcd(b[l+1]...b[r])),其中 b 是 a 的差分数组。维护 b,可以 logn 得到 a[l],logn 得到 gcd(b[l+1]...b[r]),区间修改转单点修改了。

题外,如果不带区间修改,可以 st 表维护,logn 回答。题解

2. 势能线段树

口胡一下这个东西,也是在某题解看到了一眼就学了。学的挺一头雾水的。
这个东西实际应用其实就是在某些操作下,区间内的元素会在某些形式上趋于一样。比如区间取 min/max、区间开根、区间删除 >= p 的数等等。博客

重点我们要考虑一下它的证明。套路大概是:我们定义一个势能,观察这个势能的 变化次数上限 和 它单次变化带来的复杂度 的关系,然后利用全局的势能总和 去分析时间复杂度。
势能分析复杂度的帮助还是很大的,比如 这 些 。

以区间开根这个比较显然的例子举例,我们维护一下区间最大值和最小值,每次区间修改的时候,在 L<=l,r<=R 的时候我们不退出,而是判断 minmax 是否趋同,若是,区间 tag 变化;反之,递归下去。复杂度均摊不大,可能单 log 或双 log。区间去 maxmin 是单 log 的。区间开根例题 | 区间开根势能线段树

复杂度分析有点凌乱,唯一的启发可能是,对一些数值方面的操作,我们可以维护一下上下界,保证“在 L<=l,r<=R 时暴力递归下去处理的复杂度不高”。简而言之遇事不决直接莽。

3. CF679E Bear and Bad Powers of 42

神必数据结构题。

发现 42 这个数比较诡异,发现 1e14 范围内 42 的整数次幂大概 13 个。考虑用线段树维护每个值具体它最近的 42 次幂的距离,那么区间加就转化为这个区间的区间减,如果出现了负数就需要暴力单点修改,在没有区间覆盖的情况下显然一个点最多 13 次,改完之后如果区间内出现了 0 就需要重新加。同理,这个“重新加”的次数显然也不会很多。

那么我们在不考虑区间覆盖的情况下,这个问题的复杂度是 O(nlog2nlog42v) 的。考虑加入区间覆盖之后我们怎样保持这个优秀复杂度。发现我们可以把连续的区间视为一个点,即,把 [l,r] 区间的所有信息全部堆到 r 上,l-1 作为新的一个区间右端点。这样我们每次覆盖最多加入 2 个节点,复杂度依旧是 O(nlog2nlog42v),很巧妙。

结合小粉兔的题解,我们考虑怎样使用势能线段树去理解证明这个复杂度。不妨定义势能为 每个值相同的连续段,比此连续段的值大的,在值域内的 42 的次幂的个数的总和。那么显然整颗线段树势能的总和是 O(nlog42v) 的。定义当前线段树的势能函数为 nlog42vlog2n,那么操作 2、3 的均摊复杂度就是 O(lognlog42v) 了。乘上的这个 logn 就是我们单次想要改变这个势能的时间复杂度代价。不知道自己说的清不清楚正不正确

在实现上,比起其他人的大篇幅,小粉兔非常机智地把连续区间的信息直接放到线段树的那个节点上了,而不是 r 上面,大大简化了代码,非常简洁可观。

注意实现起来细节真的挺多的,注意 push_down 的时候要当成一次区间修改来做,不然标记没法正常下传更新,区间赋值的时候注意减法 tag 要清零。Code.

4. P9061 Ynoi2002 Optimal Ordered Problem Solver

维护阶梯线,用线段树/平衡树维护这个轮廓线上的点,查询用容斥做,三维偏序优化为二维偏序,每次把新加入的点插入线段树/平衡树,用优先队列之类维护一下每个未删点的位置。

5. LOJ3661 集训队互测2021 蜘蛛爬树

对于 u,v,最终路径一定是 dis(u,t) + d \times a_t + dis(t,v)。分类讨论 t 在路径 (u,v) 的哪个地方。

例如在 u 的子树内,答案(相对路径 u,v 长度的)变化量就是 2dep_t + d\times a_t 的一次函数,用线段树维护 u 子树内这个一次函数的凸包,因为可以离线,所以线段树内直接线性合并。
另一种情况是 t 在 u,v 路径上靠近 u 的那部分,变化量就是 2dep_t - 2dep_l + a_t \times d,其中 l 是 u 和 t 的 lca。那么我们考虑把这样的 t 的一次函数信息都一步一步跳重链,挂在重链链顶端,查询的时候从 u 开始跳重链,每到一个重链链顶就查询两个:从该顶端向下的重链信息,以及从该顶端向上的信息。

这个 trick 很重要,很常见。

6. P9058 Ynoi2004 rpmtdq

点分治一下。可以证明对于每个重心,经过它的路径中,可能产生贡献的点对数量不多。简单地说,对于每个点 i,有用的点只有它前面和它距离最小的点 以及 它后面和它距离最小的点。所以最后我们的有效点对量级是 O(nlogn) 的。预处理出来,每次查询二维数点即可。

脑筋急转弯,如果求的是 max,根据直径的可分并性,对于两个点集,它们合并起来的直径长度是 各自直径端点相互之间长度取 max。如 P1 直径 (a,b),P2 直径 (c,d),P1+P2 直径为 max(ab,cd,ac,ad,bc,bd)。线段树维护即可。

二维数点

直径分并性

楼房重建(单点修改 查询前缀最大值个数 log^2)

7. 永恒悲剧

https://contest.xinyoudui.com/contest/93/problem/372

二元组的楼房重建。感觉没太懂。

8.uoj515 前进四

seg beats

吉司机线段树 线段树3 势能线段树

区间取 max,查询区间和。

维护 mn, num, mn2, sum, tag,表示最小值及其数量、次小值、和、是否合并了最小值和次小值。每次看 chckmax 的 c 与 min 的关系,若 c<min,跳过;若 min<c<min2,合并 min 和 min2;否则,递归下去处理。因为每次递归我们数的种类一定减少,所以复杂度 O(nlogn),也就是势能线段树。

无题号,给一个长度为 n 的链,有若干条连边,2e5,每次查询区间 [l,r] 的导出子图有多少桥/割边。

首先割边坑定是链上的边,如果一个非链边从 u->v,那么 u,v 之间的链边就都是割边了。根据这个性质,我们考虑对每条链边定义 t_i,表示当前如果询问 l <= t_i,那么它不是割边;若 l > t_i,那么它就是割边。我们离线对 r 从左往右扫,那么操作变成了这两个:线段树维护 t,区间对新边的左端点取 max + 对询问查询有多少 t_i <= l。
考虑怎样用 seg beats 做这个问题。重新考虑我们的过程,由上述,第一种情况(不考虑)和第三种(递归下去处理)都没有对数进行实质性改变,所以我们考虑在每一次进行第二种情况(合并 min 和min2)的时候用树状数组维护值域,min 处 - num,c 处 + num。复杂度的话,我们执行情况二的上限是 O(nlogn),单次树状数组修改复杂度是 log,总的就是 O(nlog^2n)。这样回答查询就变成树状数组的简单查询了。
概括:seg beats + BIT 的运用。

uoj515:
问题可以显然地转化为查询前缀 max 个数。考虑我们依次考虑前缀 1~i,每一次我们用线段树维护操作的时间轴,即一个节点代表一个操作在前缀 1~i 上的答案。每次从 i -> i+1 就是把 a(i+1) 在不同时间的值对应取 max,同上铺垫题,我们询问的答案就是最大值的变化次数,所以每次对应位取 max ,处理 seg beats 的情况二时,我们都把对应位的 ans + 1。查询就变成简单单点查了。总复杂度为 O(nlogn)。

9. P2414 NOI2011 阿狸的打字机

ACAM 的 fail 树的运用。
建出 fail 树,性质是每个节点的父亲都是它存在的最长的那个后缀,即每个节点的所有祖先都是它的后缀,且长度随深度单调递减。那么问题转化为把 y 的所有前缀在 fail 树上 val=1,其他 val=0,答案就是以 字符串 x 代表的那个节点在 fail 树上的子树和。这个可以用树状数组做。
问题在于怎么快速回答多个问题。考虑询问离线,把所有 y 相同的询问全部放到一起。然后从头模拟建 trie 树的过程,每结束一个字符串就回答它的问题,trie 指针在移动的过程中同时动态对树状数组更改,单次查询复杂度降为 log。很厉害的做法。Code.

10. CF1483F Exam

上面 阿狸的打字机 是它的铺垫。
考虑对于较长串 s_i 考虑能够满足条件的 s_j 的数量。由于 ACAM,我们转化为前缀的后缀去做。即,s_j 一定是 s_i 某前缀的最长后缀。显然如果 j 是合法的,那么满足 它在 s_i 中出现的次数 = 它在 s_i 中未被覆盖掉的次数,否则它任意一次的出现被覆盖掉,它都是不合法的。第一个问题显然是 阿狸的打字机,第二个问题,我们考虑对可能成为的所有子串 j 按右端点排序(可能成为即 它是 s_i 某前缀节点在 fail 树上最深的、代表完整子串的节点),然后根据左端点判断是否被前面的覆盖了(类似二维偏序)。总复杂度 O(|S|log|S|),|S| 代表总串长,Code.

11. P4298 CTSC2008 祭祀

对 DAG 求最大独立集,输出大小、方案以及每个点是否可能成为一个“独立点”。
根据 Dilworth 定理,求最大反链,转化为求其闭包的最小链覆盖,跑 floyd 求出闭包,一个点拆为入点和出点,跑二分图匹配。最后反链的大小就是 n-|MM|(Min Path Cover 1/2)。方案呢?跑一遍 bfs 求出 S 集合,对于原图点 i,若 u_i 在 Sx 中,v_i 在 Ty 中,那么它就是在最大独立集中的点。这些变量定义可以详见 高阶一 学习笔记。3
如何判断每个点是否可能在最大独立集中呢?考虑必选这个点,删掉所有 它可达/可达它 的点(必须不能选),剩下的再做一次匹配,若剩下的最大独立集大小等于 ans-1,那么这个点就可以是。

“对于原图点 i,若 u_i 在 Sx 中,v_i 在 Ty 中,那么它就在最大独立集中”这个证明可以见 高阶一 学习笔记 L3 的 Dilworth 定理的证明。或 小粉兔题解。

12. CF1946E Girl Permutation

树的拓扑序计数。
组合数的方法略。考虑确定一些大小关系,即若 i>j,连 i->j。最后形成一棵外向树。考虑最后合法的数的分配方案就是此树的拓扑序,为 n!iszi。证明考虑不合法的拓扑序是儿子在父亲前面,则对于所有节点而言,所有情况中只有 1szi 是合法的。所以总的合法数是 n!iszi

13. CF1948E Clique Partition

非常好构造,爱来自下界。
首先考虑弱化版问题,如果不存在上界 k,原问题的下界是什么。若序列长为 m,那么下界为 |1-m| + |P_1 - P_m| >= m。考虑构造取到这个下界。记 P_1 = v,则 P_m = v-1,进一步 P_2 = v+1, P_3 = v+2, P_m-1 = v-2, P_m-2 = v-3,其他同理。所以我们考虑把原序列划分为 n/k 个部分,每个部分这样构造即可。复杂度 O(tn),n 这么小因为 spj 复杂度高。Code by tyw.

14. CF1943C Tree Compass

还是构造。
这个第一眼肯定想到重心之类的。考虑找到直径,对着直径染,当直径染完了其他点肯定也都染完了。如果直径长度(点数)为奇数,抓住中心点,次数就是 len/2 + 1;若为偶数可能会复杂一点,需要手模一些样例,记中间那条边为 (x,y),若 len%4 == 0,发现 x 和 y 可以交替互染,次数为 len/2;反之 len%4 == 2,我们只用 x 染色,次数为 len/2 + 1。然后 O(n) 找直径即可。注意特判直径长为 2 或 4 的情况。n 很小依旧是因为 spj(所以这个一定程度上可以误导/提示?)。

15. szoi 诗

看起来是个串串。考场手搓 KMP 拿 20 分跑路。我竟然 2h 手搓出来 KMP 了,喜

正解 SAM + 主席树。
乱搞 根号分治。

根号分治之所以叫根号分治,大概是因为它按照根号大小来分治,复杂度通常带根号
原来哈希可以处理字符集大小 1e6 的串的。不过模数要使用 12 位模数,999662099292。小模数容易祭。实在用不了大模数,就双哈希吧。
小的预处理,大的暴力查。哈希维护即可。
感觉自己暴力的意识还是有点欠缺的,考场暴力的效率比较差。

16. szoi 相似 & P6795 [SNOI2020] 排列

精神 AC。改三天写了 15 k 发现被卡常遗憾离场。
弱化版 P6795 [SNOI2020] 排列。这个只需要求一个,原题要求 O(n) 个的答案。一个静态一个动态,难度确实不在同一层。

本质是一样的,就是维护方式不同。左右分组,左边贡献就是经典连续段计数,线段树即可(或析合树),右边发现数一定是按照值域连续放一起最优。证明的话大概可以用调整法,把不连续的调成连续的会不劣更优。

考虑统计跨左右的答案的贡献。针对左侧的每个数,计算以它为左端点、右端点在右侧的连续段个数。那么它的答案不为 0 有一个必要条件,即 a[i,m] 在 a[1,m] 的值域上是连续的(记左侧最右点是 m)。所以我们只用维护这些点,不妨叫它们为 good 点。good 点基本上都会有贡献,除了一些边界极端情况。假设有 k 个 good 点,good_1 是最左边的 good 点。我们把 good 点对应 a[good_i,m] 的值域拎出来从上到下排着放,good_1 的值域在最底层。我们会发现,good_i-1 的值域一定包含 good_i 的值域,i 任取成立。而且它们的值域,如果不是 max(good_i-1,m)=max(good_i,m) 就是 min(good_i-1,m)=min(good_i,m)。这是它们的性质。

我们考虑构造方案,逐渐填充不完整的值域,即从值域 a[good_k,m] 开始,如果中间有空位就优先在右侧把它的值域填完整,然后到 a[good_k-1,m]……那么针对每一个值域,它往左右扩张的时候就会产生贡献。不妨记它往左扩展为 L,往右扩展为 R。显然 L 和 R 都是有范围的。扩张的时候这个值域对应的 good 就会贡献++。但是上面提到了,相邻 good 的值域之间不是 max 相同就是 min 相同,意味着它们 L 和 R 其中一个的范围是相同的。这个时候就要特殊情况特殊讨论了。

不妨记值域 a[good_i-1,m] 相比 a[good_i,m] 多出来的“空隙”数量为 cnt_i。“空隙”是指如果我们把值域画出来,把不出现在 a[1,m] 的数的位置空出来,那么空隙就是空出来的连续块。

假设现在在计算所有 good 点值域往右扩张的贡献。不妨把所有 R 范围相同的 good 视作一个大块。在这个大块里面,我们按照相邻之间的空隙数量把它们在划分成小块:如果 cnt_i=0,那么 good_i 和 good_i-1 在同一个小块;反之,两个不在一个小块。不妨记 tr_i 表示如果我们在 good_i 这里往右扩张,能够获得的贡献是 tr_i x wr。其中 wr 表示这次扩张对每一个能贡献到的 good_i 的贡献值。如果这个 good_i 是某一个小块的块首(即这个块里面编号最小的那个位置),那么它的 tr_i=1;反之,tr_i=tr_i-1 + 1。为什么这样就自己画画去模拟一下吧。

不过还有一个特殊情况。当 good_i 和 good_i-1 的空隙数量为 1 时,我们发现可以通过巧妙的安排来使 tr_i-1 的值 + 1。具体地,针对 good_i,让它的扩张顺序变成 LR;针对 good_i-1 所在的整个小块,让它们的扩张顺序变成 RL。反正这种情况的时候,tr_i-1 是会 +1 的。

最后 wr 的系数就是对这个大块里面所有的 tr 取 max,ans += trmax x wr。
L 当然也是同理。有一个细节小问题,就是 LR 不会互相影响使贡献减小,导致取不到上界(即二者总贡献直接相加)吗?前面我们已经提到了值域的性质,就是不是 max 相同,就是 min 相同。也就是说,不管怎样,tl 和 tr 一定有一个是 1。这时候虽然先填左还是右确实会影响,但是影响前后被影响的一方的答案都是 1。所以综合下来就是不影响啦。

嗯。如果静态(即弱化版 P6795 [SNOI2020] 排列)的话就非常好做了,跨越的贡献计算是 O(n) 的,复杂度瓶颈是左侧连续段计数,O(nlogn)。
但如果是动态,那就有的你写的了。我的实现实在是太差了,导致常数巨大无比。动态的话你不仅仅需要维护 good 点集合的变化、good 点值域范围 maxmin 的变化(即大块的维护),还要去维护相邻值域之间空隙数量的变化(因为这个会引起小块分割状态的变化)。非常非常非常的麻烦。虽然理论复杂度仍然是 O(nlogn),但我的 15k 代码因为在计算跨越贡献时使用了 3 棵线段树而常数巨大。第一个变化(good 点集合)用一个栈即可维护,第二个要对 max 和 min 分别使用一棵线段树和栈去维护,第三个要另外开一个线段树,容易发现每次引起相邻空隙数量差值变化的地方的数量是 O(1) 的,所以小块之间的变动是可以暴力修改的,其余的就线段树标记即可。

我的代码只做到了维护前两个变化。第三个实在是写不动了。
但是 std 只有 6k 就做完了。相比之下超级优美了。我至今为止并不清楚 std 的做法到底是什么,因为 std 的题解写的一把抓,连最基本的 LR 顺序不同而引起贡献系数的不同 都没有提到,所以我只能非常遗憾地把它视为一篇错误、不完整的题解,或者说只是我的能力不足没有悟到作者真正的用意(?)
对于动态版的维护方式,完全是我自己想出来的。动态静态本质的做法,无疑是正确的,因为它经过了洛谷题解区三位大佬的讲解。难点在维护。尽管我已经非常努力了,但这个维护的方法依旧是很复杂很劣质,导致不开 O2 能把大数据跑 6 秒。而本题的时间限制是 1.5s。只能说,std 太优秀了。
虽然相比之下静态版非常非常简单(虽然它还要你输出方案,但我相信这个是不难的),但静态版已经是一道黑题了哦。所以这个动态版的难度真是黑上加黑,15k 也实现不了看来也是情有可原的了。
静候能者,期待动态维护的优美解法。
哦因为我懒得画图所以上面口胡的做法非常抽象,可读性非常差。但这个确实是要经过大量的试错和模拟才能得到的结论和做法,三天的实践还是难以用区区几百字描述出来。
Code.

跑回来 update 一下。经过学习 std,感触还是比较大的。空隙数量的变化其实并不用刻意去考虑,因为新加入的数一定在栈顶,所以下面的空隙数需要补的一定会补,无需理会的之后自会得到妥善处理。另外,我需要推翻前面的一个结论:相邻的值域只会有一段不同。这是错误的。确实会存在两端都比之前更大的情况。所以结论一定不能错,一旦中间结论错了后果还是比较致命的。
至于段的最值维护,std 非常聪明,因为它是一个类似栈的形式,当前的状态我们可以记录在栈顶,在新加入一个数的时候,我们不去动前面的记录,只更新这个新加进来的即可;在删除栈顶之后,新的栈顶就保留了新状态的答案,很巧妙,这样我们就不用费心费力重新再算一遍了。

17. P6010 [USACO20JAN] Falling Portals P

原来这道题这么简单。原来还有直线凸包这个东西。
回忆一下考场做法。验证了条条大路通罗马。我的想法其实没错,但是后续真做不来。点凸包做法 太强了。主要因为它是随着起点变化而变化的,所以很难维护,需要维护多个凸包,很难做啊。

然后有线凸包做法。相比起前一个做法我们把它视为定点,这一次我们把它视为动点,考虑它的运动轨迹。根据贪心,如果终点在它的下方,那么它一定会尽可能地往斜率更大的直线走,方式就是在直线交点处进行直线切换。显然这个往下跳的状态是确定的,进一步观察发现它其实是在一个凸包上“滑下去”的。此时这个凸包上直线唯一的限制就是 a_j < a_i。所以没有点凸包做法那么麻烦。动态维护这个凸包然后二分终点直线和这个凸包的第一个交点即可。比较好写,Code.

线凸包的维护方法就是栈 + 判断直线 a 和直线 b&c 的交点前后顺序即可。

inline bool chck(int x, int y, int k){
	return (a[x] - a[k]) * (y - k) < (a[y] - a[k]) * (x - k);
}

	while((tp and (op ^ (x > st[tp]))) or (tp > 1 and chck(st[tp - 1], st[tp], x))) tp -= 1;
	st[++tp] = x;

附:点凸包的 Andrew 算法复习。(不记得就现推吧。)把点按照 x 从小到大、y 从小到大的优先级顺序进行排序。然后栈 O(n) 顺序遍历,可以求出上/下凸壳。然后倒序再做一遍就可以得到完整的凸包了。栈的时候需要用向量叉乘进行判断,重复一下 a x b = a_x x b_y - a_y x b_x,a->b左转为正,右转为负。没办法,这个是要背的

18. szoi 雷同

NOI2023 合并书本的改编题,把题面第二段的“重量”和“磨损值”调过来。

看到合并联想到荷马史诗,二叉树。每次新建节点合并两棵树。贡献计算:重量就是重量 x 深度,磨损就是除最长链以外的所有链的 2^len 之和 - (n-1)。
也就是说对于任意形态的二叉树,我们都能算出来贡献和。
考虑构造最优的二叉树。DP!考虑到有重量和磨损值两个变量,贪心构造不可取,那就是 DP 了。(或许构造最优解还有其他做法?)
比较特别但合理的构造二叉树状态 f_i,j 表示已经确定了 i 个点的位置,现在这棵二叉树还有 j 个叶子没有确定到底是不是叶子。
这里运用了这棵二叉树的左偏性质。树一定是左偏的,因为这样可以最大程度抑制磨损值的扩张。

转移的时候涉及到对于一个二叉树的叶子,快速求以它结尾的长链的长度。对一棵二叉树,考虑只对它的叶子节点编号,从左到右 0~n。那么编号为 i 的叶子,以它结尾的长链长度 len,2^len=lowbit(i)。

转移 O(1),总复杂度 O(n^2)。

19. UNR#8 D1T1 最酷的排列

赛时不知道为什么把自己困住了。通常来说这种题目都是探究是否有一套“组合拳"然后再利用它去做。这道题也是。两个性质/原则,第一,小的数只能往前走,大的数只能往后走(此处大小是相对的);第二,当 1 上的数不够小的时候,我们总是希望把 0 上更优秀的数移动到这个位置上。结合这两个作为突破口即可。
赛时总是认为交换区间一定是包含整块的,然后一直在死犟块和块排序的影响,把操作给局限化了,自己为难自己。

20. UNR#9 D2T1 兵棋

往一个胡同死钻了 5 小时。以后还是要在读完题之后先大致把题目划分出来几个思考方向。否则最后钻进一个死胡同之后就很难想到其他方向了。
这道题,首先很容易想到块的思路,按照块的角度去思考。失败。
如果我们从单个士兵对答案的贡献的角度思考呢。它能否有贡献取决于它的前面是否一直出现和它一样的士兵。所以我们考虑一个前缀的末尾的变化。f_i,j 表示第 i 个位置在 0~k-1 轮中出现了 j 次 1。如果只是用状态 f_i,0/1/2 表示末尾没有出现 1、没有出现 0、都出现了 是不够的。
另一个角度就是发现每一次的操作其实等价于把序列中所有的 10 删掉,转化为括号匹配。然后不会了。

21. szoi 发光虫

一眼 2-SAT。外面套个二分。问题在于怎么优化建图。如果不考虑不同种类之间不连边,就是数轴上的区间连边,线段树优化建图。考虑种类怎么连。一个优秀的想法,把同种的点,在线段树上,对它所有的祖先点 O(logn) 个全部 rebuild,不指向这个点即可。总时复 O(nlog^2n)。

22. szoi Cir

分析一个点能看见的条件。代数表达出来,一个条件是 gcd(i,j)=1,莫反一波即可。因为有条件 1/i^2 + 1/j^2 < 1/r^2 所以 i<1/r j<1/r 所以 n 的范围可以和 1/r 取 min。
赛时被计算几何给吓到了,其实和计算几何没关系。大胆假设,小心求证。

23. szoi 隔膜

不难。计数题,用 dp 去算那个组合就行。

24. szoi 皮配

好题。有一定难度。
首先很显然就是优化边数,给中间加一列 20 个节点。这样就是一个简单图。可以跑 KM O(N^3)。
考虑根据一些性质进行优化。我们想找到一种方案,记 f_i 表示 2^i 这个点最优方案下能够得到多少流量。发现这个是可以贪心的。考虑何时我们会放弃本可以在 i 处匹配的两个点,而去在后面匹配。发现此时最多减少 2 * 2^i-1 而一定增加 2^i,所以肯定是不劣的。
考虑在前面流量数都确定的情况下,怎样去最大化当前这个点的流量数。考虑怎样快速求出一个贪心的、合法的完美匹配。完美匹配,我们考虑利用 Hall's 定理去快速求出。根据 Hall's 定理的拓展,我们枚举当前 i~19 构成的所有集合,然后考虑它们的邻域,则 flow_i <= |N(S)|-\sum flow_x (x \in S and x \ne i)。对所有的边界取一个 min 就是 flow_i 合法的最大值。这个邻域的维护就要用到高维前缀和了。

25. szoi New

暴力 O(n2m2) 爆标。
首先简化一下这个哈密顿路的判断条件。在一个矩形当中,一个点只能向四周移动,它只能往比它大的、最小的点移动。这是一定的。若当前情况下,\sum a[nxt_i]-a[i] = max-min,那么说明这个矩形是合法的。因为 nxt_i 受到矩形边界的影响,所以我们考虑枚举上下边界和左右边界,暴力统计。加上一些简单的剪枝和硬性优化即可。
比如说,rep(i,0,1)rep(j,0,1) 会比 for(int i:{0,1})for(int j:{0,1}) 要慢。

26. szoi 梦

非常厉害的树形 dp,刷新了我对树形 dp 的认知。
非常暴力的做法,枚举来自不同儿子的最大可能的贡献然后取 max。定义 f_i,j 表示 i 子树内距离 i 为 j 的点的 max a_x 的值。g_i,j 即为 b,同理。
考虑怎样优化这个树形 dp。空间上,通常来说我们都要开 n^2 的空间,但是我们发现每一个点的有效空间都是有限的,同时儿子的 f 和父亲的 f 只是一个整体平移的区别,所以我们考虑只开一个 O(n) 的数 val 来维护 f,真正的 f 是指针数组,对树进行长链剖分。时间上,长链剖分后,暴力合并只发生在链顶,每次暴力合并复杂度 O(k),所以总复杂度是 O(nk) 的,可以过。

27. UNR#8 D1T2 里外一致

谔谔,不提组合数自己为难自己 QAQ。
列一维 DP 方程,如果不提组合数想硬算,加上莫队,喜提 60 分。
如果提了组合数,发现剩下 (2^{c_i-1}-2) = 2(2^{c_i-2}-1),也就是说每一次 f_i 从 f_i-1 转移过来都会 x2,又发现我们是对 2^64 取模,所以 i>64 的情况根本不用维护!!!非常巧妙。
然后发现一堆 (2^k1 - 1)(2^k2 - 1)... 相乘很难算,维护这个东西,考虑枚举选择了 -1 的括号数量,剩下要维护的就是一堆 2 的次幂相乘了。维护这些东西,再搞一搞组合数就做完了。

补充:
莫队。指针 LR 初始时 L=1,R=0,每次移动指针的时候遵循“先加后减,先L再R”的原则。排序遵循“先L再R原则,R根据奇偶性比大小”原则。

对 2^64 取模。unsigned long long / unsigned int 都有自然溢出功能(一定要是 unsigned)。用 unsigned long long 维护,自然溢出等价于对 2^64 取模了。但没这么简单。组合数部分需要取逆元,phi(2 ^ 64)=2 ^ 63,根据欧拉定理,a^phi(x) \equiv 1 \bmod x,所以有 a^{ 2 ^ 63} \equiv 1 \bmod 2 ^ 64,前提条件是 gcd(a,2 ^ 64)=0。所以在求逆元时,我们需要把它包含的 2 的因子先提出来然后再计算。

void init() {
    fac[0] = ifac[0] = 1; 
    for(int i = 1, t; i < N; i ++) {
        mp2[i] = mp2[i - 1] + __builtin_ctz(i);
        fac[i] = fac[i - 1] * (i >> __builtin_ctz(i));
    }
    ifac[N - 1] = qmi(fac[N - 1], (1ull << 63) - 1);
    for(int i = N - 2, t; i; i --) {
        ifac[i] = ifac[i + 1] * (i + 1 >> __builtin_ctz(i + 1));
    }
}

28. CF156C Cipher

发现多测组数很多,而且对字符串多测总长没有限制。做的时候被这个多测限制住了,而且对问题本质的归纳并不够清晰准确,没有换个角度重新审视问题。
一开始的思路是归纳操作的本质,发现可以任选两个位置,一个 +1,一个 -1,发现操作在和上面有互补性(这种互补性还可以体现在矩阵的行列问题上面,即操作之后对象的从总体角度上来看并没有发生本质问题)。其实这里就可以推导出问题的本质:总和是不变的。如果我们把字母视作数字 1 到 26,两个字符串相似等价于序列长度相同,且序列和相等。这样就可以用 dp 解决。
这里还有一个启示。在发现组数很多,方法在复杂度上严重受限的时候,可以考虑是否是预处理然后降低得到答案的复杂度。即尽可能使方法不受到输入变化的影响,尽可能独立。

29. CF156D Clues

原来是 prufer 序列的衍生结论题。

第一眼思考这个问题的本质。发现等价于给一张完全图,点有点权,边权等于端点之积,生成树权值等于边权之积,求所有生成树权值之和。发现这个可以缩为等式 \prod w_i^d_i 其中 d_i 是点的度数。所以答案和度数的分配方案有关。在度数确定的情况下,可以算出生成树的权值。但是度数序列和树的形态并不是一一对应的。
prufer 序列和树的形态一一对应。且一个节点在 prufer 序列中出现次数 = 它的度数 - 1。prufer 序列巧妙地把度数和树的形态联系起来了。考虑对于一个确定的度数序列,满足它的树有多少。等价于构造一个 prufer 序列,其中第 i 号节点要出现 d_i - 1,一次类推。组合数。分子分母消一下,最后方案数即为 (k2)!(d11)!(d21)!(dk1)!。答案的式子即为:
image
发现如果提 -1 的部分出来,剩下的就是多元二项式定理的形式。最后的答案即为:nk2si,其中 s_i 是第 i 个连通块的大小。

注意:如果模数不定(输入模数),且存在特判输出(常数)答案的情况,一定要记得让常数对输入模数取模!!!

30. CF101C Vectors

好新奇的数学题。
官方题解用了复数来形式化表达,其实可以感性理解为 A 旋转了 xx 度的形式。发现所有加上的 C 都可以归纳到某些统一形式,-C,-iC,C,+iC,进一步归纳到 aC+biC,其中 a.b 属于整数集。然后 A 对最后总和的贡献其实只是 A \times i^k。所以枚举 A 的四种旋转状态,即得到等式 aC+ibC=B-A_x。化简得到 a b 关于 C 的解,判断一下是不是整数即可。
感觉关键是把 A 和 C 的贡献给弄明白,发现这两个其实可以割裂,并不是那样交缠复杂的。

31. ARC182A Chmax Rush!

感觉自己做的时候被降智了。其实这种每个操作两种选择,问最后选择方案数的,一眼就是 2 的次幂类型,但赛时不造咋回事,降智了。考虑什么情况下一个位置可以有两种选择。显然一点是,我们考虑这个操作之前的操作中,那些 v 更大的数,它们的 p 值。如果这些更大的值统一地在 pi 的右侧或者左侧,那么一定存在一种合法方案。否则,无解。
进一步考虑,不妨所有更大的都在右侧。此时 i 操作选择左或右是否还有限制。此时 i 只能选择朝左。前提,那些比它大的都可以朝右。剩下那些没有特殊考虑的,一定是比 vi 小的,那这些操作不管左还是右都无所谓了,不影响。
所以一个傻子式的 O(n^2) 就搞定了。
我赛时在干什么啊???纯纯多想 5 分钟少写半小时了。害。第一次总是这样惨烈的。毕竟熟能生巧也不是毫无道理的。

32. CF1993D Med-imize

首先当然是一眼二分标数法。然后考虑怎么判断可行性。规则是删掉连续的、长为 K 倍的数的段,然后让剩下数的和 > 0。为何是 > 0,因为我们让 >= mid 的为 1,< mid 的为 -1。简单分讨可以发现只有这样不容易出问题。
当然考虑 dp。这个状态的设计需要十分小心。因为我们最后要让它成为一个近乎线性的 dp。这里我们是用 dp 来判断可行性。然后就有一个很巧妙的套路了,可行性转最优性。我们平时通常见到的都是最优性转可行性,例如背包。但这里我们用可行性转最优性。为什么呢?因为在这里让它恰好为 0 的难度要比 让它的和尽可能大 要难。恰好为 0 就需要你记录前面的删完之后的和,但尽可能大你只需要让它取 max 即可。
我们想要求出前 i 个数删了之后能得到的最大值。发现在 i%k 不为 0 时,删掉的数数量是确定的;为 0 时,则有两种可能。既然删掉的数的数量是确定的,所以没有其他多余的信息需要延续给后面,我们就可以设计一个一维 dp 了。
实现的话,由于余数为 0 时有两种可能,所以我们要多存一些。我用了 vector 实现,写起来比较麻烦。也可以学习题解第一篇 Register 的做法,用记忆化搜索转移。

吐槽:自己对复杂度的敏感度还是不够。写了个 map 还傻傻地以为是单 log 的。

在做的过程中,我也注意到了同余。Alex_Wei 就是利用同余做的。他的做法是二分 + dp,非标数法,算是中位数的另一类做法吧。我们发现一个数如果没有被删去,那么它一定是所有没有删掉的数中,排第 (i-1)%k+1 的数。如果被选中的数里面,大于等于 mid 的数有 c 个,那么 check=true 的充要条件就是 2xc-1>最后留下的数的个数。所以记 f_i 表示选了前 i 个未删的数之后,c 的最大值。没错,二者本质其实一样的。不过是利用了同余省掉了状态一维。

33. CF1991D Prime XOR Coloring

电信诈骗!!!我竟然在 2024 年遭遇了最惨的一次电信诈骗!!!
幸好回头是岸查看题解了。我的脑子出了问题,结论证了个假的。这就是不手玩的后果!!!
发现这个样例透露出一丝不寻常的气息。出题人把 n<7 的所有情况的答案都给你了。这很不对劲啊。
通过最后 n=6 的样例我们知道对于 n>5,色数最少是 4。可以证明色数构造可以取到下界。把对 4 同余的数涂上同一个颜色。此时发现模 4 同余的数,它们的异或和一定是 4 倍,一定不是质数。我就说为啥非得是异或呢。。
服了经典套路:得到下界->构造下界。我怎么就没想到呢???

警示:手玩很重要!不要脱离样例!证明结论的时候要记得推导实例!

34. CF1995D Cases

被高维前缀和整破防了。
首先经过一番艰难的思想斗争,可以转化题意:给定 n-k+2 个 01 串,长度 <=18,要求构造一个 1 最少的 01 串满足 它和任意一个给定 01 串的 AND 值不为 0。
乍一眼很套路啊,感觉可以秒。然后就 GG 了,想不出来。题解 1k 直接破防。我其实也想过从反面考虑这个角度,不过没想出来,总结一下原因大概是太急了没有细心想(?)
言归正传,考虑每个给定 01 串的补集,如果答案串是这个补串的子串(这里指包含的意思),就一定不行,反之一定行。判可行性、子串,虽然可以枚举子集,但这个复杂度未免太高。所以考虑使用高维前缀和优化。这种状态之间通过包含关系连接且答案具有传递性的就可以尝试用高维前缀和去做。
注意一个小细节:因为看划分的段的末尾,所以最后一位必须要选上。

高维前缀和!!!反面考虑!!!

35. CF101E Candies and Stones

做法太牛了。星星眼。
抽象化一下题意。给你一个 nxm 的网格,你要在线上走,往上或往右,每到一个格点都有分数,求分数和最大的路线方案。然后空间出奇得小。
如果普通无脑 dp 的话这个空间过不去,就算是 bitset 也过不去。不过这都是次要的,重要的是这道题的网格分治 dp 是出现在 noip2023 的方法,记得不错的话是 T3(谔谔,生蚝这么做的,我不记得了,我只知道自己的做法 doge)。
考虑给这个网格分治一下。对于当前层的 solve,已经钦定了当前需要解决的矩形范围:(sx,sy) ~ (ex,ey)。我们考虑找到这个过程中间位置的最佳转移点,以此来把这个大矩形进一步划分为左下角和右上角的两个子矩形,继续分治下去。注意我们之所以这么做,是因为这道题需要我们输出方案,我们必须确定具体的转移过程。
转移的话,由于空间限制太紧了,我们考虑按行转移。转移过程中存一下可能成为最佳转移点的状态即可。由于我们是想找到最佳的中间状态,所以考虑分别从起点、终点开始走,计算走到中间状态的代价。最后合并起来再取最优值。干讲可能讲不清楚,还是看代码比较好。注意在存转移过程和递归下去的参数的地方要注意一下小细节。

inline void slv(int sx, int sy, int ex, int ey, int cur, int sz){
	st[cur] = {sx, sy}, st[cur + sz] = {ex, ey};
	if(sz < 2) return; vector<pii> vec;
	rep(i, sy, ey) f[i] = g[i] = 0; int k = sz / 2;
	for(int i = sx, dx; i <= ex; ++i){ dx = i - sx;
		for(int j = sy, dy; j <= ey; ++j){
			dy = j - sy; if(dx + dy > k) break;
			if(dy) f[j] = max(f[j], f[j - 1]); f[j] += (x[i] + y[j]) % p;
			if(dx + dy == k) vec.push_back({i, j});
		}
	} k = sz - k;
	for(int i = ex, dx; i >= sx; --i){ dx = ex - i;
		for(int j = ey, dy; j >= sy; --j){
			dy = ey - j; if(dx + dy > k) break;
			if(dy) g[j] = max(g[j], g[j + 1]);
			g[j] += (x[i] + y[j]) % p;
		}
	} ll mx = -1, t = 0; pii ret;
	for(auto it : vec){
		int i = it.fi, j = it.se; t = f[j] + g[j] - (x[i] + y[j]) % p;
		if(t > mx) mx = t, ret = {i, j};
	} k = sz / 2;
	slv(sx, sy, ret.fi, ret.se, cur, k); slv(ret.fi, ret.se, ex, ey, cur + k, sz - k);
}

36. CF1994E Wooden Game

又是一个诈骗题,还放在 E。CF 真的够癫。属于想了不敢信的。
发现我们可以通过不断删叶子,得到任何我们想要的数。一个突破口是考虑 k=1 的情况,此时容易发现不管怎么拆,都不如整棵树优秀。以此为突破口,发现我们并不会把一棵树特意拆成几个部分,使得它们的或和更大。所以容易发现和树的形态毫无关系。
考虑贪心。倒序考虑每一位,如果它是 1 但是目前 ans 这一位为 0,那我们肯定保留这一位。但是一旦出现一个 1 位且 ans 并不需要这一位,那么 ans 就可以贪心地或上 (1<<i)-1。

37. CF1987E Wonderful Tree!

挺好的,比起那些诈骗的 E,这个好多了,至少它给你拐了个弯((
我竟然没做出来。可能是太困了,脑子不够灵光。其实是一步之遥。
很显然,如果你认真做一会你会发现你的 dp 方程是不能设得和具体的权值相关的。还有就是感觉和到叶距离相关,有点感觉是贪心。然后我的想法是 a_i-\sum a_v,表示“宽容度”,然后对于一个点感性上我们会挑选最近的、终点宽容度>0 的那一条路径。考虑抽象化、准确化这个过程。如果定义 b_i=a_i-\sum a_v,那么一次操作就是挑选子树内的另一个点 v,b_v>0,然后 b_u+1,b_v-1,代价是二者距离,距离拆成 dep_v-dep_u,就会发现这个东西可以贪心。
注意到叶子结点的 b_i=inf,然后这道题数据范围很良心,n^2logn 可过。

还有另解。比如说费用流。但也是依据这个 b 的变动来做的。
上面这个做法对每个点开 vector/set 存子树内 b 大于 0 的点,发现这个东西如果采用归并的方式排序,可以省去一个 log。个人认为如果采用重链剖分的方式 dsu on tree,可以降到双 log。

范鸽鸽的认可。
image

38. CF1993E Xor-Grid Problem

好题!好题!!大大滴好题!!!

一眼拆位了。然后从只考虑纵向操作的角度思考。然后找到了一些奇奇怪怪且没啥用的结论。
大方向搞错了QAQ。这种用全局异或和来替代某个数的操作,是个经典套路,手玩一下会发现来来去去就是那 n+1 个值,再没有其他了。所以如果我们只考虑纵向操作,等价于给这个矩阵末尾添加一列,分别是每一行的异或和。每一次操作等价于把前 m 列中某一列和第 m+1 列交换。行操作也是同理。
重点是!我们发现行列操作其实是相互独立的!!!不管怎么交换,它包含的数的集合是不变的。由于我们不关心过程只关心结果,所以只需要考虑这个问题:删掉一行一列,再给这些行和列重新排序,问最小的美丽值是多少。
考虑列与列之间的贡献怎么算。发现它们的集合是确定的,进一步,我们发现每一个数属于哪一个行集合也是确定的。所以!这一对列与列的贡献是确定的,可以预处理的,不受排列影响的(即如果它们相邻那么它们的贡献是确定的)。行也是同理。所以现在的贡献是轻易可算的了。
考虑如何计算总贡献最小的排列方式。我们显然可以枚举删掉的行和列然后用状压求出来总贡献最小的排列方式。考虑怎么优化这个复杂度,发现要省去一个 n 的枚举量。仔细思考后发现,如果已经确定删掉哪一行了,结合状压,列与列之间的贡献确定,就可以求出来每一种列排列方式的最小贡献。dp 之后再枚举删去哪一列。同理可以统计行与行之间的贡献。

所以不能一眼拆位?大方向应该优先列出来,不能吊死在一棵树上QAQ。以及,善于发现操作的本质,挖掘操作的性质,不能被杂乱复杂的表象迷惑了。

39. CF1991E Coloring Game

形式比较经典,在图论上染色的博弈论。颜色数少所以并不难。问题是这么简单的实现我竟然还写得错漏百出?总结一下原因大概是当时注意力都放在染色的逻辑上了,导致一些细节问题就没有精力去注意,才导致细节方面出错。所以实现前必须把代码的逻辑框架都推理出来,不可以想个大概觉得自己行了就去写,框架要先想好,实现的时候才能把注意力放在细节上面。
一开始读错题了推出来若织结论去看了一眼题解,然后看到了二分图。这道题大概 35% 不是自己的想法。不过也推出来了。首先一个显然的结论:任意两个同色点之间未染色的点数不可以为奇数,异色点恰好相反。否则对手可以一直给相同的两个颜色把你卡掉。即,同色点要间隔着放,异色点要相邻地放。
这样可以简单地胡出来一个构造性的方案了。想要进一步完善,考虑图的弱化版本-树。发现对于一个无环图,一定可以构造可行着色方案。考虑带环,再结合上面的结论,可以发现二分图可行,带奇环不行。

如果不是二分图,选择先手,一直给颜色 1 2 即可。若是二分图,先跑个 dfs 分出左右部,然后随意给左右部钦定一个颜色,直到一方染完、但另一方的颜色迟迟不出现时,再把第三种颜色归给未染完的部即可。

40. CF1997E Level Up

借此庆祝我国在 2024 年巴黎奥运会夺得 40 金,金牌数与漂亮国并列第一((

难得的、自己脑胡出来的、紫题(?)
不知道算不算紫,可能介于蓝紫之间。题解区各显神通,主流做法有:根号分治、二分加各种优化、整体二分、主席树上二分(heavy data structure)等等。做法太多了。为啥我家整体二分相对冷门呢?!?!?!
唉,什么时候才能熟练识别并运用根号分治呢,感觉每次都不太能看出来是根号分治,或者说没这个意识 QAQ。

其他做法不是很想去一一了解了。时间不太够。讲一下我的心路历程(?)
一眼感觉这个题有点典。这种是过程参数变化,还有一种是起始参数变化,这两种在推导过程类型的题的动态里面比较常见。不过印象里没啥万金油,还是具体情况具体分析吧,或者纯属于我的做题量还不够。
首先很显然的一点是,对于一个数,它能否被计入的这个状态,随着过程参数 x 的增长而单调的。具体地,对于每个数,它都有一个分界点 Ansi,在 x<Ansi 的时候它的答案都是 no,反之都是 yes。一旦发现它有这个单调性质就应该反应出来这个分界点的存在,而分界点通常的解决手段都是二分。这一点我在做的时候反射弧太长了,没有即刻想通这一点。
本题就是抓住这个单调性作为突破口。对每个数都找到分界点,很显然就是整体二分了。细节方面,发现如果在当前二分到的 mid,i 的答案是 no,j 的答案是 yes,那么就可以分为两部分继续二分下去。且,在 i 所在的那个 Solve 中枚举到的任何 x,j 在此情况下的答案都是 yes。这还是利用了单调性。
总复杂度单 log。这不香吗?为什么要搞个二分加上各种奇奇怪怪的优化呢?

大致思路就是这些了。不过想要实现上 ac,还有一个很重要的细节,就是上述 j 对 i 的贡献——即使当前 Solve 中我们没有切切实实加进来 j,但是我们要考虑到 j 对后续的影响。这个统计非常细节,很容易算错。首先,对于本轮二分到的 mid,只可能是 yes 的节点对 no 的节点的后续产生影响。比起存和前者之间的虚拟贡献数,不如直接存从 1 到这个点的虚拟贡献数。在能不受到前者的影响时,尽可能不和前者勾连,特别是这种前者还在不停动态变化的。

大概就这些,感觉我废话越来越多了。

41. CF101D Castle

这种连续性的树上问题(比如说一直保持连通块的状态,或者这样行走),因为带有连续性,所以通常可以一个子树一个子树地考虑。通俗地讲,就是这种情况下能够定义状态“从 x 进入子树完成行走/扩张再从 x 出来”。比如说树上的某种路径规划问题。
先套路把分母去掉。容易发现这个总时间是可以累计的,很好算。问题转化为一个点的直系儿子如何定顺序使得该排列对应的总时间最小。
求最小代价的排列,我想到了之前矩形异或 CF1993E,也是求最小代价排列,方法是状压。但这里显然不能状压。
再有就是利用这个代价式的特征去求,一般情况下就是利用性质进行贪心。本题也不例外,但它难点在于,这个性质并不是全局性的。换句话说,如果从全局性角度考虑可能无法用贪心求解。这里用到的贪心是“先钦定,再调整”,即我们从好入手的、相邻顺序之间的性质进行考虑。通过对比调换相邻两个数带来的总代价差异,我们可以发现 i 排在 j 前面更优当且仅当 w_i<w_j。所以对于任意的排列,如果存在这样的“逆序对”,调换一定更优。所以最佳的排列就是按照 w_i 递增的排列。

做题的时候要有意识按照“先钦定后调整”的思路,也即增量角度,考虑变化的增量,如果是 + 就改变,反之则不变。

42. CF1995E Let Me Teach You a Lesson (Easy & Hard Version)

不知道为啥感觉这个标题挺耳目一新的(
好题啊!套路好题啊!套路很新颖,属于那种没做过就不会,做过也可能不会的。
以前从来没见过这种套路(额或许是我的矩乘使用频率很低QAQ)

偶数情况直接薄纱即可。奇数情况则有点灼手,但是我们发现这个交换它始终是存在与两两之间,也就是对于一对数,要么换要么不换,不会出现这个数跑到其他地方的情况。再者我们试图将复杂的问题简单化,交换过于复杂我们就重新调换顺序。重新编号,把原来各种连线交错的交换局面整理简化为一个环,对于相邻的两个位置,要么它们俩的和对答案有贡献,要么它们俩就可以交换。

然后我们注意到题目问的事“极差”。极差这个也很典,通常可以在二分的题碰到。主流两种做法:一是枚举最小值,让最大值最小;二是双指针,这种就依赖于单调性了。其实两种方法在本题都适用,属于换汤不换药,不过出于好理解我们就以第二种为例。

考虑对于当前枚举的值域 [L,R],判断是否存在一种交换方案使得每一对数的和都在这个值域范围内,问题属于可行性问题。发现对于每一对数,不妨记编号小的为 p,大的为 q,那么它有 4 种状态,p 换不换 * q 换不换。所以,L,R 的取值其实只有 4n 种。考虑用类似 dp 的方式进行判定。dp 的主要限制是这一对 p 的选择要和上一对 q 的选择相同。注意我们需要记录第一对的选择,因为它是个环。所以记 f_i,s1,s2 表示考虑到第 i 对,第一对的选择是 s1,当前对的选择是 s2,是否存在合法方案。转移的时候枚举前一个的方案,判断是否满足上一个的结尾和这一个的开头选择相同即可。

但这样的 dp 复杂度过高。观察一下有哪些状态是我们可以省掉的。对于 s1,其实我们只关心第一对的 p 的选择;对于 s2,其实我们只关心第 i 对的 q 的选择。嗯,这是不是可以合并?新状态即为 dp_i,s。转移的时候枚举上一个的 p 是否交换即可。复杂度还是不行,考虑进行优化。矩乘优化。这种类似连环锁扣的转移可以用矩乘优化。用线段树维护,支持动态修改。
在求极差的时候用第一种方法就是 (min,max) 矩乘;第二种方法就是 (or,and) 矩乘。
矩乘优化感觉用在这两类地方:转移方式和当前状态无关(快速幂) / 转移不与前面多个不同状态有关(硬转,但支持动态修改)。才疏学浅,对矩乘的运用见识还不够多。

43. CF1990E2 Catch the Mole(Hard & Easy Version)

很不错的构造。
很容易想到两个 case:链情况,考虑二分;菊花图情况,考虑随意指一个点,若是 no,老鼠自己会跑出来。还能够发现一些性质:如果查询某个子树,答案是 no,那么这个子树整棵都无效,可以删去不影响。
发现上述两个做法哪个都不能少,考虑怎么结合起来。再结合上上面的性质,发现如果答案是 no 我们就减小了答案的规模;如果我们能够确定老鼠在哪一条从根出发的链上,那么就可以直接通过二分找到老鼠的位置;我们可以通过一直选某个叶子让老鼠一直往上走。
所以我们试图找到老鼠所在的链。没什么好办法,只能选择排除,直到找到老鼠所在的链。假如我们找到了老鼠所在的链,我们就可以用 15 次以内把它移到根。160-15=145,我们要在 145 次以内找到老鼠所在的链。但我们只能询问子树内是否有老鼠,怎么确定它所在的链?想到小老鼠可以自己走出来。我们在找到子树之后,可以让小老鼠自己走到子树根节点,然后我们就知道它所在的链了。显然这个子树不能太深了,否则小老鼠走出来所用次数过多;这个子树也不能太浅了,否则我们无法枚举找到。
除了 log 的次数规模,不要忘了折中时可以取 sqrt。不妨记子树根延伸到叶子的最深深度为 p_x,我们考虑枚举所有 p_x=\sqrt n 的子树。此时子树内至少有 \sqrt n 个节点,我们的枚举所用次数不会超过 \sqrt n。同时在找到子树之后我们把小老鼠“赶出来”所用次数也是 \sqrt n,再加上最后的二分,总用次数是 2\sqrt n + log n,恰好在 160 左右。

很巧妙的构造。通过这道题构造根号次数的方案,其实和很多根号分治类型的根号算法有异曲同工之妙。根号可以很好地平衡两种方法或者两个方面的复杂度。
话说,在思考这道题的时候,我其实被困在了找到子树却找不到小老鼠具体在何处的难点。以及自己确实没有想到把两个方法结合起来的方式。但是大方向我是正确的,并且上述用到的三个结论我都自己推出来了。可是思考的时候没有整合自己得到的信息,没有理清思路,找到难点,换个角度思路攻破难点。反倒是被难点一困再困。

题外,在思考此题时我发现选点很灵活,一时不太好下手。感觉大致有两种:一是找到关键点或有特殊性质的点;二就是枚举。题目给出的树并没有特别性质,从根或者叶子入手也不好做,所以我就想到了重心。结合上面说到的性质,我们查询重心的子树,如果成功,就是子问题;否则就舍弃掉重心的子树。每次舍弃的规模大概都是 O(n/2),应该也是可以的哇?在官方题解下面的评论区里面有类似的做法,好像也是可行的……

44. 关键边

李老师自己出的小清新热身题……标题是我随便取的。讲课的时候没想出来 QAQ

题面:给一棵 n 个点的树,多询,每次给 k 条关键边和石子数量 s。要求把 s 个石子放入树上的节点,节点可以没有石子,也可以有多个石子,要求全部放完。定义一条关键边的“不平衡度”为,删掉该边之后,两个连通块各自的石子总数之差。请最小化所有关键边“不平衡度”的最大值。输出这个值。n<=2e5, \sum k<=2e5。

什么二分 + 虚树,不好不好。李老师有言,T1 就要用简单的做法,不要写什么虚树浪费自己的时间。
通常看到这种题我们都会从二分的角度考虑,然后再想怎么分配石子……但通过简单的手模,我们发现一个性质:如果一条关键边,他的祖先有关键边,后代也有关键边,即有三条关键边可以连成一条链,那么这条关键边就是无效的。因为它的不平衡度无论怎样都不会比左右两侧的更大。所以真正有效的关键边放在树上,是不会存在 >2 长度的链的。考虑一个菊花样式的怎么得到最优解。
手摸一下三条关键边的情况,它们有一个中心交点。此时会发现我们把 s 三等分分别放在三条关键边远离中心点的那个点是最优的。所以拓展到所有情况,我们把 s 进行 m 等分,然后放在有效关键边远离中心点的端点上即可。答案很简单,判断关键边用树状数组维护即可。

李老师在讲这道题的时候,是通过枚举 k=1, k=2, k=3 的情况来讲解的。李老师有言 x2,一道题你手模很多组,或者不断打表,总是能发现规律的。学习学习!!!

45. sg-noi Pond

https://vjudge.net.cn/contest/649628#problem/A

很巧妙的 dp。
这个就是之前做的 CF101D Castle 时说的带有连通性的问题。发现青蛙走的路径一定是包含 K 的、连续的一段区间。
就可以很容易得到一个 O(nk) 的区间 dp,f_{l,r,0/1} 表示走完了 [l,r],最后停留在左/右端点的最小贡献。转移 O(1)。
考虑优化一下,尝试做到一维。这里有一个很巧妙的、优化 dp 的切入口。二维变一维就表示我们要舍弃一些存留的信息,但是我们答案的统计是依赖于 是否遍历过、上一个结束位置的。
所以考虑更改答案记录的方式。让 f 记录的贡献变成 已经遍历的点的贡献 + 未遍历的点的“当前贡献”。什么是“未遍历节点的‘当前贡献’”?假如我们现在在 [l,r],结束点在 l。那么对于 >r 的节点,它们未遍历,它们的当前贡献是当前时间 + l 到达它的距离。也即是说,记录的这个“当前贡献”表示如果现在从结束位置走到它的贡献。这样做的好处?发现右端点的位置不重要了。不管右端点在哪里,都不影响 l 右边的点对 f 的贡献。也即是说,现在的状态 f_l,无论 r 取多少,它都是这个取值。如果某个点它被遍历了,那它的贡献就不是“当前贡献”;反之,它的贡献就是“当前贡献”,还不是十足确定。
这样做可以让我们摆脱对 右端点 以及 是否遍历过 的依赖性。此时 f_l 代表的意义是:如果以遍历区间以 l 为左端点的最小贡献。当然这个贡献是包括那些“预支”的贡献的。考虑转移。此时上一个结束位置显然是 l,f_l -> f_j (j>k,l<k),要往右走。注意,此时青蛙从 l 出发,每往右走一步,l 左边未遍历的点 的“当前贡献”就要 +2。所以转移:fjfl+2(sjsl)(l1)。另一端同理。最后答案就是 f_1/f_n(显然 f_1=f_n)。
怎么转移。类似 Dijkstra,我们每次选择未遍历的、最小的 f_nw 来更新其他的、未遍历的点的 f 值。这样依旧是 O(n^2) 的。发现对于 i<j<k,f_i>f_j。记已遍历的点区间是 [l,r],新遍历的点只能是 l-1 或 r+1。考虑如何快速转移,发现上述转移式可以斜率优化。维护已遍历的 f 值构成直线的凸包。查询的话,发现我们查的新状态 j 都是单减/增的,所以单调栈弹队头就行了,线性的。

46. szoi ?

名字忘了,有原:二维平面,若干节点,请求出最大的 曼哈顿距离/欧几里得距离。

速出结论但是不会做。
放在三角形里面,这个值就是 cos+sin,等价于最大化 (cos+sin)^2,等价于最大化 2sincos,等价于最大化 sin2\theta。你看,这个三角变换我还是很牛的哈哈。
其中 theta 取 0 到 90 度。所以尽可能接近 45 度可以使这个值最大。然后不会做了
不妨过每个点作一条斜率为 1 的直线。如果有两个点共线就可以直接取最大值了。反之,考虑有无特别性质简化求解。发现如果答案点对出现在不相邻的两条直线上,那么它们各自和中间某条直线上的点的连线 和这条线 围成了一个三角形。在这个三角形中,不可能另外两条线的斜率都比第三条直线的斜率大/小。所以这条直线一定不是最优解。
剩下就很好做了。按照 x-y 和 x+y(斜率分别为±1)排一次序,然后直接对所有 calc(a_i,a_i+1) 取 max 即可。

47. szoi 命中注定之人

这个名字念起来很带劲啊
给一个序列,要求划分为不超过 k 段的连续段(可以等类理解为子串),要求所有段权值之和最小。一个段的权值定义为:这个大区间内,整体异或和等于 d 的子区间数量。
k<=20, n<=1e5, a_i<=1e6.

首先可以写出来一个无脑 dp 转移。f_i,k,发现第二维比较废,形式大概是 f_i=g_j+w_j,i,发现这个形式很熟悉啊,观察一下 w_j,i,试图证明它是蒙日矩阵。w_x,y + w_x+1,y+1 <= w_x+1,y + w_x,y+1,手画一下这个区间发现是显然满足的。
理论上来说,此题到此结束。但 531 的实力你是可以信任的,写 3.6k 的二分栈遗憾离场,不造在干啥.jpg。很适合去拆迁,因为不撞南墙不回头。
二分栈不可做啊。没办法快速查询 w_i,j。曾经有那么一瞬间我发现这个东西的确不可做,就是不可能,但是我没相信自己,然后现实让我相信了。翁老师一句话掐灭了所有希望。
分治吧。既然可以离线,短小精炼的分治为何不写?细节在于如何保证复杂度是单 log 的,以及如何快速计算贡献。因为贡献和查询区间的范围有关系,所以考虑在线计算贡献,即维护动态变化的增量,这个是好维护的。为了保证单层的复杂度是 O(n) 的,我们发现唯一确定的是 \sum r-l = n, \sum qr-ql = n,也即是说我们的移动指针只能分别在这两个区间内移动,不可以在类似 [l,qr] 这样的区间移动。所以我们试图在每一次 Solve(l,r,ql,qr) 前保证已经统计了 [ql,l-1] 范围对答案的贡献(显然有 ql<l)。而这个可以在指针范围合法的情况下维护。
所以分治要注意复杂度,指针移动范围要确保是 O(n) 的。
分治和二分栈的选择,除了是否离线,还有贡献是否可算。

48. CF1994F Stardew Valley

构造类题目。常见的生成树自底向上的构造问题。
欧拉回路构造,考虑使所有点的度数为偶。试图找到非必经边的去留方案,且每个点的度数奇偶性是确定的。直接做感觉没什么思绪。
退而求其次,考虑合法性怎么判。显然,如果奇偶性之和是奇数,即有奇数个度数为奇的点,那显然是不合法的。剩下情况就是度数为奇的点只有偶数个。尝试构造一种简单的方式,直接上图不好考虑,考虑它们的生成树,为了避免后效性,我们尝试自底向上考虑,如果这个点目前边奇偶性不合法,就断掉它和父亲的连边。这样做好处一个是无后效性,另一个是具有普遍性,即每个点(根除外)都一定有一条自己可控的边。
试图证明这个方式的可行性。按照上述方案,我们保证除了根以外的所有点都已经满足条件。试图证明:若按该方案进行分配,一定能找到一个可行解。考虑若根无法被分配。此时根度数的奇偶性被改变,则原本偶点会变成奇点,奇点会变成偶点,那么此时奇点的数量一定会变成奇数,则已分配的总度数之和是奇数,显然不合法。所以不会出现这种情况。
决定好非必经边去留之后,直接 dfs 即可找到可行欧拉回路。

其实也有一点“找到理论上界,构造满足上界”的味道。

49. CF1156E Special Segments of Permutation

线段树分治大假特假。启发式合并大好特好。
换个角度,确定中间 max 的位置,l,r 取值范围确定。显然有想法枚举 l 然后判断 r 的位置。看起来是 O(n^2) 的。
不急,虽然这个套路用过很多次,但还没有试过讨论一下这些 max 取值区间的性质。其实它可以看作笛卡尔树,很合理。笛卡尔树的形态是很容易被卡到 O(n) 的,但采用巧妙的优化,套路地,枚举小的那一方统计答案,那么此时遍历量最多的就是二叉树,深度 O(log),复杂度降为 O(nlogn)。
原来这个 max 取值区间看成笛卡尔树之后,等价于逆向的启发式合并过程,每次挑小的遍历总复杂度是单 log 的。

50. P4897 最小割树

50题祭!其实并不是写什么学习笔记,只是学习的时候想到了一些相关证明,记录一下。
试图构造某种机制,可以通过某种预处理来达到较为迅速的查询,且符合最小割的基本性质。考虑建出来一棵树,可以不保证形态问题,应为可以支持做 n 次最大流,O(n^3m)。每次随机选取当前集合的任意两点,在原图上以它们为源汇跑最大流求出它们的最小割,然后在树上用权为最小割的边连接这两个点,然后分别递归去处理刚刚求出的 S 和 T 集合。最后一定形成一个树结构,因为由集合分治得不可能有环。
保证无环,即,若 t 能到达的点 x,y 不可能在不经过 (x,y) 的情况下到达 x。则任意两点的最小割即为二点树上路径权值的最小值。试图证明它的正确性。
若 x,y 直接连通,必然满足。否则反证,若它们之间的最小割小于树上路径的最小值。记此时以 x,y 为源汇在原图上跑最大流之后和 x 相连的集合为 S,另者为 T。由于 x 和 y 在树上并不直接连接,且所有点在树上都是联通的,所以 S 集合中某个点 u 必然和 T 中的某个点 v 在树上直接连通。那么 mincut(u,v) 必然满足 >= mincut(x,y),因为此时图上的最小割 (S,T) 已经满足割开了 u,v。与假设矛盾。
所以跑 n 次最大流 Dinic 建出最小割树,然后每次查询直接树上路径最小值即可,可以用 bfs 单次 O(n) 查询。

51. CF343E Pumping Stations

喜,感觉自己对题目的把控越来越好了。
首先题面比较吓人。
简单说一下我的思路过程吧。首先考虑几个角度,一从最大流角度考虑,看最大流能否直接算出来;二回归最小割本质,即每条 s-t 路径上都至少删掉一条,可以转为高维前缀和做;三找到某种特定条件,转化“排列”问题。但三种角度分别考虑感觉都不太可做。在手模小样例的过程中,“图”让我想到了转化为最小/大生成树去做,最大生成树即可找到 u->v 所有路径中最小权的最大值,但最小割是要求和的,所以暂且也被 ban 掉了。考虑这个题的难点在哪里,一最小割的计算;二构造 max 排列。最小割的计算暂时没有思路,考虑如何构造 max 排列。即若已知任意 mincut(x,y) 如何构造收益 max 的排列。发现该问题是有后效性的。考虑贪心是否可行。虽然无法严谨证明,但通过调整法我们大概能够感性地发现它似乎是满足贪心的,但由于难点一未能解决所以暂且无法严谨证明贪心的正确性。不过直觉可贪。在此过程中,我也曾联想过有关无向图有哪些处理手法,例如桥、割点、点双、边双等等。但似乎都不太有用。

神奇地发现我的思路竟然几乎包含了正解的所有内容。上述标粗的地方都是和正解相关联的,我都想到了,但却并未想到把它们都结合起来,像一名侦探一样,把所有小点融合起来,调整先后顺序,如同细线串珠一般把它们串联起来。感觉自己太牛了,竟然把最小割树部分的“最小割本质、最小/大生成树”都想到了。

正解。考虑建出最小割树。然后手模发现性质:子树之间不可能交错遍历,一定是先遍历完一棵再下一棵。再画一下发现最后第一问答案(即所求式的值)记为最小割树的所有边权。求方案的话,可以像卡老师那样遍历一个树,也可以像 yzh 那样直接搞 priority_queue 每次贪心选最大的边。

补充。我的写法是并查集启发式合并,时空都是单 log。不用真的建出树,把树边按照边权从大到小排序,初始每个点的排序就是它本身。遍历每条边,合并的时候新排列就是两个排列直接拼接。

52. CF1983F array-value

感觉自己还是有很多细节没有想到或者没有想全。

吐血了,这一篇杂题记录搁置了两周差不多。悔不当初啊,如果好好写了这道题,模拟赛就不至于似得如此惨了。。。
两个方法,第一个比较传统。二分 k,然后对每个点找到最大的、和它异或和 <k 的下标数量。这个用 01 trie 就行。
第二个方法。重中之重!!!虽然它在这道题打不过第一种方法,但是它里面的结论真的很重要!!!著名结论 一些数两两异或的最小值=将它们从小到大排序之后相邻两个数的异或和的最小值
证明:大概口胡一下。这种全局最小值=排序后相邻最小值感觉还比较常见的,不仅仅是异或,也可以拓展到其他很多方面,比如说 bitset 是否包含就可以按照 1 的个数排序然后判相邻的是否包含。证明比较像 SA 中把后缀排序之后相邻 LCP 长度之类。
考虑 a<b<c,然后不妨 a 和 b 在从最高的第 i 位开始不同,具体 b_i=1,a_i=0;b 和 c 在第 j 位开始不同,具体 b_j=0,c_j=1。考虑 a xor c 的值,若 i 和 j 不同,记 k=max(i,j),那么在第 k 位某个和 b 相同,某个与 b 不同,所以 a xor c 的最高位的 1 就是在第 k 位。那显然不如 b 与 a/c 某一个在 min(i,j) 开始不同的优。若 i 和 j 相同呢?模拟赛时就是这个点没想通然后 GG 了。实际上,i 和 j 不可能相同!若相同,则表示 a 和 c 在第 i==j 位相同,这和 a_i=0,c_j=1 矛盾。所以对于 a<b<c,一定有 min(a xor b, b xor c) < a xor c。
然后这样就可以尺取了。

53. CF364D Ghd

感觉自己在乱搞这方面还是欠缺了些。
随机化。题目的突破口在于“集合大小大于 n/2”。也就是说,我们随机在序列中选一个数,都有 >1/2 的概率,选到最终答案的倍数。对于选中的数 a_i,我们拆出它的所有因数,然后统计这个序列有多少个数是它的整数倍,从大到小遍历这些数,如果有一个出现次数 >n/2,就是答案。
随机 10 次。因为多了就 tle 了。
出错的概率是 1/1024,其实还是蛮高的。

54. CF1599F Mars

数列哈希,太神奇了!!这篇题解写的非常非常不错,很清晰。
容易发现,对于确定区间和的区间,且知道它的公差,我们是可以推断出首项的值的。具体地,区间和为 S,公差为 d,有 2S=(2a+(n1)×d)×n,化简得 a=2Sn(n1)d2n,其中 a 为首项。然后每一项都可以推出具体的值了。但我们想快速查询是否满足条件,how?
想要找到一种不受乱序干扰的、确认等价的关系。哈希!且不受乱序干扰的哈希!直接求和似乎满足条件,但这样容易重,不如变成对每个数的 k 次幂求和。或许目前相乘看起来也满足,但是如何快速回答查询呢?问题等价于如何快速求出首项为 a,公差为 d,项数为 n 的等差数列各项 k 次幂的和呢?

image

根据 k 项式定理,我们拆开了。现在和 n 有关的部分可以简单地预处理出来,和 a、d 有关的减少到单次 O(k) 级别可以计算的了。预处理 + 回答的总复杂度为 O((n+Q)k)。k 取到 37。

55. szoi 染色

题意:现在给出一棵个节点的树,一共有条边,初始树上的每个结点颜色均为白色。
然后进行次操作,每次操作选择树上的两个节点,我们把树上 u 到 v 的简单路径上全部节点染成黑色(包含)。
对于每次操作结束后,请你求出当前树上最长相同颜色路径是多少?(操作不独立)

被染色创飞了。准确来说,被动态维护连通块直径创飞了。
动态过程类型的问题。考虑动态加入点,合并连通块,问题在于如何对两个连通块合并成的新连通块求直径。
考试的时候寄在这里了。唉,脑子是越来越退化了。
赛后突然想到,这个问题其实以前写过一个云剪贴板,问过金老师的……对于两个点集 S,T,新直径的两个端点一定来自于这两个点集原本各自的直径端点,共 4 个。所以每次并查集合并的时候枚举 2x2=4 种直径情况更新新连通块直径长度即可。
总复杂度单 log。

56. szoi 逆序数

题意:多次查询区间 [l,r] 内,i[l,r]j[l,r][ai=p][aj=q][i>j],其中 p < q。n 与查询次数级别都为 1e5。

考试一眼根号,然后不会做了/fn。
一直在想把区间长度根号,完全没有想到根据出现次数把数值根号。太局限了!!思维不够打开!!
如果两个数出现次数都 < sqrt(n) 那直接暴力扫一遍,双指针即可。
如果其中一个数出现次数 > sqrt(n),称作关键数,这类数不会超过 sqrt(n) 个。那么就可以统计这个关键数出现的询问的答案,暴力扫一遍即可。
总复杂度 O((n+q)sqrt(n))。

询问了一下 lty 的动态开点线段树做法。额属于理性很难证明复杂度可过,但很难卡(可能像我 noip2023T3?)。
动态开点线段树 + 线段树合并。对每个值开一个线段树,然后合并的时候 up 就可以维护区间答案了,如果是 0 就不建下去了。然后查询的时候重新合并一下答案即可。常数比较大,且比较容易被 Hack,不那么推荐,但乱搞搞也是可以水过的。
崇尚乱搞精神!复杂度啥的,均摊一下好了(

57. szoi 字符串问题

题意:给 k 和字符串 S,要求找到 S 最长的子串 s 满足 s 首位相接 k 段之后仍是 S 的一个子串。输出 |s|xk。|S|,k <= 1e6。

惊,SA O(|S|log^2) 跑过 1e6,1.5s。
yesss,是写了 SA 的做法中跑的最快的!!(跑不过二分 + 哈希/fn。)
很典的题了。我的第一直觉就是,对于最终 s 出现的下标 i,显然满足 qry_mn(rk_i, rk_i+len) >= len * (k-1)。后缀排序,倒序枚举 len,然后不断把满足 >= len*(k-1) 范围的后缀加入并查集。重点维护一个连通块内是否存在 sa 值之差恰好为 len 的下标对。下标对,考虑启发式合并,然后遍历较小的下标集,set 查询另一个中是否存在和它恰好差 len 的下标。但这样随着 len 的递减,先前维护的信息会失效。考虑两个集合 Sx 和 Sy,是在 Len 时合并的,那么对来自 Sx 的一个数,不妨找到在 Sy 中,和它 sa 值之差 <= Len 的最大值。这样的好处是当 len 减小的时候我们依旧可以判断之前已经合并的连通块中是否有 sa 值差为 len 的下标对了。由于我们找到的这个值一定 <= Len,如果 ==Len,直接推出;否则一定会在之后的某个 len 用到。

另一个做法就是运用很典的分段。汗,我确实没想到 QAQ。我的做法比起分段,确实有点莽了哈哈。
倒序枚举 len,然后枚举起始段的段首,显然答案串一定包含 k 个段首点。用 SA O(1) 即可向左右扩展。
另一个做法,常数更小,就是二分 + 哈希,二分向左向右拓展的长度,哈希维护。不是,这也太草率了吧!这个做法可以在时限开到 500ms 的情况下 ac。明明都是双 log,怎么常数差距这么大!

58. 毛虫树

题意:一条链,往上面挂若干点,保证挂上去的每个点到链的距离为 1。这样的树称作毛虫树。请给树上每个点标号 1~n 记为点权,树的边权记为端点权值之差的绝对值。要求最后树上的边权各不相同,即为 1~n-1。n<=1e4,构造。

发现直径长度为 3 和 4 的情况都能简单构造出来。直径长为 5 的情况我太飞舞了没想出来。递归构造。
既然是递归,我们要缩小问题规模。显然,当前就要确定边权为 n-1 的边,且随之确定的是 1 和 n 被标在哪两个点。然后问题被分解为子问题,递归下去。
根据题意,不难发现对于任意形态的毛虫树都是有解的。经过一番尝试也不难发现,如果我们想把点集分为两部分,就要把数集分为两部分,但这是不好做的。
所以考虑让链尾那个点为 1,和它相邻的就是 n,然后把这个点删掉,递归下去。
画画图即可发现这样是显然可以递归下去的。
实现也很简单。

59. Task Process

题意:有n个任务1~n。假设:同一个时间单位你只能处理一项任务;任务i只能在时刻 ri 后开始处理。 (r1,…rn给定)任务i需要 pi个时间单位才能完成。(p1,…,pn给定)

贪心题,不难。但做的时候脑子绕了一个弯(
考虑这样一个弱化版:现在有 m 个任务,每个任务用时不同,要求最小化 \sum t_i,即结束时刻。根据最小等待时间的结论,一定安排用时最小的先完成。
那本题就是这样一个动态版,用优先队列维护即可。

一开始以为是原长最短的优先完成。但写完发现寄了,如果它只剩 1 了,但此刻加进来一个原长比它原长短的,也依旧是优先完成它,而不是那个现在加进来的。
还是没有想清楚这个贪心的本质啊QAQ。

具体实现的话,要注意时刻范围不是到 200,而是 maxn * maxp + maxr 的范围。

60. P6033 合并果子 加强版

这个题太癫了。为了不放过单 log 真是无所不用其极了。
奇技淫巧!我超,奇技淫巧!!!
一眼,明明就是贪心啊,怎么做到线性呢。每次挑最小的两个出来,当然会想用优先队列。
超,奇技淫巧!!!先桶排一发然后放进普通队列,另开一个维护合并后的值。容易发现如果我们每一次都挑最小的两个值合并,合并出来、新的值一定是单调递增的。证明的话反证法即可。考虑运用这个特性。另开一个队列维护合并出来的值。每次要最小值的时候就比较两个队列的队头即可。

什么 STL 啊,特别是队列(双端),通常会搭配 while(pq.size()) 来食用(当然你不这样用更要注意),里面如果又取了一次队头,就一定要判断队列是否为空。不过当它是空的时候,弹出来的值应该是 0(可以自己再试试),你也可以运用这一点。无所谓,但一定要注意它是空的情况,否则会 RE/WA 的很惨。

更新:关于优先队列。在 while(pq.size()) 的循环中,如果出现中途 break 出去或者直接 return 了,如果要多次使用,就一定要清空!不然就调到函数里面定义(但这样也容易忘记把默认的大根堆调成小根堆,晕)。

另外,使用迭代器的时候,一定一定一定必须必须必须要注意特判!特判!特判!如果它是 begin() 会怎样,如果它是 end() 会怎样。必须!一定!特判!一旦用了迭代器就必须要非常小心。

以及最近碰到好多要求线性的题。大多是在原本带单 log 或双 log 的基础上加以优化,找到瓶颈,利用性质。此题的瓶颈在于排序,那就观察我们丢进去排序的数有没有什么特点。

61. P2168 荷马史诗

第 n 次做这个题了。求 WPL 和最大深度,优先队列贪心即可。

细节:关于补充节点数量。写的时候凭借记忆和手模猜了个 while((pq.size()-1)%(k-1)) 没想到猜对了。看了题解里的一个证明,每次合并,都会减少 k-1 个节点,我们目的是把 n 个节点减少为 1 个,而每次减少 k-1 个,所以能够“完美合并”的充要条件就是 (n-1)%(k-1)==0

62. pairs

在你谷传了这道题

经典结论:对于任意一个数 x,符合“y <= x and x + y = 2 ^ k (k > 0)”条件的 y 有且只有一个。证明不说了。
考虑按照数值从大到小贪心,因为大连向小,每个点连向比它小的点的边只有一条,所以这个图是不存在环的。就可以当做二分图匹配来做。
或者更简单,直接从大到小考虑,这个数如果还有剩说明它只剩下往前匹配一条路可以选了,此时匹配它一定不劣于不匹配它。因为如果不匹配,说明那个更小的 y 要留给其他 x' < x 来匹配,但如果 x' 本来还有机会和其他 y' < x 的 y' 来匹配,这样就会白白折掉一次机会。大概就是这么个调整法可以证明。

之所以写这个,是因为写的时候一直没有调出来,甚至还写了个“spj”看是不是自己的解不对。结果 cpp 错了,spj 也错了,差点就要摇金老师说这道题 std 有问题了QAQ。
在打开某个点的输出时发现,对于 x==y 的情况,不能够 t=min(a[x],a[y]), ans += t, a[x] -= t, a[y] -= t;,这是错误的。唉,还是有 corner case 没有注意到,不够细心。

63. CF632C The Smallest String Concatenation

结论:字符串 sort,若 ab<ba,则视为 a<b。然后从小到大排序即可。

不太会证啊。关键证明应该是“若 a<b, b<c, 则可以推出 a<c”。即这个重定义的小于是具有传递性的。如果分情况列举 ab 不互为前缀、bc 不互为前缀……很那分讨出来。
证明很巧妙。如果我们把字符视作数字,字符串视作数,那么 AB<BA 等价于 A×10b+B<B×10a+A,为了区分我就换成了大 A,小 a 表示位数。左右移项一下得到 A×(10b1)<B×(10a1),再除过去,得到 A10a1<B10b1,而这个是具有传递性的。

然后根据这个引理,用调整法的思路,就可以证明上面那个结论的正确性了。

64. P8660 区间移位

这道典题(之所以典是因为它的出题人QwQ),我好像已经听过两三遍了/yiw
做法属于读了之后感觉超级无敌显然,但做的时候又不那么容易。

首先肯定二分,然后限定了每个区间的移动范围,转化为判定性问题:是否能覆盖完整个数轴。说一下思路历程。首先直觉按照右端点排序。然后考虑记 f_i 表示用前 i 个区间能覆盖从 0 开始的多长的连续的一段。转移的时候就能让中间那些“没用的”去往右覆盖……然后比较复杂没分析出来。

感觉自己的思路从某一个小方向开始岔了……确实从 dp 的角度来看的话,确实有很多需要考虑的情况,不太好搞。但其实可以从“最优性”来考虑。对当前结束位置(即从 0 开始覆盖的一段),找到可以通过移动来覆盖它的区间中,右端点最小的区间来覆盖它,并把这个区间尽可能右移再覆盖。

考虑证明这个贪心。反例就是最优解在覆盖某个结束点 A 时没有取右端点最小的区间。不妨记贪心选择的区间是 D1,而最优解里面选择的区间是 D2。欲证明我们可以调整这个最优解使得用 D1 来覆盖 D2,并且不改变其最优性。
记 D1=[A,B], D2=[A,B'],有 B'>B。情况 1,D1 在移动之后无法覆盖 B',即它完全被 D2 包含了,那么对于之后的 B' 一定有一个区间来覆盖,并且在最优解中 D2 显然已覆盖了它。也即 D1 的移动范围被 D2 完全覆盖了。那此时我们切换让 D1 来覆盖 A 当然无伤大雅,并且还能为后面制造更多的机会。
情况 2,D1 在移动之后能覆盖 B'。运用归纳法,不妨假设这个最优解后面选择了 D1 来覆盖 B',且 D1 新的结束位置为 B''。此时我们依旧可以调换 D1 和 D2 的覆盖目标,仍然满足条件。
所以这样的贪心思路是合法的。

其实,在想到贪心之后,这个贪心手段和贪心证明都非常显然了。

trick:如果在二分的时候,对那个 mid 的范围有特殊的要求,比如本题,要求 mid 要么取整数,要么取 x.5。那么就可以把 r=rx2,然后把 mid=l+r>>1 得到的 mid,chck(mid/2.0) 这样丢进去。
另外一种也是乘 2,不过是把区间下标范围全部 x2,二分的时候正常传 mid。

在做贪心的过程中,以这道题为最终契机(?),我也感觉到了一些贪心和 dp 之间的区别。以 gym101291M Zigzag 为例,n^2 做法就是一个无脑的 dp,但贪心却能让复杂度降到线性。还有这道题。dp 感觉就是你通过找到一些关键的、具有一定代表性的“结点”状态,然后枚举可能性进行转移。但贪心就是直接把这些可能性给你优化成 1 种,既然只有 1 种可能性那当然直接做直接求即可。
dp 也由于它“一环扣一环”的形态,导致你必须要在“没有后效性”的前提下才能使用。但贪心的性质更多时候其实是全局性的。不过是 dp 能应付更加多样的可能性罢了。

65. SP186 POI1998 The lightest language

定位为类哈夫曼树的贪心问题。

不知道为什么,我写的代码总是 TLE,非得改成和题解的几乎一模一样才能过。
直觉就是哈夫曼树,但是在统计贡献时的计算方式不是 WPL,是 \sum lf_x \times w_x,其中 lf_x 表示这个子树内叶子数量。然后我手模了一堆发现父亲的儿子数量一定不少于儿子的儿子数量。想过用哈夫曼树的做法合并,但是发现如果某个 w_i 特别大,那肯定不会合并超过 i 个儿子。然后感觉是 dp,就否掉了这个优先队列的做法。
实际上还真是。我这个答案的表示方式好像把问题复杂化了。实际上贡献可以表示为到根路径上点权可能不为 1、叶子权值都为 1 的哈夫曼树问题。只不过每次合并的子树数量是不确定的。
我们考虑倒序来建这棵树。即,根(上面)是确定的,不断去拓展下面的叶子。因为贡献之和叶子有关,所以我们只用维护叶子的权值和它们的权值和。每一次,我们拿出来权值最小的叶子,从它拓展出 k 个新叶子放入集合,并把它自己删掉。因为目标叶子本身是等价的(即叶子不区分),所以这样贪心是可以的。
复杂度感觉很迷。题目开 5 秒还是卡常卡得跟什么一样。

66. CF59E Shortest Path

感觉自己脑子被 Dij 搞退化了。既然可以 bfs,为啥搞一堆最短路做法。

记录前驱,边权都为 1,可以 bfs 少一个 log,在时间方面,特判掉一些明显不存在不合法的 abc 情况。还有注意一下,在这个问题中,bfs 过程中,一个点是可能重复经过的,但边一定不会重复经过。用边是否已经过来判断是否加入新状态进队列。

新科技:tuple。今天下午在写拆点 Dij 的时候就在想要是有一个 triple 类比 pair 的就好了( C++11 开始支持 tuple,类型可以很多样,类比 pair 用法,定义的时候 tuple<int, bool, string, char> 数量不限。赋值的时候 make_tuple(a,b,c,d),读值的时候比较复杂,读第 0 位就是 x = get<0>(nw),其中 nw 是我们的 tuple。缺点:在最初赋值之后不能更改。

67. P3403 跳楼机

怎么说,感觉自己明明知道大致做法,但是却寄了很久。很多细节没有注意,再有感觉在实现上面碰到了一些瓶颈。

一开始觉得只是判断是否可能产生这个余数即可。写完之后发现并不是,还要知道它是什么时候开始存在的。但此时中途发现问题之后,并没有重新复盘一遍整个问题、整个解法的正确性,直接就改了 vis 的意义,改了松弛时的边权。但问题是,边权不都为 1 的情况下,是不可以用 bfs 的。而,在改了边权 vis 之后,vis 的取值范围就不再是 int 了,会超出去,不仅是数据范围,还有初始值的大小也要改变。此时 vis 的意义是,余数 i 要出现,得等到 vis[i] \times x + i 才开始有。但是!每一次 Dij 的时候,我们取出来的应该是 vis[i] \times x + i 最小的余数,而不是 vis[i] 最小的余数。

总是一股脑不想全就冲去写代码,出错了也只改一点点,不重新复盘,这是很危险的。而且很浪费时间。
Think twice, Code once !!

68. UVALIVE 4098 new island

UVA 是不会写的,这辈子都不会再写的。
挺好的贪心吧,既然不写代码就写写记录吧。

一开始被骗了。边的花费和路径长度是无关的,无关的!所以可以初步简化一下问题,只需要关心原本有边直接相邻的两点在新图中是否有长度 = 2 的路径即可。
突破口:边长都是 2 的次幂,且答案的贡献形式是求和。根据 2 的次幂在求和方面的特性,我们可以按照边权从大到小考虑每一条边,如果可以删则一定得删,否则就算把其他的全删掉贡献之和也不够。所以看到贡献和 2 的次幂有关,也要想到贪心。

然后就是判断是否可删。可删等价于原本有边直接相连的所有点对在删了之后都依旧有长度为 2 的路径相连。“长度为 2”,且点数是 200 级别,就可以直接维护了。

69. ARC061E すぬけ君の地下鉄旅行

和 CF59E 有很相似的地方,导致第一眼直接把 CF59E 莽上去了。然后发现 T 了,原来 CF59E 里 nm 是可过的,但这里过不了,就算是 01 bfs 也过不了。考虑稍微转化一下,减少一下边数。发现瓶颈在于我们每一次都记录上次是从哪条边转移过来的,在颜色数比较多的情况下很冗余。而边视作点复杂度量级也较高,所以考虑把同色边合并考虑。同色边边权为零,等价于这个连通块内怎么走代价都为 0,所以直接对每个颜色的每个连通块建虚点把点边的复杂度降到线性即可。

70. CF2005D Alter the GCD

gcd 你还有多少秘密是朕不知道的。太牛了,D 的通过人数稳定地比 E1 少一半多。

gcd 另一个重要性质:前缀/后缀 gcd,最多只会有 O(logV) 种取值。

所以对于一个确定的 L,会有很多下标不同但是 gcd 贡献相同的 R。然后就衍生出很多做法。比如对于确定的 L,可以不断二分找到下一个可行的 R。注意这种方法还要算上计算区间 gcd 的复杂度 O(logV),所以是过不去的。
对于区间个数之类的统计,还有分治。分治就可以完美利用这个性质进行统计。复杂度双 log 可过。
另一种更好写的做法,是在 status 里发现的,考虑直接去维护对于确定的 L,后面 gcd(suf[R+1],gcdr(L,R)) 的值的可能性。这其实和上面第一个做法本质相同,不过更巧妙地通过延用之前的可能性来简化每一次计算可能性的复杂度。

71. CF2005E2 Subtangle Game (Hard Version)

很显然,dp 把 l 那一维去掉。想法是游戏按照 l 的顺序进行,如何能省掉 l 这一维?

题外:做 E1 的时候没有用 a 序列值域 7 的性质。

性质:在博弈论里面应该比较常见和通用(?)对于序列 a,如果某一个数出现了多次,那么从它出现的第二次开始,后面全部可以砍掉,不影响最后问题的结果。考虑这个矩阵 b 里面,2 出现了多次,当前这一轮要选 2,我们欲证明不管这一轮是谁在玩,他一定选择子矩阵内没有其他 2 的 2。感觉证明比较显然。

既然他们一定会选择子矩阵内没有另外的 2 的位置,那么序列 a 里面一旦出现了重复的值就可以把后面全部砍掉了。现在矩阵内每个数只会在选择序列中出现一次。E1 中状态 f_i,j,k 表示在 (i,j) 的子矩阵,当前选第 k 项,是否有必胜策略。此时就可以优化为 f_0/1,i,j 表示在 (i,j) (n,m) 这个矩阵中,能够先手赢的最小的序列下标。0/1 是为了区分先后手的。这样就可以优化掉一层 l 了。

所以优化也不一定是说观察转移过程然后用数据结构之类的优化,也有可能是直接从状态入手进行优化。

72. SP4235 QUEEN - Wandering Queen

之前有一个放反光镜的,也是最短路。那个做法是对每个行和每个列分别另建一个虚点,像地下铁旅行一样,然后每个点连两条指向对应的两个虚点,边权为 1,最后答案即为 dist/2。

但这个不能这么做,会爆。考虑进一步优化点数。不妨直接把每个不同方向的缩成一个点。此时所有边的边权也都是 1。不过确实省了很多不必要的虚点,合并了很多等价的实点。

这道题空间超大,时间很毒瘤。要用前向星 + 手写队列卡常。

73. ural 1325 Dirt

如果用双参数 Dij 跑要带个 log。考虑利用这张图的特殊性把 log 优化掉。

注意到题目是先 cost 再 dis。考虑先用 01bfs 求出起点到任意一个点的最小花费。然后重新建边,若 cst_i <= cst_j 且 i,j 相邻,就连(i->j)然后跑 bfs 求出的最短路径即为最小花费对应的最短路径。

考虑证明正确性。证明分为两部分:1 走出来的最短路径一定是 cst 最小且长度最短的路径。2 原图对应的最小花费最短路径一定可以通过这样找到。
先证明 2。原图对应的答案路径一定满足 cst 是不减的,否则一定存在更短的路径。那么既然满足 cst 不减,它一定存在于这个新建图中,所以可以找到。
证明 1。主要是证明它的 cst 是最小的。数学归纳法。对于路径上相邻的两点 i,j 满足 cst_i <= cst_j。分类讨论,若 col_i == col_j,则 cst_i == cst_j,此时走到 j 的路径一定满足 cst 最小。反之,若 col_i != col_j,则 cst_i + 1 >= cst_j,结合 cst_i <= cst_j,可知有两种情况,一种 cst_i == cst_j,另一种 cst_i + 1 == cst_j。注意此时 i,j 满足相邻,而 cst_i/cst_j 表示从起点走到 i/j,路径上颜色最小切换次数。而起点颜色确定,切换次数确定,则终点颜色确定。而又有 col_i != col_j,所以不可能有 cst_i == cst_j。所以只能有 cst_i + 1 == cst_j。此时若存在 cstt_j 满足 cstt_j < cst_j,即当前找到的这条路并不是 cst 最小的,则有 cstt_j < cst_i + 1,只可能是 cstt_j == cst_i,矛盾。所以 cst_j 这样转移过来一定是最优的。

所以可以用两次 bfs,线性解决。此题这样做的可行性根本是因为对于相邻而 col_i != col_j 的两点,必有 cst_i != cst_j。这样可以帮助我们在 cst_j==cst_i 和 cst_j == cst_i + 1 两种情况种排除一种。但一般图并不能这样直接排除。所以这个方法只适用于特殊图。

74. P3385 【模板】负环

第一次写 Bellman-ford。算法流程就是松弛 n-1 次,如果还能继续松弛就是有负环。

细节:题目问是否存在从 1 出发能到达的负环。所以还要维护一个 vis 表示它是否能从 1 出发到达。

Bonus:是否存在负环(即不需要从 1 出发能到达 这个限制)。但此时也不代表我们就可以用上述不加 vis 的做法做。最好的做法是添加超级源点,能直接到达任意一个点。

update:关于 spfa。如果想用 spfa 判负环,注意一定是记录入队次数而非松弛次数,若入队次数达到了 n 次就说明存在负环。不过更加优秀高明的做法是记录最短路径包含的边数,若这个边数达到了 n 说明存在负环。在松弛的时候一起松弛这个,判断更加高效。

75. ABC209E Shiritori

SG 函数在带环图上的计算。

游戏大概是 UVG 的有向图版。走到不能走下去,走不动的输。在树上的话是很好解决的,直接推:后继都是先手必胜就是先手必败,后继存在先手必败就是先手必胜。

但是在有环的情况下,可能出现平手情况。就是两个人都选择在环上一直走下去。其实这个平手的情况不难推出来,平手情况一定发生在环上,且如果这个环的后继都是先手必胜,那么不管怎样,两个人都会默契地走向平手局面。

怎么推出来具体每个点是什么胜负情况呢?“冷处理”就好了。说白了,还是按照在树上的推断规则,按照拓扑排序的顺序,能推出先手必胜的就 col_x=1,先手必败的就 col_x=-1,暂时推不出来的就不管它就好了。最后 col_x==0 就是平手点。

queue<int> q;
rep(i, 1, tot) if(!e[0][i].size()) q.push(i), col[i] = -1;
while(q.size()){
	int x = q.front(); q.pop();
	for(int fa : e[1][x]) if(!col[fa]){
		if(col[x] < 0) col[fa] = 1, q.push(fa);
		if(col[x] > 0){
			cnt[fa] += 1;
			if(cnt[fa] == e[0][fa].size())
				col[fa] = -1, q.push(fa);
		}
	}
} 

76. POJ3169 Layout & POJ1201 Intervals

都是差分约束的模板题。

第一题就是常规的,求 d_n - d_1 的 max 值。此时我们按照正常利用三角形不等式转化不等式然后跑最短路即可。这里我用了 Bellman-ford。
注意,无解(即约束矛盾)当且仅当图中存在负环;答案无限当且仅当源点汇点不可达。

第二题就是变式,求 d_n - d_1 的 min 值。此时发现再按照上述做法是行不通的。考虑把原本 (u,v,w) 的边建反边,(v,u,-w),然后跑最长路,但是源点汇点不变。注意构图的时候要记得判断源点汇点是否一定联通。

77. P3275 [SCOI2011] 糖果

我认为这道题在一定程度上揭示了差分约束、拓扑排序、DAG 上 dp 的一系列联系。

一开始只是想用差分约束怎么解决。按照上面所说,我们可以引入一个点 d0,要求所有 d_i >= d_0,且 d_0=1。然后建反图,跑最长路即可。发现边权只有 0 和 1,很遗憾,01bfs 并不能跑最长路。所以还是只能跑 bfs/spfa 之类。Dij 可以跑最长路?违背了 Dij 贪心的底层逻辑啊。

题外。这里其实也提供了一种思考问题的切入点。在题目只有两种边权的情况下(也许可以尝试拓展到常数种边权),可以尝试分开两种权值单独考虑。

但拓宽一下思路,发现,在只考虑边权为 1 的权时,即,题目的要求简化为 “A 一定多余 B”,不就是昨天的车站分级吗?而车站分级拓扑排序的表象下,是在 DAG 上进行 dp 的本质。

但这道题和车站分级不同的是,它有另一种限制,即 A 小于等于 B,即二者可以相等。虽说 dp 的底层逻辑不变,但是这却让我们转移过程更加棘手。差分约束的本质也是 dp,不过是套上了最短路的框架来转移而已。

考虑解决掉环这个问题瓶颈,发现对于 0 边构成的 SCC,它们一定是取等的,这是确定的。然后缩点,此时再加入 1 边进行考虑,如果 1 边的加入构成了环,显然无解;如果指向同一个 SCC,也是无解。剩下的情况就是由 0 边和 1 边共同构成的 DAG 了。既然已经是 DAG,直接拓扑在上面转移即可。

78. CF282D Yet Another Number Game

很经典的博弈论的 dp 转移。转移逻辑老套路了,若后继全部必胜则必败;若后继存在必败则必胜。

直接 dp 是 O(V^4) 的。但很多时候向前转移和向后转移的复杂度是不一样的。考虑用每一种必败状态向后转移。同时试图证明必败状态的个数不会超过 O(V^2)。试图证明对于确定的 a,b,至多存在一种 c 使得 (a,b,c) 是必败。根据必败的定义,它的后继一定都是必胜,所以对于 c' < c,(a,b,c') 一定是必胜。考虑 c' > c 的情况,若它存在一个后继必败态 (a,b,c),所以它也一定是必胜态。综上所述,必败态不会超过 O(V^2)。考虑只针对必败态进行 O(V) 向后转移。总复杂度 O(V^3),可过。

涨见识了,原来必胜态和必败态的数量级也是一个切入口。比如在这个游戏中两者的量级就有很大的差异。到底还是根据后继全部必胜则必败的逻辑来大概框定这个数量级。

但这个游戏本身对我们还是很有启发性的,也是很经典的一个问题。

但首先我们回归一下 Nim 游戏。Nim 游戏当且仅当在 XOR a_i == 0 的情况下才先手必败,反之先手总是必胜。这个是如何证明的?
首先,Nim 游戏证明的一个引理,对于 n 堆石子,无论 n 取多大,且当前所有堆数量异或和不为 0,那么经过一次操作之后,一定可以让它变为 0。考虑当前异或和 res 的最高位,一定有一个 x,它的这一位是 1,那么我只要让 x' 在这一位不为 1,后面取和 res^x0 一样的即可。x0 是 x 去掉 res 最高位之前部分后的值。
第二个引理,如果当前异或和为 0,那么经过一次拿取之后,异或和一定不为 0。即 res ^ x ^ x0 == 0,且 res == 0,则有 x ^ x0 == 0,即 x == x0,也就是一点没拿,不符合规则。
有了这两个引理,就很好证明了。说明在初始异或和不为 0 时,先手总是能在不为 0 的局面操作,并操作成为 0 的局面给后手。直到最后后手无法继续操作,先手获胜。但在初始异或和为 0 时,局面刚好调转,此时先手必败。

回归到这个威佐夫博弈问题。原威佐夫博弈问题,n=2。考虑这个问题和 Nim 问题的不同之处在于,它多了一种操作,即给每一堆都同时拿走 x0。
我们只考虑这个操作带来的、相较于 Nim 游戏的变化。在 n=2 的情况下。很显然,此时异或和为 0 的局面经过一次操作后,异或和仍然可以为 0。也即,先手如果把不为 0 的局面操作为 0,反倒而会导致后手直接获胜。所以这个游戏就不是 Nim 游戏这么简单的了。
威佐夫博弈在 n=2 的情况下,假设石子的个数分别为 x, y( x < y ),那么先手必败当且仅当 5+12(yx)(yx)=x。对,前面那个是黄金分割率,1.618,写代码的时候直接写 1.618 即可。细节在实现的时候,一定要注意转型问题,多打几个括号。正确写法:(int)(1.618 * (b - a)) == a

但上述情况在 n=3 时又有所不同了。考虑在 n=3 时全局异或和为 0 的一种情况。

x y z
1 0 1
0 0 0
1 1 1
0 1 1

由 x ^ y ^ z ==0 可知,在任意一位,三者的情况一定属于上述 4 种的一种。此时再考虑如果我们同时从这三个数中都拿走 x。若 x 为奇数,三者最低位的 01 状态一定会同时取反。可显然,同时取反后,3 个数在最低位的 01 状态不符合上述任何一种情况,即异或和为 0 的情况不复存在。若 x 为偶数,那就找到 x 从低往高的第一个 1,一定也满足上述情况。
所以在 n=3 的情况下,异或和为 0 的局面经过一次操作后不可能依旧为 0。如此一来我们在 Nim 证明中的两条引理依旧适用,先手必败的条件也就依旧适用。

Bonus:在 n 为奇数的情况下,似乎都适用。

79. 括号

未知题号。题意:给定由( 和 )组成的字符串 s 和 t,长度均为 n(n <= 5000)。将 s 和 t 交错起来成为合法括号序列,问方案数。

想到时候局限在顺序转移的框架中了。这种问题有一个经典状态 f_i,j 表示把 s[1,i] 和 t[1,j] 交错的合法方案数。然后就在想维护什么当前前缀和的信息……
实际上根本没必要。既然是 f_i,j,就说明最后一个字符不是 s_i 就是 t_j。考虑最后一位如果是一个(,那么只要前面的是合法的,这个就是合法的。反之如果是 ),那么不合法的情况只出现在左括号数量 < 右括号数量的情况。本质就是我们一直保持合法(即左括号数时刻 >= 右括号数)即可,不用存其他信息。

以及,记录前缀和这种通常是用在有不定字符的情况下的。在字符都确定的情况下,合法情况是可以直接判断的。

80. AT_dp_j Sushi

期望 dp。但需要注意的是,在推转移方程之前,一定要走完完整的“三步走”。确定初始和最终状态、确定初始的 dp 值,最后再写出转移方程。
在这里,f_0,0,0=0,意义是如果当前没有任何寿司,游戏结束。目标状态是 f_a,b,c,意义是对于 (a,b,c) 局面期望拿取多少次。我们的转移是从 f_0,0,0 到 f_a,b,c。所以在写转移方程的时候就不能按照“下一步拿了什么转移到我这个状态”的思路去写,因为这样你其实是在从后往前转移的。所以正确的转移应该是“我这一步相比前一步,拿了哪一种”。还有就是拿空的情况,只需要考虑“我这一步相比前一步拿空了”,不需要考虑多步。

81. UVA10559 方块消除 Blocks

不做 UVA 是你的谎言。
不是,等等,一遍过 UVA 有点不习惯啊哥们。
记忆化搜索转移 原来这么香?!?!?!?!?!?!?!

区间合并到空,很典的区间 dp。上区间 dp,f_l,r……然后?容易想到我们要用区间首尾的合并情况去转移。枚举一下,一种显然是首和位在一起,但是中间的情况怎么做?中间的过程很重要,因为可能首尾的合并可以加入中间的某次合并,但是…万一冲突,要对比所有情况,又要如何记录?记录区间“最后一次”的合并?还是区间首尾所在的合并?看起来直接转移到下面这个状态,并不是一定程度上直接等价的“子问题”。

刚刚我们的思路是区间的首尾向中间“靠拢”进行合并。那如果我们换一种思路,让这些合并都发生在最左侧呢?具象化,就好比一只手在序列的右边去“挤压”这个序列,然后一直推到最左侧,就把它们全部合并起来。可是怎么转移?最后连贯的段可能散落在不同位置,如何才能记录它们长度的和,更进一步,如何找到最优决策?

这里我认为用到了一种极为先进(物理先进)极为少见的极为巧妙的方法,就是“预处理答案”。通俗地讲,我在当前这个状态转移的时候,就把后面转移可能会用到的、与我相关的状态先求出来,更像一种“预判”。比如说,我现在在状态 [l,r],但是我“预见”到在之后的转移中,我的尾巴后面可能会堆积一些“想要去前面和前面合并的块”,那我就对所有可能的情况求出来最后的价值。等到之后真的“预言成真”之后,直接调用我已经“预处理好的答案”即可。

具体如何操作呢?首先,这些“堆积起来的、想要去前面”的块,颜色一定是相同的。否则,假设我现在有两堆块,一种排在左边,颜色是 a,另一种排在它右边,颜色是 b。那我完全没有必要带着 a 一起转移啊,因为最后一定是 a 先于 b 去合并掉的(如果 b 先合并的话为什么要堆积在这里呢,此时就互不干扰了,没必要堆积),而这一坨我们可以下放了子问题去做。没错!这里是真的子问题,而且大家都是堆积起来等待去和前面同色块一起合并的,又有左边的一定早于右边的合并,所以递归解决子问题即可。

现在我们知道,堆积在尾巴处的,一定是颜色相同的一堆块。再节省一下没必要的状态,我们断言,状态 f_l,r,堆积在它后面的块的颜色一定和 col[r] 相同。为什么?因为我们没必要转移到 f_l,r',而 r' 和我们现在的颜色又不一样的状态,没有益处,也不会有转移状态上的变化,因为你也不可能在这里合并掉。所以这种冗余状态就可以直接去掉了。

现在我们的状态 f_l,r,k 表示要合并完区间 [l,r] 的数,以及接在 r 后面的、k 个颜色和 a_r 一样的方块的 最大可能代价。转移的话,讨论一下 a_r 和堆积的 k 个是现在合并掉还是再堆积到前面处理。转移方程就很显然了,不多废话了。

实现的话,记搜记搜记搜!!!记搜的好处是不用管那些状态的先后顺序,而且!有很多冗余的状态用不到就不会去算!!不管状态的先后,写起来超级方便!!

inline ll dfs(int l, int r, int k){
	if(~f[l][r][k]) return f[l][r][k];
	if(l == r) return f[l][r][k] = 1ll * (a[l] + k) * (a[l] + k);
	
	f[l][r][k] = dfs(l, r - 1, 0) + 1ll * (a[r] + k) * (a[r] + k);
	rep(p, l, r - 2) if(col[p] == col[r]){
		f[l][r][k] = max(f[l][r][k], dfs(l, p, a[r] + k) + dfs(p + 1, r - 1, 0));
	}
	return f[l][r][k];
}

今天废话有点多。

82. P1052 [NOIP2005 提高组] 过河

顺带再说一下做 过河 的时候发现的小 trick 吧。

过河 主要是多余的状态直接忽略,具体忽略多少要用到一些数论去判定。但是!更聪明的做法是,在知道过程中有很多冗余状态(一定可以走到,又没有石子,完全没有转移的必要)的情况下,不用管精准可以省多少,直接对着数据范围省就好,不用动脑QwQ。比如说,再去掉正常转移所需的 O(T) 复杂度,剩下的就是我们的状态可以有的量级,即 1e7,然后石子数又只有 100,大胆一点,在两个石子距离 <1e5 的情况下,我们都直接强硬转移((反之如果大于 1e5,就把它缩到 1e5。正确的是 > 90 多就可以缩了 QwQ。

另外还有一个相关的数论小知识。
对于互质的 p,q,方程 xp + yq = s 在 s 为整数是一定有解;在 s > (x - 1) * y - x 时一定有非负整数解。证明的话考虑同余最短路,也就是之前 墨墨的等式 里面的。

83. CF2013E Prefix GCD

gcd 你还有多少秘密是朕不知道的 x 2。

会想到尝试第一次放最小值进去。但是会发现可能在 x 后面接的数构造出的新 gcd 不够小。但实际上,这种情况并不会发生。简单手模一下就好了。假设存在另外两个数 a,b,(a,b) < min((a,x), (b,x)),欲证 abx 放置顺序是 x 在最前时取到最优。记 a = kg,x = mg,则有 k > m。x + (a,x) = (m + 1)g <= a。所以 x 放在最前最优。

简而言之,本质结论就是:存在 A < B 时,有 A + gcd(A,B) <= B。这道题显然可以先去重。不妨记前面已放置的 gcd 为 A,假设除了最后一位不同,最优解的 gcd 是 B(A < B),那么此时可以发现我们完全可以把 a 插入最优解且一定不会更劣。

然后,值域 1e5,放入最多不超过 10 次的数,全局 gcd 就变为 1 了(质因子)。

记得先把全局 a_i / gcd(a_1...a_n)。

84. CF107C Arrangement & CF1111E Tree & szoi 交换

带限制的排列计数问题。

交换是在一个链式的 DAG 上计数合法拓扑序。Tree 是计数分组方案数,转移式本质没有不同。Arrangement 是求字典序第 k 小的符合限制的排列数,限制形如 位置 a 上的数必须小于位置 b 上的数。

从易到难。交换根据图的特殊形态,链式,也即限制仅仅存在于相邻的两个位置。按照链的顺序进行 dp,考虑 f_i,j 表示在前 i 个数,第 i 个数相对大小是第 j 个。这样的好处和本质是,全局排列本身具有全局性,直接做需要记录已经使用了哪些数,但既然我们的限制仅存在于相邻的两个位置,就可以把它简化为子问题了,即只考虑当前子问题的相对顺序即可。做法符合 dp 子问题递归求解的思想。

Tree 提供了进阶的思路。原本点两两之间不是祖先后代关系的限制,比较显然可以转化到 dfn 上去做,确定了一个顺序,使得问题可以顺序求解,依旧可以进行子问题递归。f_i,j 表示前 i 个特殊点(排序后)分成 j 组的方案数。f_i,j = f_i-1,j-1 + (i - s_i) * f_i-1,j。转移思路,要么 i 重新成为一组,要么加入到前面已经分出来的某一组。限制被转化成,它的祖先必定在前面已经考虑过了,且它所有的特殊点祖先必定被分到了不同的 cnt 组。那么剩下 j-cnt 组就是它可以分去的组。

Arrangement 就是交换的 Bonus。且限制较 Tree 更加一般化,不具有什么特别性质。首先,我们的限制不仅仅是存在于相邻之间,意味着仅像 交换 中记录前一个的相对大小是不够的;其次,仿照 Tree 那样确定一个可以转移的顺序的话,位置之间的大小关系可能是非常复杂的,即是图而非树,无法像 Tree 里面用 dfn 序简单确定一个转移顺序。
所以需要转换视角。这道题另一个难点就是字典序第 k 小。字典序第 k 小的通常做法是贪心确定一部分,再计数统计。既然位置不能找到一个合适的转移顺序,不妨顺序考虑值域。发现从值域的顺序考虑,我们依旧可以处理限制。现在的做法变为考虑这个值 x 填入哪个位置 p。在不考虑字典序问题时,即纯计数符合限制排列数,x 可以填入 p 的充要条件就是位置 p 的前驱已经填过数了,但具体填的是什么不重要,因为我们是按值域顺序转移的。遗憾的是因为这个限制它不具有特殊性质,我们需得记录下已填过的数的位置,用 01 表示,即状压。
然后再来考虑字典序的限制。一个误区是依然枚举值域然后对每个数尽可能往前面位置放。这就是 菜肴制作 的经典错误了。正解还是传统做法,对每个位置枚举填入值 i 然后统计在某些位置已确定的情况下有多少种合法排列数。部分位置确定并不影响上述状压 dp 的正常转移。
前驱啥的,求个闭包即可。(其实不用求前驱也行……)


inline ll calc(){
	rep(s, 0, (1 << n) - 1) f[s] = 0ll; f[0] = 1ll;
	rep(s, 0, (1 << n) - 2) if(f[s]){
		int i = __builtin_popcount(s) + 1; bitset<20> nw = s << 1;
		if(rv[i]){ 
			if((nw & G[rv[i]]) != G[rv[i]]) continue;
			f[s | (1 << rv[i] - 1)] += f[s]; continue;
		}
		rep(j, 1, n) if(!p[j] and !((s >> j - 1) & 1)){
			if((nw & G[j]) != G[j]) continue; f[s | (1 << j - 1)] += f[s];
		}
	}
	return f[(1 << n) - 1];
}

85. szoi 魔王神

题意:n 个数初始全为 0,两种操作,一种是把 [l,r] 里面权值为 x 的数全部更改为 x+1;另一种查询区间 [l,r] 中最大值。

难点:修改操作是在值域上的,查询操作是在下标上的。
特点:值单调,修改操作只加不减。值域比较小。

考场上想到过 fhq Treap,对每个值维护它所在的下标。但是修改的时候无法 Merge。
其实这个题,从连续下标的角度考虑会更容易想到。

实在憋不出来了,只能说朱老师太神了。考虑从值单调入手,在位置 i 上的数在一顿操作之下一定是单调不减的。利用这个单调性。延续对每个值域考虑的思路,考虑怎么查询,如果单纯直接去每个值里面找下标,肯定不行的,做法有点愚蠢。不过值单调,启发我们去找最大的、出现在 [l,r] 过的值。

具体而言,每次操作 1 的时候,我们无需把 x 维护的、在 [l,r] 的下标删去,直接复制到 x+1 维护的里面即可。换句话说,对于值 x,维护它曾经出现过的所有下标。这样做的好处是,每次修改操作的时候,可以直接把 rt[x] 在 [l,r] 的一段丢到 rt[x+1] 的 [l,r],并且可以覆盖 rt[x+1] 原先的信息。查询的时候,二分答案,查 rt[mid] 这棵线段树里面是否有 [l,r] 内的下标。每次 Merge 过去,最多大概新建 log 个节点,其实和主席树的节点存法和指向是比较像的。

利用值单调,在保障查询正确性的前提下保存所有历史版本,很厉害。

image

86. CEOI2023 Balance

rdf 模拟赛做过,csp 模拟赛放 T3 还是不会。建议去似。

S 为 2 的次幂,肯定考虑分治。考虑 S=2 的 sub 怎么做。构造方案题。往图论方向思考,二选一的问题,一种解决是 2SAT,另一种是给边定向。这里 2SAT 肯定不是,考虑给边定向。给边定向,要么是放在欧拉图上做,欧拉图更加能解决“找到一种均衡的解决方案”,另一种就是套上网络流做上下界可行流。这个数据范围肯定不是做网络流,网络流更适合做求最优解的问题(?)
考虑欧拉图定向。这个是符合这道题的,因为要求差<=1 就是在求一种均衡的解决方案。如果总度数为奇数,就可以给这些奇点两两连边或者添加超级源点。另外,欧拉图是好的,因为度数都为偶数,天然满足入度=出度,而且一定有解。
分治的话,把 S=2^k 归约到 S=2 的情况,就是序列劈两半然后左右对位判断是否交换。

87. CF83E Two Subsequences

CSP2024S T3 原题搬到 01 trie 上做。很典的问题模型,关键性质是所有字符串长度相等。

按照给定的字符串顺序转移。由于长度相等所以需要记录的转移信息只有另一个序列末尾的 01 串的状态。因为最后一个串 i 所在的序列末尾一定是 i

转移是显然的。fi,s 表示到第 i 个串,另一个的末尾 01 串是 s,拼接总长度最小值。fi,s=fi1,s+mw(ai,ai1)。其中 m 是字符串长度,w(ai,ai1) 表示它们拼接时可以省略的最长长度。另一种转移是 fi,ai1=fi1,s+mw(ai,s)。上述转移取 min 即可。

发现第一种就是全局加。第二种发现可以放到 01 trie 上做。具体地,对于 fi1,s 的值,我们可以把它放到 01 trie 上所有表示 s 后缀的节点。这样做第二种转移的时候,只需要拿 ai 在 01 trie 上跑,对应的 w(ai,s) 的值就是当前已经走了的长度。

调了 1.5h 的细节:每次新求出 fi,ai1 的时候,就把这个值放到 01 trie 上的、所有表示 ai1 后缀的节点上。但是要记得 trie 的根节点的值也要对它取 min,因为转移的 w(ai,s) 可以为 0,即两个串的匹配长度为 0

88. CF79D Password

注意每次覆盖的是连续的区间。题意等价于让你操作使得某几个特定的位置的覆盖次数为奇数。

覆盖次数,区间覆盖,想到差分。操作转化为对距离差为 a_i 的两个点同时进行翻转。发现的 kln 的值都比较奇怪,并猜想最后时间复杂度可能有 O(nkl)。k 比较小,考虑状压,记录特殊点的状态,那么每次我们的操作就是针对两个特殊点进行同时翻转。考虑如何计算代价 cost(i,j) 表示要同时改变第 i 和第 j 的特殊点状态的最小操作次数。异或,区间覆盖启发我们用最短路,因为路径上除了首末以外其他点的贡献都是 0,所以对每个特殊点都跑一遍最短路,边权为 1 直接 bfs 即可,复杂度 O(nkl)。然后就可以 O(2^k k^2) 转移了。

Bonus:其实不算 Bonus。如果题目的“区间”改为“子序列”,那就很好做了,可以考虑使用背包合并覆盖区间。

89. CF1463F Max Correct Set

去年做过一遍但今年看到还是没有什么思路。

首先肯定会想到 x=y 的情况。此时 1 到 n 可以拆分成若干条链,问题等价于相邻两点不能同时选,所以最后答案就是 \sum 链长度除以 2 上取整。
考虑怎样扩展到 x!=y 的情况。延续上述的思路会发现 1 到 n 此时变成了若干条链交织起来,形态可能非常复杂,做不了。考虑换一个角度思考问题,考虑最终答案形式。不妨搬到 01 串上做这个问题。在 x=y 的情况下,答案一定是一段长为 x 的 1 然后接上一段长为 x 的 0,然后一直重复,即以 2x 为周期这样循环下去。启发我们是否能通过构造一个周期然后找到答案。

合法性。考虑如果已经找到一个长为 x+y 的合法的 01 串周期,试图证明不断重复它构成的新的 01 串依旧合法。反证法。若存在 a,b (a<b) 满足 b-a=x,考虑 a-(b-x-y),代入发现它的值为 y,说明此时循环节本身不合法。另一边同理。

最优性。考虑如果正解并不是一个长为 x+y 的 01 循环节周期得到的。那么必定能找到两段长为 x+y 的 01 子串,用 1 数量更多的那个作为循环节得到的长为 n 的 01 串一定更优。

所以问题变成找到含 1 数量最多的、合法的、长度为 x+y 的 01 串。状压 dp 即可做到 O((x+y)2max(x,y))。细节:注意最后 n 可能并不能整除 (x+y),即最后的 01 串并不是整周期,也就是说,上述“含 1 数量最多的、合法的、长度为 x+y 的 01 串”可能并不是最优解(因为可能某一个 1 集中在前面的 01 串作循环节答案更优)。所以在 dp 的时候就要算总贡献,即 val * times + [i<=res]。

卡空间,实现需要精细。滚掉第一维长度,边转移边计算答案。

90. P7962 [NOIP2021] 方差

又是离正解很近的一集。

先推一波式子,然后发现并没有什么头猪,只能看出来结果要让序列的数尽可能平均。从操作入手,发现复杂的一点在于经过若干次操作之后可能变得很复杂,比如 a1-a2+a3 之类不断迭代,而且操作的位置不同计算出来形式也不同,感觉并不可做。试图探究操作的本质。回顾到我们的目标是让它们尽可能平均,所以考虑操作的本质和取平均的关系。然后画了画变化后发现,操作的本质是交换差分序列的相邻两项。

这样就好做多了。然后考虑把差分数组代入刚刚推到一半的式子,发现很恶心。推式子的目的是寻找突破口或入手点。推到一半没推了,有点恶心。现在的问题变成重拍差分数组,使得最后方差最小。手玩了一下小样例,然后画了画增长曲线,发现我们肯定会把大的差分放到左右两边,中间放差分小的肯定是最优的。否则感性理解调整法可以得到更优的解。用题解的话来说,就是差分数组最后是单谷的。

然后是一个什么问题呢,重排序列最小化代价问题。但是这个代价是全局性的(因为我们重排的是差分数组,但是答案的计算是基于原序列的,即要做个前缀和才能算)。所以想到了几个方向:一,代价可以在过程中计算,可以边做边维护;二,可以找到一种转化,即用另一种“体系”来衡量评价这个代价的最优性;三,全局性的代价,实在不行就不能 dp,只能考虑贪心,通过一些性质直接定位最优解;四,估价函数,类似搜索 A* 那样进行一个初步的粗略的估价,但其正确性是无法严格保证的,这一条不太在我们的考虑范围内。通过手模第二个下发样例,第三条也基本可以不考虑。先不急着找新的转化(因为我们已经转化过一次了),发现这个代价其实是可以边做边维护的。但前提是利用单谷的性质。

额这时候已经思考了一个小时了,太久了就没继续了。但实际上已经非常接近了。dp 的本质是把一个问题归纳到它的子问题。考虑部分分。发现对于每个差分值,我们只需要考虑它是在左侧还是右侧。所以我们可以按照差分值的大小从小到大考虑它们的位置。最后代价主要由两个部分组成:和 与 平方的和。发现如果每次转移都考虑它对后面的所有贡献是不可做的。所以不妨只考虑在当前这个 n 规模较小的 子问题 中,它的贡献。关键信息是 和 与 平方的和。定义状态 f_i,s 表示考虑了前 i 个差分值,和为 s,的最小的平方的和。注意这个和 s 表示现在已经考虑了的、i 个数(对排好的差分序列做前缀和)的和。转移方程并不难。复杂度 O(n^2V),观察数据发现可能有很多的差分值为 0,直接跳过不转移即可。

这里也有一个启发。对于 dp 计算贡献,我们有时候会“预支贡献”,比如上面 45. sg-noi Pond,就是状态省掉区间的一端点,转移维护贡献的时候一起计算 目前还没有考虑到的点 对全局的贡献。但我们有时候也会“忽略贡献”。比如这道题,我们不考虑当前转移的这一项对其他现在还没有考虑到的点的贡献,只把当前的转移完完全全当成一个“子问题”来做。然后等到后面转移的时候,再把前面刚刚没有算的、对它的贡献计算进去。显然,两种不同维护方式的选择要看这个代价本身的计算维护特征。

忘了说。90 题祭!!

91. CF280C Game on tree

非常厉害的期望解题方式:随机变量指示器。

顾名思义,引入指示器来描述事件的发生情况。指示器 X 只有 0 或 1 两种取值,1 表示事件发生,0 表示事件未发生。在本题中问题可被抽象为随机一个 1 到 n 的排列,求染色点数的期望。那么记 W 为最后染色节点数,X_i 为对应点 i 的随机变量指示器,那么有 W = \sum_i=1^n X_i。

两边同时取期望,有 E(W) = E(\sum_i=1^n X_i)。再根据期望的线性性,有 E(W) = \sum_i=1^n E(X_i)。然后就是随机变量指示器的巧妙之处了。具体数学中给出了断言“期望的线性性质利用指示器随机变量作为一种强大的分析技术;当随机变量间存在依赖关系时也仍旧成立”。也就是说,虽然这里 Xi 相互之间并不独立,但我们却仍然可以独立地考虑它们的期望。

即,这里 E(X_i)=P(i),其中 P(i) 是 i 被染色的概率。独立地考虑 i,在所有排列中,只有它排在所有祖先的前面,它才会被染色。所以 E(X_i)=P(i)=1/dep_i。

所以本题的答案即为 \sum_i=1^n 1/dep_i。另一篇讲随机变量指示器的题解。

Bonus:后面部分和树拓扑序计数是一样的。这不禁让我联想到了 84 szoi 交换,链式带限制图的拓扑序计数。这两个部分,是不是可以结合起来呢。

92. AGC014D Black and White Tree

博弈。唉,这种博弈的规律感觉和构造一样比较捉摸不定啊,,

对于每个白点,后手都要让它和一个黑点相邻。样例给的大多是链的情况,手模了一下链的情况,发现长度为奇数的时候先手必胜,偶数时后手必胜。但是模的不够多,没有找到先手必胜的本质,也没有能够从链扩展到其他情况。
在链的基础上,考虑如果有分叉怎么办。考虑最简单的分叉,四个点,(1,2) (2,3) (2,4) 这种情况,发现先手一定会选择 2,然后后手必输。这个分叉可以简化到 3 个点的情况,即,如果某个点有两个叶子儿子,那么先手必胜。
仔细思考一下先手后手必胜的原因。回归偶链的情况会发现,后手只要贴着先手下就可以必胜,其本质是此时有完美匹配,后手只需要下先手下的匹配点即可必胜。而在没有完美匹配时,先手可以通过不断选择某个叶子的父亲,后手必选叶子,然后删掉这两个点。此时必剩下孤立点,而这个孤立点的周围被删掉的点一定是白点(因为我们自底向上删,而先手每次操作更浅的那个)。先手直接选择那个孤立点即可。
模拟过程即可。

93. P3345 [ZJOI2015] 幻想乡战略游戏

动态带权树重心 + 带权距离统计。

问题大概是点边带权,动态修改点权,每次找到 val(x) 的最小值,其中 val(x) = \sum dis(x, v) \times a_v。

首先肯定能想到一个动态移动点 x 的贪心:如果往某个方向移动的收益为正,那么一定往那个方向移动。可以证明这个“收益为正”的方向至多只有一个。收益为正的充要条件是,对于边 (u,v),假设当前在点 u,那么这条边把整棵树划分为两大部分,v 所在那一部分的带权点数(即只考虑点权不考虑边权)一定大于 u 所在的。也即 v 所在那一部分的带权点数 > N/2。N 是总点权和。所以显然这样的方向只会有一个。只要一直沿着这个方向走,直到不能走(即周围方向都是负贡献),那么这个点就是答案点。

从上述分析也可以看出来,最优点的选取和边权是毫无联系的。所以,问题可以转化为树上动态带权重心。考虑怎样快速求出新的重心。遍历一遍树复杂度不够优。如果按照上述方法模拟:每次找到 sum > N/2 的儿子,往这个方向走,复杂度是深度相关的。

这里就有两种思路:要么找到快速模拟的方法,要么直接减少树的深度。后者是动态点分治即点分树,不会;前者就是线段树二分 + 树链剖分。发现我们走的路径是一条链,树上的一条链,考虑用树链剖分优化复杂度。发现对于这个点的重链,我们是可以通过二分来找到最深的、sum > N/2 的儿子。但是如果这个点的重儿子 sum 不满足这个性质,此时就要看轻儿子,也就是跳到某一条轻链,再继续上述操作。如果实在没得跳,就结束了。显然,跳轻链的次数是 log 次的,因为每跳一次,我们问题的规模就会至少缩小一半。选择轻儿子的复杂度是 O(度数),题目保证了度数 <= 20,再加上线段树上查询子树大小的复杂度,找到重心的送复杂度是 O(nlogn^3)。

找到答案点之后,就要快速计算 val(x)。套路地把 dis(x,v) 拆成 dis_x + dis_v - 2 * dis_lca,其中 dis_x 表示 x 到根的路径长度。前两个都是很好维护的,考虑最后一个怎么维护。发现 lca 一定是 x 到根路径上的点,还是形成了一条链,所以还是能用树链剖分优化。记 g_x = (sum_x - sum_mxs) * dis_x。然后就可以像动态 dp 那样的套路用树链剖分的轻重儿子来优化计算过程了。

实现上没有什么特别的细节,半小时一遍过!!!

树链剖分提供了一种解决树上链式问题的优化手段,即类似分块对于线性结构(能这样叫吗)的优化,拆分成子问题,以灵活组合回答询问,并且保证修改的复杂度。相比之前下,由于树形结构,树链剖分拆分后的规模可以降到 log,分块只能降到 \sqrt。但我认为二者的本质是相同的,都是拆分为子规模然后灵活组合。

94. SP186 LITELANG The lightest language

哈夫曼的拓展,肯定考虑贪心。然后莽了一坨拍了一坨之后发现,我们可能会在弹出一个值后,往优先队列里面放回一个比它更小的值。这个和 Dijkstra 必须是正权是一个道理。出现这种情况就说明我们优先队列贪心取最小的策略是失败的。

那怎样做呢。还是延用贪心的想法,如果这个点已经没用,那么它的所有后继也一定没用。上面错解的原因启发我们当前找到的最优解可能并不是真的最优,可能存在后继更新答案的情况。结合上面两条,我们在贪心地选满 n 个之后继续找,直到后继对答案不再有贡献即可。

难点在于发现贪心假了之后如何调整做法。启发我们尽管当前贪心算法假了,也许可以从漏洞入手,结合性质,完善拓展算法。

95. szoi 12256 石樱旅行

思维题。好题。

题意:有 n 个形如把一个在 x_i 的石头转移到 y_i,横坐标轴。推车只能至多装一块。1 单位距离花费 1 单位时间,转向时间花费是 C,问完成所有任务然后回到原位原方向的最小时间。1e6 个任务。

感觉很棘手,考场上考虑探究它是怎么走的。回到原点原方向说明有来必有回,且转向次数为偶数。考虑 sub C=0 的情况。此时不考虑转向的代价。猜想答案可能是所有 abs(x_i - y_i) 的和的 2 倍,错误。有反例。正解考虑对于某一条线段,假如有 a 个任务从它的左边到右边,b 个任务从它的右边到左边,那么它至少要贡献 2 * max(max(a,b),1) * len。原因还是有来必有回。在不考虑转向时,答案就是这个。
考虑 sub C=1 的情况,此时我们不会用路程去换转向。考虑如何计算、最小化换向次数。感觉从这里开始就需要一点人类智慧了。问题可以进一步抽象,抽象为一堆环连接成一个大环(因为我们最终走出来的路径就是一个环)。如果这个位置的经过次数比旁边的更多,就说明会多出来一个“凸出”的小环包含它。一个环就代表了一次转向。
现在我们的决策是可视化且可以被描述出来的。C 的代价是不确定的,也就是转向和距离长度是很难断言是怎样一种融合程度的。如果 dp 呢?路径跨度可能很大,暂且无法找到非常好的确定方式。贪心?对于这道题 C 的不确定性,贪心似乎是一种好的解决方式。我们可以不断地比较调整路径的长度代价和将要减少的转向代价,来贪心地调整我们的路径长度。
严谨的证明考虑把具体到每一段上的行走抽象成一个箭头,箭头首位相接,每个位置有 cnt_i 个左向箭头和 cnt_i 个右向箭头,我们想要在一定程度上让它们尽可能首尾相接。
然后可以阅读题解,题解写得很好了。

image
image

启示:对于复杂问题,要有抽象问题的能力。这里转向代价和路径长度之间的不确定性,使得一眼看去并没有非常明确的方案。并且直接行走的策略看起来非常灵活不确定。从 C=0,C=1,C=inf 入手是非常好的切入点,C=0 可以帮助我们挖掘问题的本质,然后去抽象成等价的、简化的问题,再对抽象模型进行最优性策略构造,实现对问题高级的转化。

96. P4246 [SHOI2008] 堵塞的交通

我愿意称之为个人年度最佳题解(之一?)。

非常流畅地解释了看似没有由头的“线段树”的背后来历,深刻揭示了看似毫不相干实则本质相同的做法共性。(感觉好吹牛。)但确实是包含了我这一段时间内对一类问题解决方式本质的思考与归纳。

启发是来自周航锐信友队的一次演讲中提到的。(其实我可以大致还原一下他的讲话核心内容。)一个是他认为所谓拿金牌的秘诀就是对算法竞赛由衷的热爱,只有这种热爱才能支持一个人一直为此努力去付出心血。否则一个人一直被其他人推动着去训练,但这种训练是不本质的,他最有可能的就是缺乏思考。有的人可能做了第一题做了第二题就会做第三题,但缺乏思考的人却不行。另一个是对一些算法、一些技巧本质的理解。这些理解可以让你把毫不相关的一些技巧方法联系起来,从而也就给了你举一反三的能力。他提到自己两次进步非常快的时期,一个是高一刚开学,另一个是高二刚开学。第一个是有学长带着他一起做题,机房有一个非常良好的氛围。第二个就是他有一个非常好的交流平台。

跑题了许多,这是题解链接

97. gym103427M String Problem

绝世字符串好题。

这个答案一定是上一个答案的后缀 border 加上新的字符得来(后缀可能是自己)。同时答案一定是一个 Lyndon Word 状物,即所有 border 的下一位一定小于从头开始等长字符串的下一位,否则答案不是最优。所以这些 border 下一位字符的大小可以看做严格不增的。所以我们肯定会选择最短的、下一位小于新加字符的 border 接上新的字符。由于这个单调性我们可以直接回退,然后新加入字符就等价于动态去做 kmp 求 nxt,复杂度线性的。

感觉难点在于不断去挖掘这个答案串的性质。绝对绝世好题。

98. P9984 [USACO23DEC] A Graph Problem P

好题。思维 + Kruskal 重构树。

感觉我需要反思一下了。到底是真的完全想不到还是单纯没有耐心不愿意继续钻研。想不到的就归纳到“思维”然后合理摆烂是吧。

一开始想到了相邻的增量做法。然后考虑用点分治优化。其他都没什么问题,但是这个答案根本没法统计。

突破口在于答案的统计方式。这个答案的统计方式其实已经极大地限制了我们的做法了。这个答案的统计方式变相地告诉我们我们是在已知扩张过程的情况下直接计算答案。换句话说就是贪心确定了遍历方式。

这是个图。所以不用想肯定先求个最小生成树再说,正确性易证。问题涉及到了边的标号,可以等价为边的权值,大小又是重要的,自然而然地联想到 Kruskal 生成树。怎么说,看到生成树边权相关想到 Kruskal 生成树。你都考虑生成树了,又考虑边权了,你还不用 Kruskal 生成树简化模型?

套上 Kruskal 生成树,发现每一次一定是扩长完成一个连通块,就是完成一个子树,然后走到父亲,走完另一个子树,然后继续……这样答案构造过程就非常显然了。考虑如何计算。考虑构建 Kruskal 重构树的过程。每次合并连接的时候,左边的答案统一都会乘上 10 的 sz_rgh 次方,加上边权,加上右边答案。所以用线段树维护区间加乘即可。

不过存在更加简便的方式。过程本质是合并。考虑带权并查集优化这个过程。精细实现后代码非常好看。注意并查集合并信息可能覆盖的细节。

99. P5999 [CEOI2016] kangaroo

提供了一种全新的序列计数的 dp 方式,甚至可以扩展到很多其他的构造性问题。启发不要被固有的解决方式限定了思维,不要因为自己做过的一些题目是怎样解决的就故步自封。

转化为排列计数,首末确定,要求是任意一个数要么同时大于左右相邻,要么同时小于,很经典的限制。发现如果没有首末的限制是很好做的,就用传统的记录相对答案按照下标顺序转移的计数 dp 即可。

但是在限制了首末的时候,似乎记录相对大小就不那么好使了,很难判断。但我们又不能全部记下来用了哪些,即状压,因为除了首末的值以外的值对我们而言都没有意义,我们只关心它们的相对大小关系。以上我们使用的思路都是按照下标的顺序去构造完善这个排列,在平衡 记录已使用过的数 尽可能简化记录信息 之间产生了矛盾。

但抛开这个思路。dp 是按照某一个既定顺序去进行的。既然按照下标构造的时候不好记录值域,那就考虑按照值域顺序进行 dp。顺提一嘴很多时候一些做法不能继续下去本质大概可以理解为是它的“进行方式”限制了我们的一些思路或者说思路的进行,使得在某些方面产生了不可继续下去的矛盾。说的很抽象,但很多时候判断这个思路是否可以继续下去确实是非常重要的。也要学会及时地舍弃一些固有的观念,学会跳出来以新视角审视问题,发现新角度。

考虑从小到大把数一个个塞进这个序列。但在最终序列里面数显然不可能都是按值域连续的。但我们进而又发现已经放好的、相邻的一些数,它们之间的相邻状态不会再被改变了,所以可以抽象化为我们在维护一些块状物,然后每次可以往某个既存块的左右侧放入新的数,也可以新加入一个块,也可以连接两个块。基本操作就是这三种,可能因为题目具体限制不同而变化,比如本题就不存在放入既存块的情况(因为一边比它小一边比它大)。f_i,j 表示放了 1~i,已经有 j 个块的方案数。转移方程显然。

启发对于一些看似不连续不好维护的东西,可以考虑转化为维护一些块状物。动态修改它的形态。

100. P4696 [CEOI2011] Matching

一百祭!!!!!!!!!!!一百祭!!!!!!!!!!!!!!一百祭!!!!!!!!!!!!!!!!!

2404 ~ 2411,半年多。

第一道 gym icpc 区域赛的树上问题还有这道题都写完一遍过了,没调。果然这个 rp 是守恒的,昨晚的罪没白遭。这是我应得的!!!

两种做法,一种比较直观,另一种比较需要脑子。

相对顺序的匹配。给的是个下标序列。发现只记录相邻的大小关系不够紧。即这个匹配的要求还是挺也严的,考虑直接上哈希。类似尺取过程,对于当前长度为 m 的区间,将下标按照对应权值大小排序之后,统一减去 区间左端点 - 1,如果和给定排列 p 完全相同就说明对了。直接上哈希,序列整体加上 x 的哈希是好维护的,所以直接维护排序后下标构成的序列的哈希即可。平衡树维护即可。扫描题解区发现线段树也能做,都行。

另一个就是 kmp 了。直接硬匹配。和字符串匹配不同的是这里的判等比较特殊,需要比较的是加入这个数之后序列离散化后是否和另一比较方相同。只考虑新加入的这个数带来的影响,可以发现我们只关心这个范围内,比它大/小的数的数量。用数据结构(比如 BIT)维护带一个老哥。发现前面已经匹配的相对顺序是完全相同的,所以直接比较前驱后驱下标是否相同即可。因为我们不想去查区间,所以要用前驱和后继两个同时限制它。只维护模式串的。不会出界因为每一次都是询问从头匹配的那里。感觉讲的好差。。

101. P6846 [CEOI2019] Amusement Park

状压 dp、容斥 dp 好题。

拿到之后只知道应该是容斥。但一点头绪都没有。

先考虑弱化版:给无向图,定向为 DAG 的方案数。

状态记录环什么的,感觉很不好做。考虑反面至少有一个环感觉很不好做,不如直接做。f_s 表示把这个集合定向为 DAG 的方案数。状压 dp 的本质和其他 dp 的本质是相同的,都是归约到等价的子问题。所以在思考转移的时候不要想着硬转,应该想着如何找到一个“顺序”,然后拆解到子问题。这里考虑 DAG 中第一层节点,考虑钦定 s 的一个独立集作为第一层节点。然后可能算重,容斥即可。实际上很好做。

感觉这个子问题是一般的。

原问题如何做。我有过一个枚举每条边的思路,但是确定单条边做上面这个问题是困难的。考虑答案的对称性。对于一个 DAG,它的反图也是 DAG。所以方案可以两两一组,一组的改变量是 m。答案就是方案数乘上 m/2。

102. P4697 [CEOI2011] Balloons

化简式子得到 ri=minj<i(xixj)24rj。显然可以整一个单调栈,x 递增,r 递减。但现在最劣还是平方的。

我们要在这个偏序集里面再深挖性质。从过程入手,看看前面已经结束的操作对现在有没有用(和上面 kmp 那个判等很像,都是用既有的状态的性质进行过程优化)。发现栈顶也是对栈内其他的项取 min 得到的,再对它自己的 mx 取 min 得到。那么如果现在 i 和栈顶计算答案,算出来比栈顶的要小,显然它就没有必要再和其他的算了,因为栈顶已经和它们算过了;反之,按照上面说的 x 递增 r 递减,就可以弹出栈顶了,然后和新的栈顶进行计算。这样就是线性。在求出的答案比栈顶小的时候做了优化,利用了已经获得的状态的性质进行优化。

换言之,考虑增量算法,和这个本质比较像。都是从某种意义程度上来说继承前面的信息,用于计算或者优化。

103. CF2032E Balanced

真成傻呗了。

开局式子列一半 a_i + 2v_i + v_{i-1} + v_{i + 1},结果是最后序列中相等的值,这里可以用相等的传递性,等号右边写 i+1 的这个式子。然后发现可以消一些项,得到 (v_{i-1} + v_i) - (v_{i+1} + v_{i+2}) = a_{i+1} - a_i。记 b_i = v_i + v_{i+1},就发现 b 的项的差会被 a 的相邻的差限制。但 b 具体是什么无法确定。假定 b_1 b_2 为 0 的话就可以暂时推出来得到其他 b 的所有值。注意答案序列虽然可能和它不同,但是值的差是不变的。然后用 b_n 和 b_1 比较判是否合法。合法的话就把 b 在值域上平移到合法即可。

对 a 做个环形差分(第一次听说还有环形差分,但是对于这种跨环的操作而言竟然还挺对),操作就是对差分数组的 +1 +1 -1 -1,发现这个操作对下标的要求很高,考虑组合起来成为新的操作。然后就不会了。构造确实太弱了。发现可以拓展成 +1 +1 0 0 0 0 0 0 -1 -1,中间 0 的个数只要是偶数就都行。然后利用序列长度为奇数(这个非常有用),一直延长中间的 0 长度直到第一个 +1 和最后一个 -1 重叠。最后操作的效果就是 -1 0 +1。然后同理对新操作首尾拼接,得到 -1 0 0 +1,然后同上理就发现我们的操作变成对任意位置 +1,同时对任意位置 -1。主要是利用了 序列形态是环 和 序列长度为奇数 这两个性质进行操作的拓展。对差分数组的操作是简单的,但对应到原序列上就会稍微麻烦一点。

对操作找规律什么的,还是要再多点耐心多尝试不同的搭配方式。

104. CF2032F Peanuts

欸,好题。

反 Nim 游戏。
博弈论游戏,Nim 啥的,好多变种拓展啊。

考虑如果只分一段怎么做,Nim 游戏。分两段呢?如果后一段的 Nim 游戏先手必胜,那两个人肯定都想在第一段里输。此时第一段变成了 反常游戏,即都想输,输就是赢(!!!)。不要搞混了,此时 Alice 目的是要在每一场游戏中赢,换句话说,只有她在第 i 场赢了,她才能去赢第 i+1 场,这里改变的只是对游戏“输赢”的定义。比如第二段如果是 Nim 游戏,然后先手必胜,此时第一段就是反 Nim 游戏,Alice 要先在反 Nim 游戏中获胜,然后她就能在第二场中获胜(因为第二场先手必胜)。

如何在反 Nim 游戏中获胜?即在 Nim 游戏的规则下,让自己输,即让自己没得拿。

  • a_i 全为 1。此时若 n 为奇数,则先手必败,否则先手必胜。(因为此时先手无法让自己成为“没得拿”的人。)

  • 否则存在 ai>1 的情况。

    • 若只存在一个 ai>1,记为情况 A,先手必胜。那么经过先手一次操作一定能变成上面第一种情况。先手可以选择拿光或者拿成 1,即此时先手必胜。

    • 若存在至少两个 ai>1,结论是,和 Nim 游戏一样,异或和为 0 时先手必败,否则先手必胜。

      • 全局异或和为 0,记为情况 B:拿一次一定变为异或和不为 0 的情况。
      • 全局异或和不为 0,记为情况 C:同 Nim,可以拿成情况 B。但不会改变“至少有两个 a_i > 1”的前提,因为异或和为 0。

      显然 BC 可以 来回切换。考虑情况 B 的时候,它操作一次后要么变成 C,要么变成 A,且一直做 B 情况最后一定会操作成情况 A。那么局面就很明了了,B 一定是先手必败,因为后手可以做到让先手一直处在 B 局面。所以,C 先手必胜。

发现 Nim 游戏和反 Nim 游戏是否先手必胜的判断条件只在 a_i 全为 1 时不同。

假设现在序列已经分好段了,倒序来看,最末尾的段是 Nim 游戏,之后的每一段属于哪种游戏都取决于它下面那个段的输赢情况(在下面那个段是 Nim/反 Nim 游戏时,它是先手必胜还是必败)。即它的游戏性质由下一个游戏的胜负决定,但它的胜负不受其他段影响,只会影响它之后的游戏的性质。所以在安排好所有游戏的性质后,我们只要让 Alice 能赢第一场就行了。

记 f_i,0/1 表示前面 i 项分段,最后一段的游戏性质是 Nim/反 Nim 游戏,然后 Alice 必胜的方案数。方程挺好写的,然后开桶优化一下转移即可。单老哥。

105. gym104901F Say Hello to the Future

好题。

三维偏序优化 dp。以及偏序相关的内容。感觉准确来说不能叫三维偏序优化 dp,因为偏序关系是本身具有的,优化的只是求解的过程。

很容易列出来方程 f_i=\sum f_j,其中 j 满足 (j,i] 合法。但这个合法区间就很不好找了。因为它和区间长度及最大值有关,容易想到单调栈做法,但很难再优化下去了。考虑形式化这个限制,f_j 贡献给 f_i 当且仅当 j<i, i-j>=max(a_j+1...a_i)。算是非常经典的区间划分的转移过程,这个区间无法简单直接地判断是否合法。考虑分治,考虑跨过 mid 的区间的合法性以及产生的对应的贡献。限制转化为 r-l+1>=max(mxl, mxr)。r>=mxl+l-1,l<=r-mxr+1。二维偏序的形式(分治已经优化掉 j<i 的一维了)。双 log 求解的基本框架已经得到了,修改后重新求值就可以考虑增量。关心那些本来不合法,但是 a_i 改为 1 之后合法的区间。最后答案乘上这一段左右两边重新划分的方案数即可。还是从区间入手,拆柿子之后还是二位偏序。

以及 CF2028D,我们行走的路线中,R_i 和 R_i+1,需要满足 R_i<R_i+1,且它们在某一个人的排列中要满足 R_i 在 R_i+1 的后面。本质是二维偏序。我们要考虑取消掉一维的限制,按照下标?不,因为有三个排列,不同排列相对位置不同不好处理。按照值域。从小到大考虑,那么 1 在任何一个序列中,排在它前面的一定都能到达,然后考虑第 2 个,它是否在某一个排列中排在 1 的前面,若是,那么对于 [3,n] 中的数,如果某次它在 2 前或某次它在 1 前,就都意味着它能被到达。

揭示偏序的本质,能让我们更好地找到优化入手的地方,侧重点就更加明显。或者说形式化拆开限制,能让我们更加清晰地发现矛盾所在,化解矛盾,解决问题。

106. CF2026F Bermart Ice Cream

非常高质量的好题。把很多基础算法思想结合在一起了。

先不提 234 操作让我们联想到的双栈模拟队列。因为会发现卡在 1 没法做下去。必须先解决 1,即新建版本。回想到像历史版本线段树,一个节点代表一个状态,然后复制的时候用指针指向代表状态的节点。本质是每次只处理修改带来的变化。缺点是空间。这里显然不能采取相同的做法,原因是空间无法承受。对每个结点维护双栈,显然有很多冗余。

历史版本的另一个 trick 是,借助这些版本之间树结构关系,离线来做。每个版本都可以找到上一步的版本,即构成了一个树结构,那么借助撤销操作,我们就可以用类似 dfs 遍历树的方式,走到一个节点就能维护出它对应的双栈状态。这是一个很经典的 trick,借助新建版本这种依赖关系构成的树结构,每次维护对应的增量。

然后是 4,这就是一个 01 背包,没什么可说的,注意值域是 2e3。

然后是 23。这很显然的双栈模拟队列。因为有“加入序列,删除最先加入的”。但这还是不够。因为我们这里需要支持撤销操作。即,我们要维护 头插 尾插 头弹 尾弹 查询全局 01 背包 的数据结构。

解决这类问题的数据结构可以归纳到如下:

支持随时 O(1) 查询全局最值,或者更加形式化地,支持以 O(T) 复杂度查询全局某信息,其中这个信息具有可合并的性质,合并一次的复杂度是 O(T)。

  • 栈:每次放入的时候,mn_top = min(mn_top-1, a_top),这样我们查询的答案就直接取栈顶即可。

  • 队列:考虑双栈模拟队列。此时是尾插头弹。用两个栈 L,R,尾插等价于 R.push,头弹等价于 L.pop,问题规约到上述栈的子问题,所以回答查询的复杂度是 O(合并答案)。若 L 为空,就把 R 里面的东西全部倒进 L 中。均摊线性。

  • 双端队列:我们还是可以做到均摊线性。此时是 头插 尾插 头弹 尾弹 全局查询。问题显然是处在每次 L 空倒 R 时。因为两边都可能弹出,每次都把另一边全部倒到这边显然是不优的。正确做法是每次 rebuild 重构两个栈结构的时候,让两个栈的元素数量相同。可以证明是均摊线性的。除此之外做法同上,查询的复杂度依旧是 O(合并答案)。

    证明:重构的复杂度是 O(\sum T_i),其中 T_i 表示第 i 次重构时两个栈元素数量之和。不妨记在第 i-1 次到第 i 次重构之间,我们新加入了 q_i 个元素。那么有 T_i = T_{i-1}/2 + q_i。因为重构必然是因为某个栈空了。那么 q_i 对于 T_i+j 的贡献就是 q_i/2^j。复杂度等价为 O(\sum q_i(1+2+22+23...))=O(\sum q_i)=O(n)。2 倍常数吧。

做法基本明了了。思路是很清晰的。实现的时候建议把 栈 和 双端队列 都单独封装一下,写起来更整洁清晰。Sub.

107. CF2026E Best Subsequence

很难碰上一个这样毫无思路的 E。

很棘手。如果按照正常思路来的话。经过一番无力挣扎,会从合并一些 1 位置集合的角度出发考虑。也就是这种选择和收益的问题。如何选择(决策)使得收益最大。其实其他问题也是这个本质,不过这种网络流更加能解决数据范围较小(且数据范围让你没有什么思路)决策的阶段性收益非常不好统计,很难阶段性找到(记录下)最优解的问题。

比如说这道题,我们就很难记录已经选择了哪些位置,因为值域范围是 2^60。且一开始收益可能持续为负,直到某一次收益突然转正。决策情况很难找到一种方式去刻画。

完全没想到网络流。和太久没做有点关系吧。其实就是最大权闭合子图问题。选择一个数,收益为 1,但代价为 popcount。等价为最小割问题,等价为最大流问题。

不过还有更优秀的做法。答案转化为选择的数个数 + 未选的 2 进制位 - 60。最大化选择数的个数 + 未选的 2 进制位数量。a_i 不能和它 2 进制 1 位同时选。二分图的形式。最大独立集。等价为最小覆盖集的补集,等价为 总节点数 - 最大匹配数。最小覆盖集 = 最大匹配数。严谨证明可以去看证明可以去看高阶一学习笔记。

所以写一个匈牙利就行。代码很短。

108. gym105170H Games on the Ads 2: Painting

怎么说,又是特殊性质 DAG 拓扑序计数。上次这个还是在上面 84 szoi 交换。

发现性质:行刷和行刷之间不可能连边,所以是二分图。一个行刷颜色只可能和一个列刷颜色相同,所以每个点连出去的边是 n-1 条。二分图,但是求拓扑序数量。

然后比较靠经验的一步来了。考虑每个点连出去 n 条边的二分图。即“完全图”,性质是在拓扑的过程中,左部和右部不可能同时出现入度为 0 的点,证明显然。所以,对于每个点有 n 条边的二分图,它的拓扑序等价于有若干个相对顺序固定的点集,点集之间不可以交换顺序,内部任意排列。方案数即为 \prod sz_i!。怎么想到呢?要么你灵光一现想到增加限制考虑性质,要么你有强大的经验积累。

然后我们就可以 2^n 暴力枚举这些实际不存在的 n 条边的方向,然后做上面的“二分完全图求拓扑序数量”的问题,对答案求和和即可。

所以,在解决问题的时候,我们不仅可以采取“忽略/删去部分限制,简化问题,构造模型,规约问题”的方式,还可以采取“添加限制,规约问题,解除限制”的方式。后者大多依赖于一个成熟的、已被解决的问题(有点废话)。但本质都是规约到其他问题模型,再解决问题。只不过在统计方案类问题中,增加限制似乎是一个更聪明的方向。

一口气来写 4 道记录,真是来清库存的来了。

109. P6240 好吃的题目

猫树分治小记 by tzc_wk

静态区间查询信息类问题。

  • 可合并撤销(群信息):维护前缀(区间和/异或和),复杂度 O(n+q)。

  • 可快速合并且支持重复计算(半群):线段树(可带修)、分块、倍增 RMQ。

  • 可快速合并但不支持重复计算(半群):线段树。

  • 不支持快速合并但支持撤销:莫队(离线)。

  • 不支持快速合并且不支持撤销:猫树(强制在线需要存下每一层分治区间信息再 log 查询)。

猫树的复杂度是 O(nlognT + qT + qlogn),其中 T 是单次合并信息的复杂度。猫树的核心是把区间集体处理,减少合并信息的次数。思路类似统计符合某某条件的区间个数。

posted @   pldzy  阅读(61)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示