Loading

杂题笔记

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(i, j) = f(i - 1. j) + f(i, j - 1) \]

\(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(i, j) = g(i - 1, j - 1) + g(i - j, j) \]

\(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\)

posted @ 2025-11-29 16:26  dbxxx  阅读(11)  评论(0)    收藏  举报