春季知识总结
春季知识总结
Burnside 引理 & Pólya 计数
主要用来解决染色相关的同构计数问题。
Burnside 引理
轨道个数
\(X^g\) 表示 \(g\) 作用下的不动点集合。
这个东西过于的晦涩难懂。用通俗一点的语言说就是:找出所有本质不同的操作方案,将操作抽象成图,拆分成若干环,然后求出不动点的数量,最后将所有操作方案的不动点数量相加除以操作方案数就是答案。因为群论相关的推导过程极少考察所以不过多赘述。
Pólya 计数
应用范围很窄。如果除颜色外没有其他限制,由 Burnside 引理可推出更简单的式子:
\(c(g)\) 表示 \(g\) 分解的轮换数量。
这里相当于就是对于每一个环只能染同一种颜色,所以每种操作方案的不动点数量就是颜色数的环的个数次方。
例
UVA10601 Cubes
典型的空间几何体同构计数问题。可以发现只需要如向下转、向左转两种转法的线性组合就可以表示出所有旋转方案。然后就可以根据此爆搜出所有本质不同的旋转方案,接下来的问题就是对于每种方案的每个环都用相同颜色染色,并且每种颜色的数量给定。如果直接 DP 时间复杂度较高,可以发现事实上只需要简单的组合就可算出方案数,最后再除以本质不同的旋转方案数 \(24\) 即可。
[SHOI2006] 有色图
较难的图上同构计数问题。考虑通过一种点的操作方式得到了一个环长的序列 \(a\),现在考虑有多少种边的等价类。那么对于顶点在同一个环 \(i\) 上的边来说,它会贡献 \(\lfloor \frac{a_i}{2} \rfloor\) 个等价类,对于顶点在两个不同环 \(i,j\) 上的边,它会贡献 \(\frac{a_i a_j}{\operatorname{lcm}(a_i,a_j)} = \gcd(a_i,a_j)\) 个等价类。将其加起来就可以得到边的等价类数量,他的不动点数量就是颜色数 \(m\) 的等价类数量次方。
然后由于 \(n\) 很大,显然不能枚举排列再计算环长序列,所以考虑直接枚举环长序列。可以发现 \(\sum\limits_{i} a_i = n\),所以不同的环长序列的数量就是 \(P(n)\),即 \(n\) 的自然数拆分方案数。注意到当 \(n \leq 53\) 时,\(P(n) = O(3 \times 10^5)\),所以直接爆搜枚举是可以的。接下来考虑有多少种排列的环长序列为 \(a\)。首先直接排列方案数是 \(n!\),然后对于每个环上计数都是圆排列,不关心圆上的起点编号,所以要除以每个环的大小 \(a_i\),接着可以发现对于相同的环长我们其实并不关心他们之间的相对顺序,所以还要对于每种环长除以环长为它的环的数量之阶乘,所以方案数就是 \(\frac{n!}{\prod\limits_{i} a_i \prod\limits_{i} c_i!}\)。最后求出不动点的数量和后还要除以操作方案数 \(n!\)。
总结
这种计数问题主要就分两种。一种就是空间几何体同构计数问题,这种问题可能难在将所有操作方案不重不漏的找出来,然后再对于每种方案去将其分解成环,然后染色方面的问题是相对简单的。还有一种就是图上同构计数问题,这种一般找操作方案和分解环的时候不会特别难,难点就在于求出不动点的数量,这种东西很多情况下都要用到组合计数。所以说很多情况下像 Burnside 之类的东西只是一层皮,扒开这层皮后就会转化为一些较为常规的计数问题。
WQS 二分(Aliens' Trick)
主要用于优化某些有段数限制的分段 DP。形式化的,对于有下列状态转移方程的 DP:
若对于分的段数 \(i\) 来说,\(f_{n,i}\) 是关于 \(i\) 的凸函数,那么可以采用 WQS 二分来优化,去掉组数限制。WQS 二分的过程相当于就是去二分一个斜率,然后用这个斜率去切由 \(f_{n,i}\) 构成的凸壳,当切到的点的最优段数就是要求的段数 \(m\) 时就停止,否则继续二分调整。通俗点讲就是给状态转移里面加了一个权重,然后看什么时候分的段数就是要求的。这里要注意的是当有多个点斜率相同时,需要取段数最小/最大的点,不然可能会出现一些问题。二分后没了段数限制问题就变成了不限段数的分段 DP:
这里的 \(\Delta\) 就是二分的权重。最后答案就是最终的 \(\Delta\) 对应的 DP 值再加上 \(m \times \Delta\)。
结论
若代价函数 \(w(l,r)\) 满足四边形不等式,则 \(f_{n,i}\) 关于 \(i\) 是凸函数。
证明比较困难。如果发现某分段 DP 的代价函数满足四边形不等式就可以 WQS 二分优化。
还有就是若原问题能建立费用流模型来求解就可以证明 DP 值有凸性,因为费用流的代价函数是凸函数。
例
[IOI 2016] aliens
该技巧国外名称的起源。
我们可以把正方形翻折一下变成等腰直角三角形。然后可以发现我们可以将需要覆盖的点的点坐标 \((x,y)\) 映射到一条线段 \([\min(x,y),\max(x,y)]\) 上,表示为要覆盖这个点就要在线段上覆盖这条线段,这样原问题就转化为了用不超过 \(k\) 条线段覆盖所有要求线段,使得这些线段长度的平方减去交集的长度的平方最小。我们可以发现如果一条线段被另一条更长的线段包含,那么这条线段是没有用的,所以可以直接将这种线段删去,排序之后可以得到左右端点都递增的线段序列。然后现在可以发现这个不超过 \(k\) 条线段其实没有用,因为将两条线段分开覆盖是比一起覆盖的代价要小的(大概原因是 \(\forall a,b \in Z^+,a^2 + b^2 < (a+b)^2\)),所以我们会直接使用刚好 \(k\) 条线段。二分之后就是相对简单的一个无段数限制分段 DP,发现可以用斜率优化,时间复杂度是 \(O(n \log V)\) 的。
总结
在写出有组数限制的 DP 方程后,若发现有凸性即可考虑采用 WQS 二分优化。关于如何发现凸性,在自己练习时最好还是自己尝试证一下,如果是考试的话可以考虑打表找规律,若对于打出的所有数据都有凸性就可以尝试 WQS 二分优化。感觉难点还是得先写出 DP 状态和转移方程,这样才有后面的优化之说。
字符串相关
这一部分相对来说比较重要。
PAM(回文自动机)
既然是自动机,那就有失配指针 \(fail\) 和转移边 \(son\),其中 \(fail_i\) 表示编号为 \(i\) 的回文串的最长回文后缀的编号,\(son_{i,j}\) 表示编号为 \(i\) 的回文串在两边都加上字符 \(j\) 后的回文串编号。我们再记录一个 \(len_i\) 表示编号为 \(i\) 的回文串的长度。接下来我们来考虑增量式构造 PAM。
对于每一个新加的字符 \(s_i\),我们都将在 \(i-1\) 时所在的回文串编号 \(last\) 记录下来,然后去找 \(last\) 的祖先链上第一个满足 \(s_{i - len_p - 1} = s_i\) 的点。若 \(p\) 已有出边为 \(s_i\) 的儿子就跳过,否则新建一个点 \(cur\),然后 \(len_{cur} \leftarrow len_p + 2,son_{p,s_i} \leftarrow cur\),接着再在 \(fail_p\) 的祖先链上找第一个满足 \(s_{i - len_q - 1} = s_i\) 的点,将 \(fail_{cur} \leftarrow son_{q,s_i}\)(注意此处 \(fail_{cur}\) 不能等于 \(cur\)),然后 \(last = son_{p,s_i}\)。这样我们就完成了构造,时间复杂度可以通过势能分析发现为 \(O(n)\),空间复杂度为 \(O(n | \Sigma |)\),其中 \(| \Sigma |\) 为字符集大小。通过此种构造方式,我们可以在线求出以每个字符结尾的回文串的数量,还能证明一个字符串中本质不同的回文串数量严格不超过 \(n\)(最多就加 \(n\) 个点)。
关于一些 PAM 的合并,可以考虑启发式合并。由于方向不确定,所以我们要同时记录一个字符串从最左边开始的 \(last\) 指针和以最右边结束的 \(last\) 指针。在合并的时候,对于插入的方向就直接按照 PAM 的方法去插入,并且更新同向的 \(last\) 指针,对于异向的 \(last\) 指针,可以发现只有插入字符后整个串是回文串 \(last\) 指针才可能会变,所以异向的指针更新是 \(O(1)\) 的。这样我们就在时间复杂度 \(O(n \log n)\) 的情况下解决了此问题。
总结
以前的 Manacher 算法只能证明一个字符串本质不同的回文串数量为 \(O(n)\),但通过 PAM 的构造过程我们可以证明一个字符串中本质不同的回文串数量严格不超过 \(n\)。但是两种算法各有优劣,Manacher 的优点在于更加小巧灵活,缺点是使用起来可能不是非常方便,PAM 更加简单粗暴,便于使用,但是可能就没有那么灵活。
SA(后缀数组)
对于一个长度为 \(n\) 的字符串,记 \(s[i:]\) 表示以 \(i\) 开头的后缀。将所有后缀按照字典序排序(空字符小于任何其他字符),记 \(sa_i\) 表示排名为 \(i\) 的后缀在原串中对应的起始位置,\(rk_i\) 表示起始位置为 \(i\) 的后缀在排序后的排名。通过定义可以发现 \(sa_{rk_i} = rk_{sa_i} = i\)。
那么如何快速求解后缀数组便成为了一个问题。
sort + 哈希
我们考虑使用 sort 来帮助我们排序。我们发现我们会涉及到比较两个后缀的字典序大小,这里可以直接使用哈希 + 二分的方法来比较,单词比较复杂度为 \(O(\log n)\),排序会比较 \(O(n \log n)\) 次,总的复杂度就是 \(O(n \log^2 n)\)。但是这种方法复杂度较高,而且常数很大,还要面临卡哈希的风险,所以不实用。
倍增
我们考虑对于两个后缀 \(s[i:],s[j:]\),其实我们不需要直接把这两个后缀全部拿来比较。我们可以先划定一个长度 \(k\),先比较 \(s[i,i+k-1]\) 和 \(s[j,j+k-1]\) 的大小,如果能直接比出来就可以得到两个后缀的大小关系,比不出来再去比后面的部分。
由此我们可以设计一种倍增做法:总共 \(O(\log n)\) 轮,对于某一轮来说,假设我们上一步设置的比较长度为 \(2^w\),那么就可以在此基础上推出比较长度为 \(2^{w+1}\) 情况下的后缀数组。每一轮我们都使用 sort 进行双关键字,第一关键字为 \(rk_{i}\),第二关键字为 \(rk_{i + 2^w}\),这样就可以得出来新一轮的 \(sa\),然后在此基础上再 \(O(n)\) 推一下新的 \(rk\) 即可。这样时间复杂度为 \(O(n \log^2 n)\),常数会小一些,而且不会错。
但是这样还是有点慢,我们考虑进行一些优化。
-
我们发现其实 \(rk\) 数组的值域是 \(O(n)\) 的,所以可以考虑基数排序,先对第二关键字计数排序,再对第一关键字计数排序即可。但是这样的常数非常大,考虑优化。我们发现第二关键字其实是不需要重新排序的,因为在上一轮我们已将将所有的后缀按照比较长度 \(2^w\) 排好序了,所以第二关键字排序时很多都是可以直接复用上一轮的 \(sa\) 的。但此时我们要考虑一下可能对于某些 \(i\),\(i + 2^w - 1 > n\),这种情况下它们的第二关键字显然是最小的,所以要先将起始下标为 \(n - 2^w + 1 \sim n\) 的后缀放到前面去,再按照 \(sa\) 中的顺序把剩下的加进来。这样我们就简单的完成了第二关键字排序,然后再对第一关键字计数排序即可。这样时间复杂度就是 \(O(n \log n)\) 的,而且常数也比较小。
-
在数据比较随机的情况下,其实我们不需要多少轮排序就能完成,就是说在比较长度很小的时候我们就已经比较完了。所以每一轮结束后我们都记录一下不同的 \(rk\) 的数量,如果已经等于 \(n\) 就可以直接退出。这样很多情况下都会跑得很快。
求解完 \(sa\) 和 \(rk\) 后,我们要引入一个比较重要的东西。
\(height\) 数组
我们定义 \(height_i = lcp(sa_{i-1},sa_i)\),其中特定 \(height_1 = 0\)。就是排名相邻的两个后缀的 \(lcp\) 长度。
结论
-
\(height_{rk_i} \geq height_{rk_{i-1}} -1\)。这样我们就将原串下标相邻的位置的 \(height\) 联系了起来,直接暴力求解即可,这样时间复杂度容易分析为 \(O(n)\)。
-
\(lcp(sa_i,sa_j) = \min\limits_{k=i+1}^{j} height_k\),其中当 \(i=j\) 时要特殊处理。这个也比较好理解,相当于是如果一段长度从当前开始无法匹配,那么由于 \(sa\) 中字典序是不断增大的,那么这段长度之后也不可能匹配。这样我们就可以打一个 ST 表,然后能做到 \(O(1)\) 查两个后缀的 \(lcp\)。
-
原串中本质不同的字符串的数量为 \(\frac{n(n+1)}{2} - \sum\limits_{i=1}^{n} height_i\)。这个理解一下就是对于两个相邻的后缀 \(i-1\) 和\(i\),从 \(i\) 开始的前 \(height_i\) 个串已经包含在 \(i-1\) 中了,所以它的贡献要减去 \(height_i\)。利用这个性质还可以求第 \(k\) 小子串,因为前 \(i\) 个后缀包含了排名从 \(1\) 开始的 \(\sum\limits_{i=1}^{n} n - sa_i + 1 - height_i\) 个子串,所以可以二分查找。
例
[USACO06DEC] Milk Patterns G
相当于求至少 \(k\) 个后缀的 \(lcp\) 的最大值,那么显然只取 \(k\) 个是最优的,而且只会取相邻的后缀。然后问题就转化为了在 \(height\) 数组上求长度为 \(k-1\) 的区间的最小值之最大值。这个直接用滑动窗口就做完了。
Z-字符串
题意:对于一个长度为 \(n\) 串 \(S\),它有 \(\frac{n(n+1)}{2}\) 个子串,\(f(S)\) 定义为这些子串两两之间的 \(lcp\) 之和,即 \(\sum\limits_{T_1 \in S} \sum\limits_{T_2 \in S} lcp(T1,T2)\)。现在对于某个给定的串 \(T,|T| = m\),对于每个 \(i\),求 \(f(T[i:])\)。
比较复杂。还是把问题放到后缀上来求解。我们考虑求出两个后缀对于答案的贡献,推导可得是关于两个后缀的长度和它们的 \(lcp\) 的式子。对于后缀 \(i\) 的答案,我们可以将其看成在后缀 \(i+1\) 的答案上加入了后缀 \(i\),接下来只需要算下标大于 \(i\) 的后缀和后缀 \(i\) 的贡献即可。这里的 \(lcp\) 很烦,可能涉及很多的量,所以我们考虑使用分治,对于每个区间找到它的中点,将其划为左右两个区间,算左右区间互相之间的贡献,这样我们只需对于每个位置求出其到中点处的 \(height\) 最小值,这样取 \(\min\) 时就可以只用算两个量。现在我们还剩两个条件,一个是下标条件,另一个是 \(lcp\) 的条件(最小值是谁),可以考虑用树状数组维护一个二维偏序。这样时间复杂度就是 \(O(n \log^2 n)\) 的,实现时注意常数。
总结
SA 也算是一种小巧的结构,记录的信息没有那么冗杂,但是这样的缺点就是可能使用起来没有那么方便。SA 与字典序相关,所以可能比较擅长解决与字典序有关的问题,在处理与长度有关的东西时可能就比较复杂。
SAM(后缀自动机)
解决字符串问题的强力武器。SAM 可以看做是一种经过压缩后的结构,可以存储一个串中所有子串的信息,并且是能接受一个字符串所有后缀的最小 DFA(相当于是最简洁版本的)。SAM 最多有 \(2n - 1\) 个点,\(3n - 4\) 条转移边,而且其 \(parent\) 指针构成了树形结构。
我们记 \(endpos(s)\) 表示一个字符串 \(s\) 在原串中所有出现处的结束位置组成的集合。后缀自动机上一条转移边 \(nxt_{p,ch}\) 表示从 \(endpos\) 集合编号为 \(p\) 的点加上一个字符 \(ch\) 后到达的新的 \(endpos\) 集合编号,可以发现对于 \(endpos\) 集合编号为 \(p\) 的所有点,加了一个字符 \(ch\) 后新的 \(endpos\) 都是 \(nxt_{p,ch}\)。相对更重要的是 \(parent\) 树。\(parent\) 树上的父子 \(p,fa_p\) 关系可以认为是从 \(endpos\) 集合编号为 \(p\) 的子串中选最长的出来,记长度为 \(len_p\)。然后从长度为 \(len_p\) 开始在开头删字符(相当于留下的是后缀),一直删到长度为 \(len_{fa_p}\),即 \(endpos\) 集合发生改变为止。可以发现当前长度为 \(len_{fa_p} + 1 \sim len_p\) 的字符串的 \(endpos\) 集合编号都是 \(p\)。可能说起来比较抽象,但是题做多了之后就可以用相对形象的方式来理解它。
构造 SAM
我觉得代码里已经把所有东西和细节都说清楚了,就直接放个代码。
auto insert = [](int ch) -> void {
ch-='a';
int cur=++cnt,p=last;
len[cur]=len[last]+1;
while(p>=0&&nxt[p][ch]==0) nxt[p][ch]=cur,p=fa[p];
if(p==-1) fa[cur]=0;
else{
int q=nxt[p][ch];
if(len[p]+1==len[q]) fa[cur]=q;
else{
int w=++cnt;
for(int i=0;i<26;i++) nxt[w][i]=nxt[q][i];
len[w]=len[p]+1,fa[w]=fa[q],fa[q]=w,fa[cur]=w;
while(p>=0&&nxt[p][ch]==q) nxt[p][ch]=w,p=fa[p];
}
}
last=cur;
};
SAM 经典应用
不同子串个数
SAM 已经存储了所有不同的子串,所以答案可以在 \(parent\) 树上求得为:
多串间的最长公共子串
可以只对第一个串建 SAM。我们定义 \(maxn_u\) 为在 SAM 上节点 u 能被所有串共同匹配的最长长度。每次新加入一个串的时候,我们都维护一个指针 \(p\) 和匹配长度 \(now\),表示在新的字符串加入前 \(i\) 个字符后能匹配的最长长度及对应的所在节点。每次新加入字符 \(ch\) 时,我们都尝试在 \(p\) 后直接添加 \(ch\),若不行则去跳 \(p\) 的 \(parent\) 链,直到能跳了为止,在跳的过程中同时要更新 \(now\)。如果最后发现可以走 \(ch\) 这条转移边,那么我们令 \(p \leftarrow nxt_{p,ch},now \leftarrow now + 1\),否则令 \(p \leftarrow 0,now \leftarrow 0\)。对于每个新加的串,我们在开一个数组 \(maxx_u\) 记录当前字符串与第一个串匹配的点为 \(u\) 时的匹配长度。每次添加完一个字符后,我们都令 \(maxx_p \leftarrow \max(maxx_p,now)\)。所有字符添加完后,我们再对 \(maxx\) 取个子树最大,最后令 \(maxn_u \leftarrow \min(maxn_u,maxx_u)\) 即可。
(伪)广义 SAM
有时候我们可能有很多字符串,这时候我们可能需要一个结构来将所有串的子串给组织起来。一种常见的方法是建广义 SAM,即先将所有串丢到字典树上,然后在字典树上按照 BFS 序去建 SAM,这样可以证明仍然是最小 DFA。但是还有一种方法是,每新加一个字符,就将 \(last\) 指针置 \(0\),然后再去添加即可。这样点数和转移边数的上限仍不变,并且构造的时间复杂度也是对的,这样我们就得到了一个伪广义后缀自动机。但是这样有几个问题,其一就是虽然其时空复杂度是对的,但它不一定是最小 DFA,但问题不大。还有一个问题,就是在伪广义 SAM 上,可能会存在一个问题就是 \(len_i = len_{fa_i}\),这个问题一般情况下也没有什么问题,因为此时 \(i\) 节点没有贡献,但是这样就必须用搜索来遍历一棵树,因为在 \(len\) 相同时内部的父子关系是乱的。不过影响也不是很大。
例
[BJWC2018] Border 的四种求法
题意可以转化为求一个最小的 \(p\) 满足 \(p > l \land lcp(l,p) + p \geq r+1\)。我们考虑使用树剖来解决这道题。对于我们在树剖过程中跳到的每个点 \(x\),我们都维护一下 \(x\) 子树内,以 \(p\) 为下标的线段树,上面存的权值为 \(p\)。这样对于 \(x\) 下端的点,我们只需要在线段树上查询 \(p>l \land p \geq r - len_x + 1\) 的最小 \(p\) 即可,这可以线段树上二分实现。然后对于从 \(top_x \sim x\) 的上的每个点 \(y\),我们都维护一下 \(y\) 及 \(y\) 的轻子树内,以 \(p\) 为下标的线段树,上面存的权值为 \(p + len_y\),然后我们在线段树上查询 \(p > l \land p + len_y \geq r+1\) 的最小 \(p\) 即可,同样可以使用线段树上二分。实现方面可以考虑将询问离线,这样就可以做到空间复杂度为 \(O(n)\)。时间复杂度为 \(O(n \log^2 n)\)。
总结
SAM 可以说是一种非常强力的结构,它可以解决很多复杂的字符串问题,所以也是重难点之一。SAM 也经常结合数据结构,但是也不能光想着数据结构(如树剖上树)。各种字符串算法要灵活使用,尽量做到简便。
拉格朗日插值
对于 \(n+1\) 个自变量及其函数值 \((x_i,y_i)\),我们可以唯一确定一个 \(n\) 次多项式 \(f\) 使得 \(f(x_i) = y_i\),具体为:
这样我们就可以与处理后快速求解 \(f(X)\)。
例
自然数幂和
对于前 \(n\) 个自然数的 \(k\) 次幂和,我们可以证明其答案为关于 \(n\) 的 \(k+1\) 次多项式,这样我们就可以通过预处理 \(n = 1 \sim k+2\) 的自然数幂和,来插出一个非常大的数的自然数幂和。
Romantic Misletoe
这个 \(\gcd = 1\) 的限制可以用莫比乌斯反演消去。剩下的问题暴力可以用 DP 求解,然后你可以发现答案是关于权值种数的 \(n\) 次多项式,所以可以先 DP 小范围后直接拉插即可。
总结
当发现答案的形式是关于某值的多项式时,可以尝试使用拉格朗日插值来进行优化。
虚树
当在树上解决一些问题(如树形 DP)时,暴力的做法是直接遍历整棵树。但是我们发现,如果我们只钦定了某些关键点是有效的,我们会发现整棵树上有相当多的点是没什么用的。所以我们现在就考虑建出我们的虚树,包含所有关键点以及关键点两两之间的 \(LCA\),这样就极大的减少了冗余的信息。接下来我们考虑如何建出虚树。
我们先将所有点按照 \(DFN\) 排序。我们使用单调栈,来维护当前虚树上的最右链。每次新加入一个关键点,先将其与栈顶元素求 \(LCA\)。若 \(LCA\) 就是当前栈顶元素,则直接将当前关键点加入栈中。否则我们一直弹栈,直到栈顶元素下面的元素对应节点不在 \(LCA\) 的子树(不包括 \(LCA\))中,中途将弹出元素与其栈中相邻元素相互连边。弹栈后若栈顶元素下面的元素对应节点就是 \(LCA\),就将 \(LCA\) 和栈顶元素连边,并弹出;否则还是先将 \(LCA\) 和栈顶元素连边,然后将栈顶元素换成 \(LCA\)。然后再加入关键点即可,最后要清空栈。这样我们就以 \(O(k \log k)\) 排序、\(O(k)\) 建树的复杂度求出了虚树。求出来之后我们就可以像之前那样在上面求解问题了。
LCT(Link Cut Tree)
LCT 是一种动态树,可以用来维护形态存在变化的森林,时间复杂度为均摊的单次 \(O(\log n)\)。数据结构题还是需要多练习才能掌握其精髓。
技巧
LCT 维护 SAM
有些题目要求我们动态加入字符,并且维护 SAM,这种情况下就可以考虑使用 LCT 来维护 SAM 的 \(parent\) 树。\(link\) 的时候可以不用 \(makeroot\),因为 \(link\) 的其中一个点总是单点。当然还有一种情况就是在 \(q\) 和 \(fa_q\) 之间加一个点 \(w\),这时候也不需要 \(cut\),我们可以直接将 \(w\) 插入到 \(Splay\) 上,具体就是先将 \(q\) 旋至其所在 \(Splay\) 的根,然后在 树上 \(q\) 前驱的位置处新建一点表示 \(w\)。这样我们可以不用打 \(reverse\) 标记。这种方法的一个应用是维护每个节点的最大 \(endpos\),可以证明如果用此方法维护 \(LCT\),那么 \(LCT\) 上每个 \(Splay\) 树的最大 \(endpos\) 是相同的,这样就十分方便。
LCT 维护基环树森林
我们考虑对于每颗基环树都断掉环上的一条边,再单独维护一下断掉的边,这样就变成了维护森林。与 LCT 维护 SAM 一样,这时可以证明也不需要 \(reverse\)。
总结
LCT 是一种比较灵活的数据结构,通过合理的操作和设计可以完成一些其他数据结构难以完成的问题(如在理论单次 \(O(\log n)\) 的时间内维护树链信息)。数据结构题还是要多练才行。
矩阵树定理
矩阵树定理可以用来解决图的生成树计数问题。
对于有向图来说,我们记 \(D^{out}\) 表示图的出度矩阵,\(A\) 为图的邻接矩阵。记原图的出度 Laplace 矩阵 \(L^{out} = D^{out} - A\),则原图中以 \(k\) 为根的根向生成树方案数就是 \(L^{out}\) 矩阵去第 \(k\) 行和第 \(k\) 列后矩阵的行列式的值。叶向生成树就把边反过来就行了。无向图可以直接将无向边视作双向边,然后随便以某个点为根就是整个图的生成树方案数。这样我们就在 \(O(n^3)\) 的时间内求出了生成树方案数。对于扩展的情况,若图带有边权,我们可以用相同的方式来求解每颗生成树的权值之和,其中生成树的权值为所有边的边权乘积。
对于更一般的情况,我们定义生成树的权值为其边权和的 \(k\) 次方,要求所有生成树的权值之和。此时我们可以将一条边的边权设置为一个长度为 \(k+1\) 的向量,然后根据 \((a+b)^k = \sum\limits_{i=0}^{k} \binom{k}{i} a^i b^{k-i}\) 的原理去定义两个向量的乘法,然后就可以套用矩阵数定理进行求解。时间复杂度为 \(O(k^2 n^3)\)。
矩阵数定理还可应用到欧拉回路计数上。一张欧拉图的欧拉回路的数量,等于随便选一个点作为根,其生成树的方案数乘上每个节点的度数减一的阶乘。对于制定起点的欧拉回路计数,需再乘上起点的度数。计算两点间的欧拉路径计数时,可以在终点和起点之间连一条边,就变为了欧拉回路计数。
例
我回来了
定义一颗生成树的权值为所有边权的 \(mex\),求所有生成树的权值和。
我们定义 \(f_s\) 为边权集合中不存在的边权值集合恰好为 \(s\) 的方案数。首先我们可以将边权在集合中边全部删去,然后对于每个点都以其为根,统计一下生成树数量之和。然后我们会发现我们可能还有更多的边权没有使用,所以可以使用容斥来解决。此时时间复杂度是 \(O(n^4 2^n + 3^n)\) 的,无法通过,瓶颈在于求解任意根生成树上。我们可以使用 \(O(n^3)\) 的方法求解。任意根相当于对于所有的 \(i\),删去 \(i\) 行 \(i\) 列后求出方案数再求和。我们可以考虑在对角线上的每个元素上都加一个变量 \(x\),最终相当于是求行列式的值中 \(x^1\) 的系数。这样直接求解时间复杂度是 \(O(n^3)\) 的,但是常数较大。我们发现矩阵中每行的值加起来都是 \(0\),所以我们用第一列去加上后面的所有列,这样第一列就全变成了 \(x\)。我们要求的是 \(x^1\) 的系数,所以我们直接把第一列全部置为 \(1\),其他的 \(x\) 全部删去,再求行列式的值就是任意根生成树方案数。这样时间复杂度就是 \(O(n^3)\) 的,可以通过。
总结
要应用矩阵树定理,首先要将图建出来,其次是运用矩阵数定理加上合适的方法去求解想要的权值和。这个东西还是比较巧妙,将线性代数和图论这两个看似不相关的东西联系了起来。
多项式相关
目前学了 FFT、NTT 及 FWT。
FFT 和 NTT 都是处理的加法卷积,即下标相加,时间复杂度均为 \(O(n \log n)\),其中 FFT 用到了复数计算,可以求解不带模数的多项式乘法,不过当值域较大时会出现精度问题,常数也由于 \(double\) 类型的计算比较大。而 NTT 用原根代替了 FFT 的单位根,用来求解特定模数下的多项式乘法,常数相对会小很多,因为没有实数运算。但是当模数的 \(\varphi\) 值中所含的 \(2\) 因子的数量较少时,可能不能使用 \(NTT\),而 \(FFT\) 又可能爆精度,所以此时有两种方法,第一种是三模 NTT,即取三个较常见的 NTT 模数,分别进行多项式乘法,最后 CRT(不需要扩展)合并即可,这样可以得到所有位置上不取模的真实值。另一种方法是 5-FFT 或 4-FFT,即先将序列中的数拆成高位和低位,然后分别计算,最后将两部分合并,但是如果直接算的话需要多次 FFT,此时可以通过一些观察和优化将次数优化至 5 次或 4 次。
FWT 是处理下标进行位运算的卷积,时间复杂度为 \(O(n 2^n)\),可以简便的求出下标按位与、或、异或情况下的卷积。FWT 同样也可以解决子集卷积,时间复杂度为 \(O(n^2 2^n)\)。
这类型的题目感觉难点还是在推出答案的式子,然后将其尽可能的转化为卷积的形式,最后用这些变换求解,所以难点一般并不在这些变换上。

浙公网安备 33010602011771号