杂题笔记
KMP
- 记录第 \(i\) 个前缀的最长 border 为 \(\pi(i)\),考虑第 \(i + 1\) 个前缀可能 border 的形态.
- 发现第 \(i + 1\) 个前缀的 border 除了第 \(i + 1\) 个字符以外,前面的部分一定恰好是第 \(i\) 个前缀的 border.
- 考虑如何快速跳转访问到第 \(i\) 个前缀的所有 border.事实上,第 \(i\) 个前缀的第二长 border 即 \(\pi(\pi(i))\).依据这个跳转原理可以快速访问第 \(i\) 个前缀的全部 border.
- KMP 即考虑 \(P + \# + T\),则完全匹配当且仅当 \(\pi(i) = m\).
- 时间复杂度是 \(\Theta(n)\) 的.因为整个算法的时间复杂度不大于对变量 \(j = 0\) 操作 \(n\) 次,要么令 \(j \gets j + 1\),时间复杂度贡献 \(1\);要么令 \(j \gets j - w\),时间复杂度贡献 \(w\).这里的 \(j\) 对应的是 KMP 里的 \(\pi(i)\).
ACAM
- 对模式串集合建立 trie 树,则节点保存了模式串前缀的状态,设节点 \(u\) 代表的前缀是 \(S(u)\).
- fail 指针 \(f(u) = v\),其中 \(S(v)\) 是 \(S(u)\) 的最大真后缀.特别地,根节点 \(f(0) = 0\).
- 这里的 fail 换成 fa 之后,深度(即前缀长度)从小到大地考虑,每个点的 fa 都是先前出现过的.因此,把 trie 的全部边删除,将 fail 看成边,得到的仍然是一棵树.
- 考虑求出 \(f(u)\).
- 类似 KMP 的思想,考虑已知 \(u\) 和它的后代 \(v = T(u, c)\),如果 \(f(u)\) 也存在一个后代字符为 \(c\) 的节点 \(z\),则 \(f(T(u, c)) = z\).
- 否则,进一步考虑 \(f(f(u))\),\(f(f(f(u)))\)……直到有一项存在后代为字符为 \(c\) 的节点为止.
- 现考虑暴力跳 fail 的优化.类似路径压缩的想法,考虑令 \(T(u, c)\) 不存在时,定义 \(T(u, c) = T(f(u), c)\). 这样以来,\(T(u, c)\) 的实际含义是:对 \(u\) 不断 \(u \gets f(u)\),直至 \(u\) 含有字符为 \(c\) 的后代时,的这个后代.这里 \(u \gets f(u)\) 的迭代可以进行零次.
- 这样以来重新考虑计算 \(u\) 的子节点 \(v\) 的 \(f(v)\).设 \(v\) 代表字符 \(c\),我们发现根据前文,\(f(v)\) 的求值方式跟 \(T(u, c)\) 十分类似,唯一的不同是 \(u \gets f(u)\) 的迭代至少要进行一次,否则一次迭代都不进行的 \(T(u, c)\) 其实就是 \(v\).因此,\(f(v) = T(f(u), c)\)——从 \(u\) 开始迭代至少一次,即从 \(f(u)\) 开始迭代至少零次.
- 根据上面的原理,设计 ACAM 建树算法.
- 深度(前缀长度)从小到大地考虑节点.对于 \(u\),若 \(v = T(u, c)\) 存在,则 \(f(v) = T(f(u), c)\).否则,\(T(u, c) = T(f(u), c)\).
- 建树算法的复杂度为 \(\Theta(n \Sigma)\).
- 下面考虑多模式串匹配的过程.
- 对模式串集合建树.则问模式串在文本串中的出现,可以考虑对每个文本串的前缀,求出它有多少个后缀在 ACAM 中出现.
- 考虑文本串 \(T\) 从 \(1 \to n\).令 \(u\) 初始为 \(0\).对于 \(c = T_i\),\(u \gets T(u, c)\).
- 首先思考一下这个 \(u\) 在干什么.不难发现,如果 \(T(u, c)\) 是 trie 原本存在的节点,那么跳转.如果 \(T(u, c)\) 原本不存在,说明 \(S(u)\) 后面接上 \(c\) 之后在模式串集合中已经不存在.因此,这里 \(u \gets T(u, c)\) 就会让 \(u\) 一直舍弃前缀,直至 \(S(u) + c\) 能在 trie 中出现.很精妙.
- 然后现在就可以考虑令 \(j = T(u, c)\),通过 \(j \gets f(j)\) 的方式,再统计 \(u\) 所有可能在模式串集合中出现的后缀.
- 观察到整个过程实际上是一个 \(j\) 跳 fail 的过程,先前已经说明 fail 关系视作 fa 关系之后,整个 ACAM 仍然保持一个树状的结构.所以可以只考虑每次只在节点 \(u\) 上挂一个标记,最后从深度最大的点向根节点拓扑统计即可.
https://www.luogu.com.cn/problem/P5357
tarjan & rst & 2-SAT
- 强联通分量 / 缩点将有向图 -> DAG,点双连通分量 / rst 将无向图 -> 树,性质变好的同时,保留了一定的可达性信息.想法比较类似只考虑保留无向图连通性信息时,可以考虑并查集.
- 点双连通分量、割点问题,可以考虑直接建 rst(rst 通常用来刻画无向图必经性,by ljh).
- 每个点只可能在一个边双联通分量里,但可以被多个点双共用(因此我们有 rst,这是点双的概念).每条边只能在一个点双 / 边双.
- 点双和边双建立在搜索树上,点双是对于 \(u\),遍历每个 \((u, v)\) 时就根据 \(\operatorname{low}(v)\) 和 \(\operatorname{dfn}(u)\) 的关系决定 \(T(v)\) 与 \(u\) 是否构成点双.而边双是遍历 \(u\) 的所有出边后,检验 \(\operatorname{dfn}(u)\) 和 \(\operatorname{low}(u)\) 的关系,决定 \(T(u)\) 整体是否是一个边双.
- 缩点和边双的代码类似,有区别的地方在于当一个强联通分量确定后,接下来在 tarjan 时应该 忽略已确定的强联通分量中的所有点,这些点的 \(\operatorname{low}(v)\) 并不应该用来更新 \(\operatorname{low}(u)\).一般用
!sccno[v]检查这个点是否应不被忽略. - tarjan 求出的强联通分量编号是拓扑序倒序.2-SAT 中,\(x \to \neg x\),应该取箭头后的,即拓扑序大的,即编号小的优先取.
P11191
https://www.luogu.com.cn/problem/P11191
一个点 \(u\) 可以被释放,当且仅当它存在后继 \(1\),或者存在一个后继 \(v\) 被释放了.
注意到对于一次 \([l, r]\) 的询问,考察满足 \(a_x \in \{a_l, a_{l + 1}, \ldots, a_r\}\) 的点 \(u = a_x\),\(u\) 能否被释放只与 \(l\) 有关而与 \(r\) 无关,且能否被释放关于 \(l\) 有单调性,记 \(f(x)\) 为这个分界点,即 \(l > f(x)\) 时,\(u\) 不能被释放,而 \(l \le f(x)\) 时,\(u\) 可以被释放.那么只需预处理所有的 \(f(x)\),则问题变为检查 \([l, r]\) 内有多少个子区间 \([f(x), x]\),转化为静态二维数点问题.
现在考虑 \(f(x)\) 的求解.令 \(u = a_x\),大致思路应该是:
- 若 \(u\) 存在后继 \(1\),则 \(f(x) = x\).
- 否则,在 \(u\) 的所有后继 \(v\) 中,找到一个对应的 \(f\) 值最大的,作为 \(u\) 的 \(f\) 值(因为只要一个被释放了,自己就有路可走).
这样的思路是粗糙的,因为我们没有说明 \(v\) 对应的 \(f\) 是什么——\(a\) 是一个可重的数列,因此点 \(v\) 可能对应多个下标.我们仔细考虑这个问题.
假设多个 \(y\) 满足 \(a_y = v\).注意到:
- 如果 \(y > x\) 则这样的 \(y\) 无意义.
- 对于所有的 \(y < x\),不妨假设 \(y_1 < y_2 < x\),则一定有 \(f(y_1) \le f(y_2)\)(利用的是 \(f\) 在右端点处的单调性).
- 因此,考虑最后一次 \(v\) 可以被释放的时机,只需要贪心地考虑 \(y_2\) 就够了.也就是 小于 \(x\) 的最后一个满足 \(a_y = v\) 的 \(y\).只需要边扫边处理便可以做到这一点.
现在考虑做法的复杂度,每次对于一个点我们都暴力扫后继,注意到 \(a\) 的可重性使得后继的总数可能超过 \(\Theta(m)\),达到 \(\Theta(n^2)\) 的量级.算法需要优化.
优化的空间在于扫到的后继总数是 \(\Theta(n^2)\) 的,但实际上有大量的扫描中,\(f\) 并没有发生更改.然后我也不知道为啥就考虑根号分治了,可能是对于这种扫度数的点,可以套路地考虑设置一个度数阈值 \(B\),那么度数小于 \(B\) 的点暴力扫复杂度是 \(\Theta(B)\),度数大于 \(B\) 的点不超过 \(\Theta\left(\dfrac m B\right)\) 个.这里有向图度数指的是出度,因为我们扫的是后继.
然后小于 \(B\) 的点就可以直接暴力了.大于 \(B\) 的点我们不再考虑到他那里去扫后继求 max,而是从后继的角度出发,去更新这些大于 \(B\) 的点.这样以来,复杂度就变成了扫描大于 \(B\) 的点的数量,即 \(\Theta\left(\dfrac m B\right)\) 量级.
取 \(B = \sqrt m\),复杂度 \(\Theta(n\sqrt m + q\log n)\).
ABC426F
ABC426F Clearance,区间 \(a_i \gets \max(a_i - k, 0)\),问每次区间总共减了多少.
关键思路:对于每个 \(i\),至多发生一次 \(a_i > 0\) 且 \(a_i - k < 0\).因此特殊处理这些单点不会对总复杂度造成影响.因此只需考虑 \(a_i = 0\) 的元素如何处理.这直接给区间设置一个属性 \(d\),每次区间长度当成 \(r - l + 1 - d\) 做即可.
通常这种问题还需快速判断一个区间内是否有特殊点,从而保证我们不会在没有特殊点的区间上浪费时间.注意到每次询问的 \(k\) 变化,我们想要快速知道区间是否存在 \(0 < a_i < k\),可以维护区间 \(\min\),特别地 \(a_i = 0\) 在区间 \(\min\) 维护中按 \(+\infty\) 处理.
P4219
https://www.luogu.com.cn/problem/P4219 动态加边,求某条边两侧联通块大小.
首先考虑并查集,那么我们发现路径压缩一定不可取,因为它会破坏图的形态只保留图的连通性结构,会让我们无法实时查询每个边两侧联通块大小.
那么启发式合并可以吗?注意到我们每次链接 \((u, v)\) 不可以简单地等价为链接 \((f_u, f_v)\),\(f\) 是祖先.其实一个简单的链就能卡死了,所以不行.
那怎么办???重新考虑一下,首先对于 \((u, v)\) 这条边,我们只需要获取这条边所在的顶点 \(r\),设 \(u\) 是 \(v\) 的父亲,则一边是 \(\operatorname{siz}(v)\),另一边是 \(\operatorname{siz}(r) - \operatorname{siz}(v)\).\(r\) 可以通过简单的并查集得到,那么 \(\operatorname{siz}\) 如何快速更新?
链接 \((u, v)\) 时,仍然假设 \(u\) 是 \(v\) 的父亲,则要更新的 \(\operatorname{siz}\) 其实只有 \(u \rightsquigarrow r\) 这一段(\(r\) 是 \(u\) 的祖先),并且是一个链加的形式.链加,查单点,我们只需要把最后的树先前建好,那么树状数组维护差分即可.
P5795
https://www.luogu.com.cn/problem/P5795
集合 \(S\) 中第 \(k\) 小的数是多少?这种问题一般转化为:集合 \(S\) 中小于等于 \(x\) 的数有几个.设其为 \(f(x)\),则只需要在 \(f\) 上二分 \(f(x_0) = k\) 的点即可,复杂度多一个 \(\log\),但通常 \(f\) 的求解要简单很多.01 trie 或者线段树这种数据结构上,二分可能可以直接通过讨论左右儿子做到,本题就是一个很好的例子.还有一个很好的例子:https://atcoder.jp/contests/abc294/tasks/abc294_f
注意到 \(\Theta(n q \log m)\) 的复杂度可以接受,考虑对 \(Y\) 建立可持久化 01 trie.下面假设 \(S\) 中二进制第 \(k\) 位前的值均一致,现在计算 \(f(2^k - 1)\),即异或结果二进制第 \(k\) 位为 \(0\) 的数量.这只需要讨论 \(X\) 区间中每个数会与 \(Y\) 这个区间上的 01 trie 对应节点异或出多少个 \(0\) 即可.
CCPC Online 2025 C
https://qoj.ac/contest/2534/problem/14549
完全图最小生成树,但是每个点找最小边可以小复杂度做到(本题是 \(\Theta(\log n)\)).
这个魔改 Prim 感觉我自己想不出来只能积累……,考虑堆优化 Prim 存边,堆 \(Q\) 中每个元素存 \((u, v, w)\) 的三元组形式,按边权从小到大排序(堆顶为小边权),同时维护集合 \(S\) 表示已经入树的点.
- 一开始选随意点 \(u\),令 \(S = \{u\}\),找到最小边 \((u, v, w)\) 令其入 \(Q\).
- 然后开始出堆.当一个 \((-w, u, v)\) 被弹出,如果 \(v \in S\) 则忽略,否则我们考虑在树中链接 \((u, v)\),表现为 \(S \gets S \cup \{v\}\),答案累计 \(w\).
- 然后同时考虑从 \(u\) 和 \(v\) 开始再拓展一条最小边入队(注意,最小边的另一个点应当保证不在 \(S\) 中,这可以用一个 set 维护做到).
这个做法为什么是对的?只需要考虑堆 \(Q\) 中的边始终满足:对于所有 \(u \in S\),满足布尔条件 \(B(u)\):可以在 \(Q\) 中找到一个 \((u, v, w)\),使得 \(w\) 是完全图中点 \(u\) 可以连出的,同时满足 \(v \not \in S\) 的,所有的边中边权最小的.
然后我好像不会了……问一下作者看看怎么回复.
P6189
https://www.luogu.com.cn/problem/P6189
\(\Theta(n^{1.5})\) 求 \(n\) 的分拆数.
考虑 \(f(i, j)\) 表示 \(i\) 的分拆数,钦定各项不超过 \(j\);\(g(i, j)\) 表示 \(i\) 的分拆数,钦定项数等于 \(j\).
那么有:
在 \(f\) 的递推中,我们钦定 分拆项单调不减.
- \(f(i - 1, j)\) 表示 \(\max = j\),即最后一项为 \(j\) 的分拆方案数.我们将最后一项撇掉,那么剩下的部分分拆数是 \(f(i - 1, j)\).
- \(f(i, j - 1)\) 表示 \(\max < j\) 的方案数,即最后一项 \(< j\).那其实就是 \(\max \le j - 1\),即 \(f(i, j - 1)\).
在 \(g\) 的递推中,我们钦定 分拆项单调不增.
- \(g(i - 1, j - 1)\) 表示拆分第 \(j\) 项 \(=1\) 的方案数.
- \(g(i - j, j)\) 表示拆分第 \(j\) 项 \(> 1\) 的方案数.考虑将 \(j\) 项拆分向下平移 \(1\)(都减去 \(1\)),即可与 \(g(i - j, j)\) 的拆分方案双射.
对 \(g\) 原地前缀和一下,让 \(g(i, j)\) 变成 \(i\) 的拆分数,钦定项数不超过 \(j\).而 \(f(i, j)\) 定义不变,表示 \(i\) 的分拆数,钦定各项不超过 \(j\).
不难发现,如果每一项都不超过 \(B - 1\),那么项数
P14409
定义长度为 \(n\) 的序列 \(f\) 是好的,当且仅当存在一个长度为 \(n\) 的序列 \(p\),使得 \(f_i\) 恰好等于 \(p\) 中以第 \(i\) 个元素结尾的最长上升子序列长度.容易看出 \(f_i \le i\),因此长度为 \(n\) 的好序列数量有限.
给定一个长度为 \(n - 1\) 的序列 \(b\),求出有多少个长度为 \(n\) 的好序列 \(a\),且删除一个元素后可以得到 \(b\).
\(n \le 10^6\)

浙公网安备 33010602011771号