写在前面
口嗨区 精神续作。
准备把这个当日记用(
2020.8.15
2020.8.15 题
SA 板子背诵检查。
断环成链,把字符串复制一遍扔到后面,跑 SA 即可。
SP705 SUBST1 - New Distinct Substrings
SAM 板子背诵检查。
一个字符串唯一对应一个状态,\(ans = \sum\limits_i{\operatorname{len}(i)-\operatorname{len}(\operatorname{link}(i))}\)
SAM 板子背诵检查。
按拓扑序求出每个状态出现次数,仅更新 \(F(\operatorname{len}(i))\)。
长的包含短的,显然有 \(F(i) = \max\limits_{j=i+1}^{n}\{F(j)\}\)。
2020.8.15 日记
你圈两件大事:
惨 咩 惨。
FELT 有缘再见。
感谢陪伴,后会有期。
2020.8.16
2020.8.16 题
SP8747 NSUBSTR2 - Substrings II
以下口胡现场:
出现次数 = 所在状态的 parent 树子树和。
静态问题可直接 dfs。动态问题考虑新加入字符的影响。
一个点只对根 -> 该点路径上节点的子树和 有贡献。
考虑 SAM Insert 时的分类讨论,发现要求支持下列操作:
- 分裂已有节点,相当于将一条边拆成 两条边一个节点。
- 建立一个新节点,指向已有的节点。
将 根-> 新节点路径 权值 + 1。
强制在线,不可线段树,使用 LCT 维护。
强 \(\hearts\) 制 \(\hearts\) 在 \(\hearts\) 线,林 \(\hearts\) 克 \(\hearts\) 卡 \(\hearts\) 特。
无 \(\hearts\) 机 \(\hearts\) 结 \(\hearts\) 合,建 \(\hearts\) 议 \(\hearts\) 击 \(\hearts\) 毙。
先考虑 \(T=1\):
建 SAM,dfs 求每个状态的出现次数 \(size\)。
再在 DAWG 上按照字典序跑,跑到一个节点就令 \(k- size_i\),并转移。
当 \(k=0\) 时,当前跑到的字符串即为所求。
\(T=0\) 时,直接赋 \(size_i = 1\),即每个状态仅出现 \(1\) 次。
再按上述过程跑即可。
然后 T 了。
发现这玩意复杂度上限是 \(O(n+k)\) 的,\(T=0\) 时必定达到上限。
跑到一个节点才令 \(k - size_i\),并转移 太慢啦!
考虑权值线段树维护第 k 小的过程:
若当前跑出串 \(S\),考虑下一步转移,向 \(c\) 转移,相当于遍历所有前缀为 \(S+c\) 的串。
考虑向 \(a\) 转移,若所有前缀为 \(S+a\) 的串的数量 \(<k\),答案串一定不以 \(S+a\) 作为前缀。
遍历它们只会浪费时间,仅需使 \(k-\) 所有前缀为 \(S+a\) 的串的数量,再考虑向 \(b\) 转移。
考虑维护以某字符串作为前缀的,所有子串的数量。
这玩意咋维护啊?? 自己 YY 了一波:
考虑 parent树的性质,对于一个状态,其在 parnet 树上的子孙,均以其为 后缀。
统计某状态为 后缀 的子串的数量,可以直接在 parent 树上统计。现在要求统计作为 前缀 的情况,想到建反串的 parent 树,dfs 即可求出以某状态为 前缀 时的子串数量。
在正串的 DAWG 上跑答案时,在转移到某状态前根据它判断即可。
但这玩意复杂度是假的/fad,还是 T 了。
因为不知道反串,正串 SAM 状态的映射关系。
想查询以某状态为 前缀 时的子串数量,只能在反串的 SAM 中把该状态的反串跑出来,复杂度就爆炸了。
这个 idea 可能还不错?以后说不定会用到。
再 YY 一波:
考虑在 SAM 上暴跳时的搜索树。
发现有许多重复节点,它们的子树还会被跑多次,考虑记忆化子树的 size。
没有去实现,正确性未知。
一个子串唯一对应 SAM 中的一条路径,第 \(k\) 小子串即第 \(k\) 小路径。
预处理每个状态的路径条数,查询类似权值线段树。
注意预处理路径条数时按照拓扑序 DP,
用 \(u\) 可转移到的节点更新 \(u\)。
需要先将各状态按照 \(len\) 进行排序,为保证复杂度使用了计数排序。
总复杂度 \(O(|S| + |ans|\cdot |\sum|)\)。
2020.8.16 爆零小技巧
结构体自带 TLE debuff!
2020.8.16 日记
全群就我不能参加 NOI
今晚大冒险翻车了草草草
晚上和 SD 群友讨论 dua 郎话题。
神仙群友居然打印本子藏在被子里半夜手冲
是我纯度不够了
2020.8.17
2020.8.17 题
求 \(S\) 的所有前缀的本质不同的子串的个数。
考察对 SAM 构建过程的理解。
对于一个确定的字符串 \(S\),其本质不同子串的个数,等于所有状态所表示子串的个数之和。
即有下式:
对于字符串 \(S\),考虑新加入字符 \(c\) 的影响。
加入 \(c\) 后,显然答案增加 不在 \(S\) 中出现的 \(S+c\) 后缀的个数。
设表示 \(S+c\) 的状态为 \(a\),考虑第一个在 \(S\) 中出现的 \(S+c\) 的后缀,会在 SAM 构建中赋值给 \(\operatorname{link}(a)\) 上。
则新字符的贡献即为 \(\operatorname{len}(a) - \operatorname{len}(\operatorname{link}(a))\)。
感觉在 SDOI 见了不少模板题了,传统艺能?
考察对 \(\operatorname{lcp}\) 单调性的理解。
\(S_1\) 加个终止符,\(S_2\) 串扔到 \(S_1\) 后面,跑 SA。
显然,答案即后半段的后缀,与前半段的后缀的所有 \(\operatorname{lcp}\) 之和。
按字典序枚举后半段的后缀,设当前枚举到的后缀为 \(sa_i\)。
若 仅考虑 字典序 \(<sa_i\) 的 前半段的后缀 \(sa_j\ (j<i)\),其对 \(sa_i\) 的贡献为 \(\operatorname{lcp}(sa_i, sa_j)\)。
由 \(\operatorname{lcp}\) 的单调性,当枚举到 第一个 \(>sa_i\) 的 后半段的后缀 \(sa_k\ (k>i)\) 时,有 :\(\operatorname{lcp}(sa_{k}, sa_j)\le \operatorname{lcp}(sa_i,sa_j)\)。
-
若 \(\operatorname{lcp}(sa_{k}, sa_j)< \operatorname{lcp}(sa_i,sa_j)\),则 \(sa_j\) 对 \(sa_k\) 的贡献应变为 \(\operatorname{lcp}(sa_k, sa_j) = \min\{\operatorname{lcp}(sa_i,sa_j), \min\limits_{l=i+1}^{k}{\{\operatorname{height}_l}\}\}\)。
-
若存在 \(sa_l, l\in (i,k)\) 为 前半段的后缀 时,作出贡献的元素增加。
考虑在枚举后缀的过程中,用权值线段树维护 字典序 \(<sa_i\) 的 前半段 的后缀 \(sa_j\ (j<i)\) 的不同长度的 \(\operatorname{lcp}\) 的数量。
上述两操作,即为区间赋值 与 单点插入。
再按字典序倒序枚举后缀,计算字典序 \(>sa_i\) 的 前半段的后缀的贡献。
分析很屑,代码有详细注释。
复杂度 \(O(n\log n)\)。
也可以单调栈/ 广义 SAM,复杂度也为\(O(n\log n)\) 级别。
SA 做法
化下式子:
考虑如何快速求后一半,即所有 \(\operatorname{lcp}\) 之和。
发现有下列等价关系:
\(\operatorname{lcp}(a,b) = \operatorname{lcp}(b,a)\),枚举 \(sa\) 一定不会重也不会漏。
类似这题的套路:「HAOI2016」找相同字符,
考虑枚举 \(sa_j\),用权值线段树维护 \(sa_i (i<j)\) 的不同长度的 \(\operatorname{lcp}(sa_i, sa_j)\) 的数量。
引理:\(\forall 1\le i < j\le n,\, \operatorname{lcp}(sa_i,sa_j) = \min\limits_{k=i+1}^j\{\operatorname{height_k}\}\)。
模拟引理,当 \(j+1\) 时将权值线段树中所有 \(>\operatorname{height}_{j+1}\) 的元素删除,并添加相同个数个 元素 \(\operatorname{height}_{j+1}\)。
添加一个 \(\operatorname{height}_{j+1}\),代表新增的 \(sa_j\) 的贡献。
贡献求和即可。
总复杂度 \(O(n\log n)\)。
线段树太傻逼了,考虑单调栈。
发现有下列等价关系:
即求 \(\operatorname{height}\) 每个区间的区间最小值之和。
经典问题,考虑 \(\operatorname{height}\) 作为最小值的区间的最大 左/右端 点,可单调栈维护。
答案即 \(\sum\limits_{i=2}^{n}(i-l_i)\times (r_i-i)\times \operatorname{height}_i\)。
注意区间长度不能为 1。
后缀树做法
考虑原始式子:
这玩意长得很树上差分。
对于 \(S\) 的后缀树,\(\operatorname{lcp}\) 即为后缀树的 \(\operatorname{lca}\)。
上式等价于后缀树上所有后缀之间的距离。
对反串建 SAM,即得后缀树。
题目转化为:树上某一点是多少 表示后缀的节点 的 \(\operatorname{lca}\) 再乘上 \(dep\)。
记录子树大小, DP 实现即可。
2020.8.17 爆零小技巧
函数无 return,爆零两行泪
边界玄学怎么办?判断正确性可靠对拍实现。
线段树不一定只开 4 倍空间,当 \(n\) 到达 \(5\times 10^5\) 级别一定要小心。
2020.8.17 日记
kero kero kero kero~
今天是风神录发行13周年纪念日,转发这条信息,你就能向守矢神社传达信仰,让诹访子继续存在,我试过,是假的,还会因为和交大校友撞日期而-1s,但今天真的是风神录发行13周年纪念日。
过膝袜到啦!
吐槽一波 博客园的 [toc],标题行重复跳转就会爆炸。
2020.8.18
2020.8.18 题
一些概念
循环同构
当字符串 \(S\) 中可以选定一个位置 \(i\) 满足:
则称 \(S\) 与 \(T\) 循环同构。
字符串 \(S\) 的 最小表示 为与 \(S\) 循环同构的所有字符串中字典序最小的字符串。
最小表示法
考虑暴力,每次比较 \(i,j\) 开始的循环同构,\(k\) 为出现的第一个不同的位置。
若出现不一样的字符,跳过字典序较大的循环同构。
最后剩下的就是答案。
int k = 0, i = 0, j = 1;
while (k < n && i < n && j < n) { //暴力比较
if (a[(i + k) % n] == a[(j + k) % n]) {
++ k;
continue ;
}
//跳过字典序较大的
if (a[(i + k) % n] > a[(j + k) % n]) ++ i;
else ++ j;
k = 0; //清空 k
if (i == j) i ++;
}
i = min(i, j);
看起来很快?
\(S=aaaa...aaaab\) 时,会被卡到 \(O(n^2)\)。
考虑奇怪的优化。
在上述匹配过程中,对于一对 \(i,j\),若 \(k\) 为出现的第一个不同的位置,有:
若 \(S[i+k] > S[j+k]\),则对于 \(i\le l\le i + k\),以 \(l\) 为起始位置的循环同构,一定不能成为答案。
用 \(S_x\) 表示以 \(x\) 为起始位置的循环同构,对于任意一个 \(S_{i+p}(p\le k)\) 一定存在 \(S_{j+p}\) 比他更优。
比较时可直接跳过下标区间 \([i,i+k]\),直接比较 \(S_{i+k+1}\)。
\(k+1\) 的次数最多为 \(2n\),时间复杂度 \(O(n)\)。
SA
复制一遍 \(S\),放到原串后边。
离散化,跑 SA,答案即 \(rk\) 最小且 \(\le |S|\) 的后缀。
SAM
复制一遍 \(S\),放到原串后边。
建 SAM,从根节点按字典序贪心,跑出一个长度为 \(n\) 的串即为答案。
注意使用 map。
SP1811 LCS - Longest Common Substring
代码
只有两个字符串,考虑 SA。
\(S_1\) 加个终止符,\(S_2\) 拼接到 \(S_1\) 后面,跑 SA 求出 \(\operatorname{height}\)。
问题变为 求前半段后缀 与 后半段后缀 \(\operatorname{lcp}\) 的最大值。
\(i\) 为后半段的后缀 等价于 \(i>|S_1|+1\)。
对于一个后缀 \(i>|S_1|+1\),设 \(l_i\) 为后缀排序后 最大 的 比 \(i\) 小 的前半段 的后缀的 排名。
即有 \(sa_{l_i}\le |S_1|,\ l+i<rk_i\),且 \(\forall l_i<k<rk_i, sa_k>|S_1|+1\) 成立。
类似地,设 \(r_i\) 为后缀排序后 最小 的 比 \(i\) 大 的 前半段 的后缀的 排名。
考虑 \(\operatorname{lcp}\) 的单调性。
对于一个后半段的后缀 \(i>|S_1|+1\),满足 \(\operatorname{lcp}(i,j)\) 最大的 \(j\le|S_1|\),显然为 \(l_i\) 或 \(r_i\),有。
则有:
先预处理,对 \(\operatorname{height}\) 建立 st 表。
\(l_i,r_i\) 可通过单调栈简单求得,计算答案时枚举后半段后缀,\(O(1)\) 查询 \(\operatorname{lcp}\) 即可。
总复杂度 \(O(n\log n)\) 级别。
一些细节:
若 \(l_i<1\) 时,该 \(l_i\) 不作出贡献,因为不存在这样的后缀。
\(r_i>|S|\) 时也没有贡献,这样的后缀已经属于后半段了。
发现一些奇妙的性质:
对于 \(sa_i,sa_{i-1}\),其 \(\operatorname{lcp} = \operatorname{height}_i\)。
考虑 \(\operatorname{lcp}\) 的单调性,有一个显然的结论:
最长公共子串为:所有满足 \(sa_{i-1}, sa_i\) 分属 前/后 半段的 \(\operatorname{height}_i\) 的最大值。
即作为答案的 \(\operatorname{lcp}(l_i,i)\) (或 \(\operatorname{lcp}(i, r_i)\)),一定有 \(l_i=rk_{i}-1\) 或 \(r_i=rk_i+1\)。
证明考虑反证法。
若 \(ans=\operatorname{lcp}(l_i, i)\),且 \(l_i < rk_i-1\)。
由 \(\operatorname{lcp}(l_i,i)=\min\limits_{j=l_i+1}^{rk_i}\operatorname{height}_j\),可知对于 \(\forall l_i<j<rk_i\),\(\operatorname{lcp}(l_i, sa_j)\ge \operatorname{lcp}(l_i,i) = ans\),取它们作为答案,答案不会变劣。
反证原结论成立,\(ans = \operatorname{lcp}(i,r_i)\) 同理。
SA
若两个位置 \(i,j\) 是「\(r\) 相似」的,那么它们也是「\(0\sim (r-1)\) 相似」的。
它们会对「\(0\sim r\) 相似」的答案做出贡献。
「\(r\) 相似」的答案即为 「\(r\sim (n-1)\) 相似」的第一问的后缀和 与 第二问的后缀最大值。
考虑倒序枚举「\(r\) 相似」的位置并计算贡献。
「\(r\) 相似」的实质即 \(\operatorname{lcp}\) 问题,先对原串跑 SA,求得 \(\operatorname{height}\)。
考虑「\(r\) 相似」的定义,则对于两位置 \(i,j\),他们是 「\(\operatorname{lcp}(S[i:n],S[j:n])\) 相似」的。
引理 :LCP Theorem
\[\forall 1\le i < j\le n,\, \operatorname{lcp}(sa_i,sa_j) = \min_{k=i+1}^j\{\operatorname{height_k}\} \]
考虑按照 \(\operatorname{height}\) 将后缀排序后的后缀进行划分。
若 \(\operatorname{height}_i\ge r\),将 \(sa_{i-1}\) 与 \(sa_i\) 划入一个集合,否则划入不同的集合。
划分后,对于所有大小 \(\ge 2\) 的集合,集合中后缀的 \(\operatorname{lcp}\ge r\),它们都会对 「\(r\) 相似」的答案做出贡献。
这样的所有集合的贡献累计,即为 「\(r\) 相似」的答案。
定义上述划分方式为 「\(r\) 划分」,考虑如何在此基础上获得 「\(r-1\) 划分」。
显然,只需将 \(\operatorname{height}_i = r-1\) 的后缀 \(sa_{i-1}\) 和 \(sa_i\) 所在集合合并即可。
考虑将 \(\operatorname{height}\) 降序排序,用并查集维护集合,按上述过程依次进行合并,即可依次得到「\(n\sim 1\) 相似」的答案。
考虑如何维护集合的贡献。先考虑维护第一问:
选出「\(r\) 相似」方案数,对于「\(r\) 划分」中一个大小 \(\ge2\) 的集合,集合中任意两个后缀的 \(\operatorname{lcp} \ge r\),该集合的贡献即为 \((size-1)\times size\)。
合并时直接 \(size\) 累加即可。
考虑第二问:
由于可能存在 \(a_i<0\),考虑维护集合的最大值,次大值,最小值,次小值。
为保证答案合法,四个值中的任意两个 都不能来自于 同一个位置。
集合的贡献为 \(\max\{max_1\times max_2, min_1 \times min_2\}\)。
合并时注意四个值的大小关系,以及集合大小。
代码中使用了 multiset 维护。
复杂度瓶颈为倍增 SA,为 \(O(n\log n)\) 级别。
使用炫酷 DC3 魔术可做到 \(O(n)\)。
后缀树
两个后缀的 \(\operatorname{lcp}\),在后缀树中代表对应两叶节点的 \(\operatorname{lca}\) 的深度。
发现上述过程中,「\(r\) 划分」的合并过程,形成了树形结构。
这棵树的叶节点均为字符串的后缀。
并查集合并,实际上模拟的是后缀树的节点合并。
考虑进行树形 DP 维护上述信息。
维护子树的 \(size\) 和 子树中叶节点的最大值,次大值,最小值,次小值。
按上述规则进行合并即可。
使用 SAM 建后缀树,复杂度 \(O(n)\)。
2020.8.18 爆零小技巧
\(\log\) 函数贼 jier 慢,建议预处理 \(\log_2\) 函数。
2020.8.18 日记
女装到了,很有那味。
就等接头人回来了(
2020.8.18
2020.8.18 题
不考虑加一个数的限制条件,本题为很水的双串最长公共子串问题。
发现字串长度,数字范围都很小,考虑暴力枚举加的数。
直接做就可以了。
SP1812 LCS2 - Longest Common Substring II
代码
多串最长公共子串问题,考虑 SAM。
如果只有两个串:SP1811 LCS - Longest Common Substring
对第一个串建 SAM,用第二个串从起始节点开始,在 SAM 上进行匹配。
若当前状态为 \(x\),如果有对应字符 \(s_i\) 的转移,直接转移即可,匹配长度 \(+1\)。
如果没有对应转移,转移到 \(\operatorname{link}(x)\),匹配长度 \(=\operatorname{len}(x)+1\) 检查有无对应转移,若没有则继续转移到 \(\operatorname{link}(\operatorname{link}(x))\),直到存在对应转移。
若始终找不到对应转移,则从根开始重新匹配。
跳 parnet 树相当于失配指针,继续利用了已匹配的部分。
匹配过程中匹配的最长长度即为答案。
考虑多串,对第一个串 \(S_1\) 建 SAM,用其他串在 SAM 上匹配,设当前匹配到串 \(S_i\)。
对于状态 \(u\),维护转移到它时最大的匹配长度 \(mx_u\),即以该状态作为后缀时的公共子串的最长长度。
在匹配过程中进行维护即可。
考虑一个状态 parent 树上的所有祖先,若该状态可被匹配到,则祖先也可被匹配到。
祖先的 \(mx\) 应为 其子树中 \(mx\) 的最大值。
\(S_i\) 匹配完成后按拓扑序对祖先的信息进行更新。
对于一个状态 \(u\),将 \(S_2\cdots S_n\) 匹配时的 \(mx_u\) 取 \(\min\),得到在所有字符串中 转移到 \(u\) 最长的匹配长度,即以 \(u\) 为后缀时 \(S_2\cdots S_n\) 公共子串的最长长度,设为 \(mi_u\)。
所有的 \(mi_u\) 取最小值,即为答案。
小细节
最长公共子串不会超过最短串的长度,应对最短的串建 SAM,以保证复杂度。
注意每次匹配完一个字串时,都将 \(mx\) 清空。
更新祖先信息时 应对祖先的 \(\operatorname{len}\) 取 \(\min\)。
SP10570 LONGCS - Longest Common Substring
上题的双倍经验,学习了广义 SAM 写法。
2020.8.18 爆零小技巧
2020.8.18 日记
写在最后
我永远喜欢露米娅,大妖精,琪露诺,小恶魔,帕秋莉,咲夜,蕾米莉亚,芙兰朵露,蕾蒂,橙,爱丽丝,莉莉白,露娜萨,梅露兰,莉莉卡,妖梦,幽幽子,蓝,紫,莉格露,米斯蒂娅,慧音,魔理沙,灵梦,因幡帝,铃仙,永琳,辉夜,妹红,文文,幽香,小町,四季,秋静叶,秋穰子,...?这里应该填谁。
来源:oschina
链接:https://my.oschina.net/u/4387121/blog/4517082