算法随笔合集

2025.6.27 ST 表

可以做到 \(O(n\log n)\) 预处理, \(O(1)\) 回答询问.

原理是预处理 \(f_{i,j}\) 维护每个左端点 \(i\) 开始长度为 \(2^j\) 的区间信息,把每个询问区间拆成可能重叠的两个区间来回答. 所以 ST 表使用的前提是查询重复信息不会改变结果,比如最值、\(gcd\) 之类的,比较神秘的有区间按位或和区间按位与.

有几处边界细节需要注意:

  • 预处理层数上界 \(\lfloor\log_2n\rfloor+1\).
  • 预处理保证右端点不超过 \(n\),即 i + (1 << j) - 1 <= n.
  • 对于询问 \(l,r\),层数 \(s=\lfloor\log_2(r-l+1)\rfloor\),则两区间对应 \(f_{l,s}\)\(f_{r-(1 << s) + 1,s}\).

求对数不必预处理,可以用内置函数 __lg().


2025.7.7 欧拉路径,欧拉回路

一个图存在欧拉回路当且仅当:

  • 非零度点互相(强)连通.
  • 点的度数都是偶数(或出度入度).

一个图存在欧拉路径当且仅当:

  • 非零度点互相(强)连通.
  • 恰好有 \(2\) 个奇度点(或恰好存在两点 \(u,v\) 满足一个出度比入度多 \(1\),一个入度比出度多 \(1\)).

2025.7.10 背包

01背包
可滚动数组,必须倒叙枚举容量 \(j\) 否则可能选到多个相同物品. 时间复杂度 \(O(nW)\).

\[f_{i,j}=\max(f_{i-1,j}, f_{i-1, j-w_i}+v_i) \]


完全背包
可滚动数组,必须正序枚举容量 \(j\) 因为可以选多个物品. 时间复杂度 \(O(nW)\).

\[f_{i,j}=\max(f_{i,j}, f_{i, j-w_i}+v_i) \]


多重背包
直接 DP 时间复杂度是 \(O(W\sum k_i)\). 利用二进制分组可以做到 \(O(W\log \sum k_i)\);利用单调队列优化可以做到 \(O(nW)\).

\[f_{i,j}=\max_{k=0}^{k_i}(f_{i-1,j-k\times w_i}+k\times v_i) \]


混合背包
物品之间互不干扰,直接分别 DP 即可.


分组背包
遍历每个组跑 \(01\) 背包即可.


2025.7.11 wqs 二分

ref:https://www.luogu.com.cn/article/hbx1okqa

wqs 二分,也叫凸单调性优化,是一类与函数凹凸性有关的 DP 优化.

假如我们有一个 \(f(x)\),已知其具有凸性(即导函数单调,数学上一般叫凹函数或凸函数),但是它的最值因为某些限制很难求. 此时我们可以引入一个参数更多的函数 \(G(x,k)=f(x)-kx\),如果这个函数在 \(k\) 确定时关于 \(x\) 的极值好算,那么我们就可以间接求出 \(k\) 不同时 \(f(x)\) 的取值. 这时候我们其实并没有考虑 $ x $ 的限制,但是根据凸性,\(x\) 此时也具有单调性,则现在有 $ f(x)=kx+G(x,k) $ 是关于 \(k\) 的单调函数,二分 \(k\) 即可求得 \(f(x)\) 最值.

为什么 \(G(x,k)\) 相比于 \(f(x)\) 会好求?在 OI 中,我们可以理解为加入的 \(k\) 使得我们暂时忽略了某个 \(x\) 的限制,但是 \(x\) 却有关于 \(k\) 单调的性质,而且我们算 \(G(x,k)\) 时能很容易地把此时的 \(x\) 求出来,那么我们就直接把 \(k\) 当未知数进行二分,根据 \(x\) 判断即可. 实际上不必过分关注凸性.

另外 \(G(x,k)\) 的极值点可能不唯一确定,也就是函数图像的极值呈平行于 $ x $ 轴的一条线段,这时候显然应该取两端点中的一个更优. 一般来说求最小值取左端点,求最大值取右端点,但是具体情况还需要具体分析.

综上,当你确定 $ f(x) $ 的凸性,或者直接确定 $ x $ 关于另一个 $ k $ 单调且 \(G(x,k)\) 好求等等性质时可以使用 wqs 二分进行优化.

wqs 二分涉及的思想是主元法,限制很强. 事实上它理应具有很强的可拓展性,即不同的 \(G(x,k)=f(x)+A(x)\) 可能适用于更多的情况.


2025.7.14 决策单调性与(二分)单调队列

决策单调性

决策单调性即决策点具有单调性,通常是下标单调递增.
更一般的,一个二元函数 \(w(x,i)\)\(w(p_i,i)\) 时最优则 \(p_i\) 为决策点,那么对于 \(j<i\),决策单调性可以定义为:

\[\forall j<i,p_j\le p_i \]

要证明某函数 \(w(x,y)\) 满足决策单调性,即证明:

\[f_{p_i} +w(p_i,i) 优于 f_{p_j}+ w(p_j,i),并且f_{p_j}+w(p_j,j)优于f_{p_i}+w(p_i,j) \]

一旦 \(w(x,y)\) 满足决策单调性,我们就可以使用单调队列来维护决策点.


二分队列

二分队列要根据决策单调性来维护队头 \(h\) 和队尾 \(t\),队列里面存三元组 \((l_j,r_j,p_j)\) 表示 \([l_j,r_j]\) 的最优决策是 \(p_j\). 容易发现队列中的所有 \((l_j,r_j)\)\([1,n]\) 划分开. 现在我们要找到 \(i\) 的最优决策点,并且根据 \(i\) 去更新后面的最优决策点.

\(i\) 的最优决策点在之前已经更新过了,所以我们从队头开始,把所有 \(r_h < i\) 的三元组弹出,因为其不以 \(i\) 作为最优决策,根据决策单调性也不会作为 \([i+1,n]\) 的最优决策. 直到 \(i\)\([l_h,r_h]\) 之间,那么我们直接取出 \(p_h\) 来求 \(i\) 的答案.

现在我们考虑怎么找到后面的一个区间以 \(i\) 为最优决策. 根据决策单调性,其一定对应最末的区间,也就是找队尾 \((l_{t_0},n,i)\) 中的 \(l_{t_0}\),这意味着我们可能要先舍去队尾的一些三元组. 我们找出 \(p_t\)\(i\) 决策的转折点 \(x\),满足 \(x'<x\)\(w(p_{t},x')\) 优于 \(w(i,x')\)\(x'>x\)\(w(i,x')\) 优于 \(w(p_{t},x')\). 当 \(l_t\ge x\) 时,\(i\) 会作为 \([l_t,r_t]\) 完整区间的决策点,但是转折点 \(l_{t_0}\) 并不在区间中,所以弹出队尾. 直到第一次出现 \(x>l_t\)\(x\) 就是我们要找的 \(l_{t_0}\),此时将其加入队尾.

实现时实际上不用存三元组中的 \(r\),只需要存一下决策转折点 \(l\) 就足够了.

二分队列与单调队列的区别体现在二分队列是维护的函数交点的单调,而单调队列直接维护的下标单调,少套了一层映射关系. 所以实际应用时要根据题目具体分析.


2025.7.16 FFT

FFT 优化 DFT

考虑将多项式

\[A(x)=a_0+a_1x^1+a_2x^2\cdots+a_{n-1}x^{n-1} \]

按下标奇偶拆成两个次数减半的多项式 \(A^{[0]}(x),A^{[1]}(x)\)

\[A^{[0]}(x)=a_0+a_2x+a_4x^2+\cdots+a_{n-2}x^{{n\over2}-1}\\ A^{[1]}(x)=a_1+a_3x+a_5x^2+\cdots+a_{n-1}x^{{n\over2}-1}\\ \]

就可以得到下式:

\[A(x)=A^{[0]}(x^2)+xA^{[1]}(x^2) \]

那我们求 \(A(x)\)\(n\) 个点值,就转换成分别求 \(A^{[0]}(x^2),A^{[1]}(x^2)\)\({n\over 2}\) 个点值,再根据上式进行合并即可.

这样直接求仍然是 \(O(n)\) 的,但是这个 \(x\rightarrow x^2\) 的转化启发我们用折半引理的结论对数据规模进行简化.

于是考虑求

\[\omega_n^0,\omega_n^1,\omega_n^2,\cdots,\omega_n^{n-1} \]

处的点值,每次平方数据规模都可以减半,就可以做到只求 \(O(\log n)\) 次而不是 \(O(n)\) 次. 具体的:

\[A(\omega_n^i)=A^{[0]}(\omega_n^{2i})+\omega_n^iA^{[1]}(\omega_n^{2i})\\ A(\omega_n^{i+{n\over2}})=A^{[0]}(\omega_n^{2i+n})+\omega_n^{i+{n\over 2}}A^{[1]}(\omega_n^{2i+n}) \]

其中下式可以进行化简:

\[\begin{aligned} A(\omega_n^{i+{n\over2}})&=A^{[0]}(\omega_n^{2i+n})+\omega_n^{i+{n\over 2}}A^{[1]}(\omega_n^{2i+n})\\ &=A^{[0]}(\omega_n^{2i}\cdot w_n^n)+\omega_n^{i}\cdot w_n^{n\over2}A^{[1]}(\omega_n^{2i}\cdot w_n^n)\\ &=A^{[0]}(\omega_n^{2i})-\omega_n^iA^{[1]}(\omega_n^{2i}) \end{aligned} \]

即有:

\[\begin{aligned} A(\omega_n^i)=A^{[0]}(\omega_n^{2i})+\omega_n^iA^{[1]}(\omega_n^{2i})\\ A(\omega_n^{i+{n\over2}})=A^{[0]}(\omega_n^{2i})-\omega_n^iA^{[1]}(\omega_n^{2i}) \end{aligned} \]

所以每次数据计算数据规模都可以翻倍,只用求 \(O(\log n)\) 次点值,DFT 的时间复杂度就来到了 \(O(n\log n)\).


FFT 优化 IDFT

IDFT 是 DFT 的逆变换.

现在我们已经将系数表达式转换为点值表达式,并且计算出卷积的点值,考虑怎么快速通过点值还原出系数表达式. 由于单位根具有周期性,不妨带入 \(-\omega_n^k\) 试着计算一下. 具体的,设对 \(A(x)\) 进行 DFT 之后得到的点值表示为

\[y_0,y_1,y_2,\cdots,y_{n-1} \]

构造多项式

\[F(x)=y_0+y_1x^1+y_2x^2+\cdots+y_{n-1}x^{n-1} \]

带入点值

\[-\omega_n^0,-\omega_n^1,-\omega_n^2,\cdots,-\omega_n^{n-1} \]

计算一番

\[\begin{aligned} F(-\omega_n^k)&=\sum_{i=0}^{n-1}y_i\omega_{n}^{-ki}\\ &=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j\left(\omega_n^i\right)^j\omega_{n}^{-ki}\\ &=\sum_{j=0}^{n-1}a_j\sum_{i=0}^{n-1}\left(\omega_n^{j-k}\right)^i\\ \end{aligned} \]

根据求和引理,仅当 \(j-k=0\) 时右式值为 \(n\),其余项全为 \(0\). 所以最后得到:

\[F(-\omega_n^k)=a_k\cdot n \]

我们只需要仿照 DFT,带入相反的点值,得到的结果再除以 \(n\) 就可以在同样 \(O(n\log n)\) 的时间复杂度把点值表达式还原成系数表达式了.


蝴蝶变换

上述算法可以使用递归分治简单实现,但是由于涉及三角函数、虚数、递归等等常数很大. 考虑寻找下标变换的规律:

\[\{x_0,x_1,x_2,x_3,x_4,x_5,x_6,x_7\}\\ \{x_0,x_2,x_4,x_6\},\{x_1,x_3,x_5,x_7\}\\ \{x_0,x_4\},\{x_2,x_6\},\{x_1,x_5\},\{x_3,x_7\}\\ \{x_0\},\{x_4\},\{x_2\},\{x_6\},\{x_1\},\{x_5\},\{x_3\},\{x_7\}\\ \]

由于每次每组的都按照组内序号的奇偶性再分组,可以观察到:第 \(i\) 轮分组从低到高第 \(i\) 位为 \(0\) 的会被分到左边的组,为 \(1\) 的会被分到右边的组.

可以发现,若有 \(2^m\) 个数进行递归分治,下标为 \(2^{m-1}\) 的最后总是会排到下标为 \(1\) 的位置,下标为 \(2^{m-2}\) 的最后总是会排到 \(2\) 的位置. 一个事实是每个下标最终的位置是最初下标的二进制位对称翻转得到的,也就是 \(m\) 位二进制下标经过变换会得到

\[(b_mb_{m-1}b_{m-2}\cdots b_1)_2\rightarrow (b_1b_{2}b_{3}\cdots b_m)_2 \]

所以我们可以预处理变换数组,会大大减小代码的常数.


2025.7.17 生成函数:ODF 与 EGF

普通生成函数 OGF

我们定义序列 \(<a_0,a_1,a_2\cdots>\)普通生成函数 \(A(x)\) 为形式幂级数

\[A(x)=\sum_{n\ge0}a_nx^n \]

同时 \(A(x)\)系数 \(a_n\) 写作

\[a_n=[x^n]F(x) \]

依据定义,\(A(x)\) 实际上是一个有无穷项的多项式,而 \(a\) 既可以是又穷序列也可以是无穷序列.

基本运算

依据多项式的基本运算,对于 OGF \(A(x),B(x)\) 也有

\[A(x)\pm B(x)=\sum_{n\ge0}a_nx^n\pm \sum_{n\ge0}b_nx^n =\sum_{n\ge0}(a_n\pm b_n)x^n \]

\[A(x) B(x)=\sum_{n\ge0}\sum_{i+j=n}a_ib_jx^n=\sum_{n\ge0}\sum_{i=0}^na_ib_{n-i}x^n \]

其中生成函数 \(A(x),B(x)\)卷积定义就定义为 \(A(x)B(x)\)。卷积有着非常重要的组合意义,我们将在下文进一步探讨.

OGF 系数的左移和右移直接对应下标的左移右移,也就是除以或乘以一个 \(x\).

封闭形式

生成函数的一大特点就是其通常具有形式简单的封闭形式,这样可以使化简变得更加轻松.

一般地,生成函数的封闭形式就是利用其系数和项数无穷的性质把其转换为有限项的恒等形式。譬如 \(<1,1,1,\cdots>\) 的普通生成函数 \(F(x)\) 有封闭形式

\[F(x)=\sum_{n\ge0}x^n={1\over 1-x} \]

具体的推导我们发现 \(F(x)x\) 对应序列 \(<0,1,1,\cdots>\),所以有

\[xF(x)+1=F(x)\iff F(x)={1\over1-x} \]

类似地,等比数列 \(<1,p,p^2,\cdots>\) 的生成函数 \(F(x)\) 有封闭形式

\[F(x)=\sum_{n\ge0}p^nx^n=F(x)px+1\iff F(x)={1\over1-px} \]

值得一提的是,由于封闭形式与形式幂级数是恒等的,所以一些对于形式幂级数的操作也可以很直观地反映在封闭形式上,遇到时要灵活应对,如

\[c\cdot F(x)=\sum_{n\ge0}cp^nx^n={c\over1-px} \]

提取系数

对于已知的封闭形式,我们可以尝试提取系数来反向得到序列.
我们将以斐波那契数列的 OGF 为例来介绍提取系数的方法之一——待定系数法.

考虑斐波那契数列 \(a_0=0,a_1=1,a_n=a_{n-1}+a_{n-2}(n>1)\),观察到

\[\begin{aligned} F(x)&=a_0x^0+a_1x^1+a_2x^2+a_3x^3+a_4x^4+\cdots\\ xF(x)&=\ \ \ \ \ \ \ \ +a_0x^1+a_1x^2+a_2x^3+a_3x^4+\cdots\\ x^2F(x)&=\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ +a_0x^2+a_1x^3+a_2x^4+\cdots\\ \end{aligned} \]

根据递推式,发现补上 \(xF(x),x^2F(x)\) 的前面几项就可以相加直接得到 \(F(x)\). 所以 \(F(x)\) 有封闭形式

\[xF(x)+x^2F(x)-a_0+a_1x+a_0=F(x)\iff F(x)={x\over1-x-x^2} \]

考虑用一定的形式来设出系数,比如

\[{A\over1-ax}+{B\over1-bx}={x\over1-x-x^2} \]

注意两边必须是齐次的.
接下来通分、根据系数恒等列出方程、求解方程,得到

\[{A-Abx+B-aBx\over(1-ax)(1-bx)}={x\over1-x-x^2} \]

\[\begin{cases} A+B=0\\ -Ab-aB=1\\ a+b=1\\ ab=-1 \end{cases} \]

\[\begin{cases} A={1\over\sqrt5}\\ B=-{1\over\sqrt5}\\ a={1+\sqrt5\over2}\\ b={1-\sqrt5\over2} \end{cases} \]

上面我们假设的封闭形式是等比数列之和,所以我们就可以带入得到

\[F(x)={x\over1-x-x^2}=\sum_{n\ge0}{1\over\sqrt5}\Big(\Big({1+\sqrt5\over2}\Big)^n-\Big({1-\sqrt5\over2}\Big)^n\Big) \]

我们就得到了斐波那契数列的通项公式.

组合意义

OGF 卷积的组合意义是把物品组合起来,可以用来解决无标号的计数问题.


指数生成函数 EGF

定义

一个序列 \(a_i\) 的指数生成函数定义为 \(\sum \limits_{i\ge0}{a_i\over i!}x^i\),也就是在 OGF 的基础上除以了 \(i!\) .

常见级数与基本运算

序列 \(\langle 1,1,\cdots,1\rangle\) 的指数生成函数是

\[\sum\limits_{i\ge0}{x^i\over i!} \]

会发现这其实是 \(e^x\) 的泰勒展开(这也是名称中“指数”的由来).

还有一些常见的序列的封闭形式:

\[\langle1,-1,1,-1\cdots\rangle\Rightarrow e^{-x}\\ \langle c^0,c^1,c^2,\cdots\rangle\Rightarrow e^{cx}\\ \langle1,0,1,0,1\cdots\rangle\Rightarrow {e^x+e^{-x}\over2}\\ \langle1,a^{\underline 1},a^{\underline 2},a^{\underline 3},\cdots\rangle\Rightarrow {(1+x)^a}\\ \]

EGF 的卷积,根据定义展开可得:

\[A(x)\cdot B(x)=\sum_{n\ge0}\sum_{i=0}^n{n\choose i}a_ib_{n-i}x^n \]

而 EGF 系数的左移,右移分别对应多项式求导,积分. 根据定义式易得,在这里留给读者自己证明.

组合意义

EGF 卷积的组合意义是物品的排列,可以解决有标号的计数问题.

而对组合对象(\(x,F(x)\) 等等)求 EGF,根据上文,展开后就是对组合对象求 exp. 所以我们也可以说 exp 的组合意义是有标号物品的排列.


2025.7.18 线段树历史最值、区间取最值操作

区间历史最值操作

可以先考虑历史最大值怎么做. 类似于线段树板子的,我们希望用某种 \(tag\) 来对其进行维护.

那么考虑历史最大值什么时候会发生变化. 现在我们已经维护了区间加法标记,随着加法操作不断进行,设现在的加法标记 \(t_1=k\),发现:

  • \(k>0\),历史最大值会增加.
  • \(k\le0\),历史最大值不会改变.

然而当我们查询时,历史最大值实际上是上次查询之后的 \(\max\{k\}\). 也就是说我们需要额外维护一个 \(t_2\) 来存 \(t_1\) 的最大值. 下传时,将 \(t_2\) 更新给历史最值即可. 这就是区间历史最值的维护.


区间最值操作

注意:这个操作涉及到一些均摊、势能分析技巧. 如果没听说过就感性理解.

这个操作最困难的点就在于它作用于很多值不同的数上,每种值的变化量不同,每种值受到作用的数还不止一个,并且限定了作用范围.

于是我们不妨来思考更加简易的操作:

  • 如果区间取 \(\min\) 只对最大值生效怎么做?那么区间中的每个最大值都减去了一个确定的数,我们只用维护区间最大值与区间最大值的个数即可.

  • 只对最大值生效的条件是什么?我们不妨设区间对 \(x\)\(\min\),那么一个充要的条件则是 \(x\) 小于区间最大值,大于区间严格次大值. 区间次大值的维护也并不困难,但是 \(x\) 如果不直接满足这个条件应该怎么做呢?一个不难证明的性质是随着线段树向下递归,最大值和次大值是单调不增的,所以查询时向下递归同样也是单调不增的. 所以我们就可以在符合条件时向下递归,直到 \(x\) 满足上述条件就可以直接根据最大值的个数来进行修改了.

但是这样看起来十分的暴力,为什么能够保证复杂度正确呢?根据势能分析,单独的区间取最值按照上面的方法是均摊 \(O(n\log n)\) 的,而区间取最值同时维护区间加的均摊复杂度为 \(O(n\log^2 n)\),非常的优秀. 证明用到的一些关键性质是最值个数单调不增以及线段树高 \(O(\log n)\). 具体证明详见吉如一老师的集训队论文.

具体实现涉及大量分讨,但是根据定义还是不难理解的,多写写熟练就好.


2025.7.21 李超线段树

线段树上维护若干一次函数在线段树每个区间中点处的最值(有点像凸壳),支持直线(线段)插入. 直线插入时间复杂度 \(O(\log n)\),是在线段树上搜索的复杂度. 线段插入时间复杂度 \(O(\log^2 n)\),因为要拆分成 \(O(\log n)\) 个区间分别递归.

考虑加入一条直线 \(f(x)\),设线段树上每个区间维护的线段并起来为 \(g(x)\). 首先考察整个区间中点 \(mid\) 哪个函数更优,分类讨论:

  • \(f(x)\) 更优,将其与这一段的 \(g(x)\) 交换.

此后进行一次判断,若区间 \(l=r\) 不再能二分就结束递归,否则继续判断:

  • 若左端点 \(f(x)\) 更优,说明左区间存在可能更新的 \(g(x)\),所以递归到左儿子继续更新.
  • 若右端点 \(f(x)\) 更优,说明右区间存在可能更新的 \(g(x)\),所以递归到右儿子继续更新.

上述算法用于维护一条直线的插入. 如果插入一条线段,则需要像线段树区间修改操作那样分出每个区间分别进行上述操作.

核心代码:

struct lct{
	int tr[maxn];
	inline ld f(int x, int id) {return (ld)l[id].k * x + l[id].b;}
	inline bool eq(ld x, ld y) {return x - y <= eps && y - x <= eps;}
	inline bool cmp(int u, int v, int x) {//u 是否比 v 优 
		ld a = f(x, u), b = f(x, v);
		return a > b || (eq(a, b) && u < v);
	}
	inline void upd(int u, int l, int r, int p) {
		int mid = l + r >> 1;
		if(cmp(p, tr[u], mid)) swap(tr[u], p);//递归修改 
		if(l == r) return;
		if(cmp(p, tr[u], l)) upd(ls, l, mid, p);
		if(cmp(p, tr[u], r)) upd(rs, mid + 1, r, p);
		return;
	} 
	inline void add(int u, int l, int r, int ql, int qr, int p) {
		if(ql <= l && r <= qr) return upd(u, l, r, p), void(0);
		int mid = l + r >> 1;
		if(ql <= mid) add(ls, l, mid, ql, qr, p);
		if(mid < qr) add(rs, mid + 1, r, ql, qr, p);
		return;
	}
	inline int ask(int u, int l, int r, int p) {
		if(l == r) return tr[u];
		int mid = l + r >> 1, res = tr[u], tmp;
		if(p <= mid) tmp = ask(ls, l, mid, p);
		else tmp = ask(rs, mid + 1, r, p);
		return (cmp(tmp, res, p) ? tmp : res);
	}
} t;

2025.7.22 长链剖分

最好有重链剖分的基础再来学这个,否则你应当先学习重剖.

类似于重链剖分以子树大小最大的作为重儿子,长链剖分以儿子中拥有最长链的为重儿子. 所以我们仿照重剖,可以写出两个核心 dfs 函数.

int len[maxn], son[maxn], fa[maxn];
inline void dfs1(int u, int f) {
	fa[u] = f, len[u] = 1;
	for(int i = head[u], v; i; i = e[i].nxt) {
		v = e[i].v; if(v == f) continue;
		dfs1(v, u);
		if(len[v] + 1 > len[u]) len[u] = len[v] + 1, son[u] = v;
	} return;
}
int tp[maxn];
inline void dfs2(int u, int t) {
	tp[u] = t; if(son[u]) dfs2(son[u], t);
	for(int i = head[u], v; i; i = e[i].nxt) {
		v = e[i].v; if(v == fa[u] || v == son[u]) continue;
		dfs2(v, v);
	} return;
}

上述代码维护了 len[u] 表示以 \(u\) 起始的链的长度.

那么长链剖分有什么用呢?我们来看看它的一些性质.

  • 所有长链长度之和是 \(O(n)\) 的. 该命题相当于所有长链无交,根据定义这是恒成立的.

  • 任意节点 \(u\)\(k\) 级祖先所在的长链长度一定大于等于 \(k\).\(u\)\(k\) 级祖先是 \(v\),分类讨论. 如果 \(u\)\(v\) 在同一条长链显然成立;如果 \(u\)\(v\) 不在同一条长链上,假设 \(v\) 所在长链长度小于 \(k\),但是 \(v\rightarrow u\) 这条链长度已经为 \(k\),所以长链应该延申到 \(u\),这与 \(u,v\) 不在同一条长链相矛盾,故假设不成立.

  • 任意节点 \(u\) 可以经过 \(O(\sqrt n)\) 条轻边到达根节点. 该命题相当于跳 \(O(\sqrt n)\) 次长链顶可以到达根节点. 根据第一条性质,我们会发现向上跳长链的长度是严格单调递增的,否则一定可以通过调整得到更长的链作为长链. 故跳的长链长度之和至少是 \(1+2+\cdots=n\),会发现长链条数是 \(O(\sqrt n)\) 的.


2025.7.25 kmp 与 manacher

KMP

可以 \(O(n)\) 求字符串每个前缀的最长前后缀.

border

字符串一对相等的真前缀和真后缀叫做 border.

KMP 求的其实就是每个前缀的最长 border.

实现

记位置 \(i\) 的最长 border 为 \(fail_i\).

考虑双指针维护,\(j\) 记录前缀的匹配,\(i\) 记录后缀的匹配. 我们在之前 \(j\) 匹配 \(i-1\) 的基础上,要找到一个 \(j\) 匹配 \(i\). 对于每个 \(i\),如果 \(s_i\)\(s_{j+1}\) 不等,那么 \(j\) 就要跳到一个 \(s_{j'}=s_{i-1}\) 的最大位置,这样一定优. 然后我们会发现这个 \(j'\) 就是 \(fail_j\) 的定义,所以直接不断地令 \(j=fail_j\) 重复上面的判断即可.

其实既可以求字符串自己与自己的最长 border 也可以求两个串的最长 border,区别只是后者额外再匹配一遍另一个串.

for(int i = 2, j = 0; i <= m; i++) {// fail[1] 无价值 
	while(j && s2[j + 1] != s2[i]) j = fail[j];
	if(s2[j + 1] == s2[i]) j++;
	fail[i] = j; 
}
for(int i = 1, j = 0; i <= n; i++) {
	while(j && s2[j + 1] != s1[i]) j = fail[j];
	if(s1[i] == s2[j + 1]) j++;
	if(j == m) cout << i - j + 1 << endl, j = fail[j];
}

注意 \(fail_1=1\) 是无价值的,而且这样会让程序陷入死循环.

时间复杂度

挪动 \(i\) 显然是线性的,但为什么 \(j\) 跳的次数是 \(O(n)\) 的?

其实因为 \(j\) 跳一次 \(fail_j\) 至少减少 \(1\),但是一次匹配上最多增加 \(1\),这就保证了 \(j\) 一定不会跳超过 \(O(n)\) 次. 所以总复杂度就是 \(O(n)\) 的.


Manacher

可以 \(O(n)\) 求所有回文子串的不同位置.

思路和 Z 函数几乎一样,是拿之前的回文信息来更新后面的回文信息.

特殊处理

回文串有奇回文和偶回文两种,也就是对称点可能是字符也可能是字符之间的空隙. 如果分类讨论的话非常地不优美,所以我们有一个聪明的解决办法:

  • 在每个字符之间包括头尾插入一个特殊字符如 #
  • 最开头插入另一个特殊字符如 ~

这样既可以避免越界也可以让所有最长回文串都只能是奇回文. 而且原串最长回文子串长度就是新串最长回文半径长度减去 \(1\).

s[0] = '~', s[1] = '#';
for(int i = 1; i <= n; i++) s[++tot] = c[i], s[++tot] = '#';

实现

考虑维护每个节点可以拓展的最大半径 \(d_i\),维护最远的 \((mid,r)\) 使得令 \(l=2\times mid-r\),有 \(s_{l\cdots r}\) 是一个回文中心为 \(mid\) 的最长回文串. 回文串对称的性质告诉我们,你要求一个 \(i\in[mid,r]\)\(d_i\),可以在这个极长回文串的对称位置的 \(d_{l}\) 基础上更新来. 具体的,与 Z 函数类似有:

  • \(i \in [mid,r]\)
  • \(d_{l}\le r-i+1\)

符合上述要求就可以直接 \(d_i=d_l\) 了. 如果不行就在 \(d_i=\max(1,r-i+1)\) 基础上暴力往两边拓展,并更新 \((mid,r)\) 就好了.

for(int i = 1, mid = 0, r = 0; i <= tot; i++) {
	if(i <= r && d[2 * mid - i] < r - i + 1) d[i] = d[2 * mid - i];
	else {
		d[i] = max(1, r - i + 1);
		while(s[i + d[i]] == s[i - d[i]]) d[i]++;
		if(i + d[i] > r) r = i + d[i] - 1, mid = i;
	} 
}

2025.8.18 差分约束

\(n\) 组形如 \(x_i+w\ge x_j\) 的不等关系求最大/最小解或报告无解,考虑用最短路的三角不等式 \(dis_u+w\ge dis_v\) (最终状态) 来类比刻画.

考虑 \(u\rightarrow v\) 属于是最短路当且仅当 \(dis_u+w\ge dis_v\),于是 \(x_i+w\ge x_j\) 就应该连 \(i\rightarrow j\) 边权为 \(w\) 的有向边. 加上虚点向各点连边权为 \(0\) 的边,跑全源最短路,原图存在负环则无解. 可以证明,这样跑最短路得到的 \(dis_i\)\(x_i\) 的合法最大解:考虑记任意一条从起点出发到 \(i\) 的路径边权和为 \(S\),根据边的定义则一定有 \(x_i<S\),所以 \(x_i\) 的上界就是 \(S\) 的最小值,即 \(dis_i\).

类似的对于形如 \(x_i+w\le x_j\) 的不等关系可以通过连 \(i\rightarrow j\),边权为 \(w\) 的边跑最长路得到 \(x_i\) 的下界.

一些常用的转化连边:

  • \(x_i-x_j\le k\),即 \(x_j+k\ge x_i\),连边 \(j\rightarrow i\),边权 \(k\).
  • \(x_i-x_j\ge k\),即 \(x_j-k\ge x_i\),连边 \(i\rightarrow j\),边权 \(-k\).
  • \(x_i-x_j= k\),即 \(x_j-x_i\le k\wedge x_i-x_j\ge k\),连边 \(j\rightarrow i\),边权 \(k\)\(i\rightarrow j\),边权 \(-k\).
posted @ 2025-07-11 15:19  Ydoc770  阅读(18)  评论(1)    收藏  举报