水题日记

五迷三道 提交于 2020-08-20 09:20:30


写在前面

口嗨区 精神续作。
准备把这个当日记用(


2020.8.15

2020.8.15 题

[JSOI2007]字符加密

代码

SA 板子背诵检查。
断环成链,把字符串复制一遍扔到后面,跑 SA 即可。


SP705 SUBST1 - New Distinct Substrings

代码

SAM 板子背诵检查。
一个字符串唯一对应一个状态,\(ans = \sum\limits_i{\operatorname{len}(i)-\operatorname{len}(\operatorname{link}(i))}\)


SP8222 NSUBSTR - Substrings

代码

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. 分裂已有节点,相当于将一条边拆成 两条边一个节点。
  2. 建立一个新节点,指向已有的节点。
    将 根-> 新节点路径 权值 + 1。

强制在线,不可线段树,使用 LCT 维护。
\(\hearts\)\(\hearts\)\(\hearts\) 线,林 \(\hearts\)\(\hearts\)\(\hearts\) 特。
\(\hearts\)\(\hearts\)\(\hearts\) 合,建 \(\hearts\)\(\hearts\)\(\hearts\) 毙。


「TJOI2015」弦论

代码

先考虑 \(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 /kk

今晚大冒险翻车了草草草

晚上和 SD 群友讨论 dua 郎话题。
神仙群友居然打印本子藏在被子里半夜手冲 /fad
是我纯度不够了 /fad


2020.8.17

2020.8.17 题

「SDOI2016」生成魔咒

代码

\(S\) 的所有前缀的本质不同的子串的个数。

考察对 SAM 构建过程的理解。

对于一个确定的字符串 \(S\),其本质不同子串的个数,等于所有状态所表示子串的个数之和。
即有下式:

\[ans = \sum_{u\in \operatorname{DAWG}}{\operatorname{len(u)} - \operatorname{len(\operatorname{link}(u))}} \]

对于字符串 \(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 见了不少模板题了,传统艺能?


「HAOI2016」找相同字符

代码

考察对 \(\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)\)

  1. \(\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}\}\}\)

  2. 若存在 \(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)\) 级别。


「AHOI2013」 差异

代码

SA 做法

化下式子:

\[\begin{aligned} ans &= \sum_{1\le i<j\le n}\{\operatorname{len}(T_i) +\operatorname{len}(T_j) - 2\times \operatorname{lcp} (T_i,T_j)\}\\ &= \sum_{1\le i<j\le n}\{(i +j) - 2\times \operatorname{lcp} (T_i,T_j)\}\\ &= \dfrac{(n-1)\times n \times (n+1)}{2} + 2\sum_{1\le i<j\le n}\operatorname{lcp} (T_i,T_j) \end{aligned}\]

考虑如何快速求后一半,即所有 \(\operatorname{lcp}\) 之和。

发现有下列等价关系:

\[\sum_{1\le i<j\le n}\operatorname{lcp} (T_i,T_j) = \sum_{1\le i<j\le n}\operatorname{lcp}(T_{sa_i}, T_{sa_j}) \]

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


线段树太傻逼了,考虑单调栈。
发现有下列等价关系:

\[\sum_{1\le i<j\le n}\operatorname{lcp}(T_{sa_i}, T_{sa_j}) = \sum_{1\le i<j\le n}\min_{k=i+1}^{j}\{\operatorname{height}_k\} \]

即求 \(\operatorname{height}\) 每个区间的区间最小值之和。
经典问题,考虑 \(\operatorname{height}\) 作为最小值的区间的最大 左/右端 点,可单调栈维护。
答案即 \(\sum\limits_{i=2}^{n}(i-l_i)\times (r_i-i)\times \operatorname{height}_i\)

注意区间长度不能为 1。


后缀树做法

考虑原始式子:

\[\sum_{1\le i<j\le n}\{\operatorname{len}(T_i) +\operatorname{len}(T_j) - 2\times \operatorname{lcp} (T_i,T_j)\} \]

这玩意长得很树上差分。
对于 \(S\) 的后缀树,\(\operatorname{lcp}\) 即为后缀树的 \(\operatorname{lca}\)
上式等价于后缀树上所有后缀之间的距离。

对反串建 SAM,即得后缀树。
题目转化为:树上某一点是多少 表示后缀的节点 的 \(\operatorname{lca}\) 再乘上 \(dep\)
记录子树大小, DP 实现即可。


2020.8.17 爆零小技巧

函数无 return,爆零两行泪 /se

边界玄学怎么办?判断正确性可靠对拍实现。

线段树不一定只开 4 倍空间,当 \(n\) 到达 \(5\times 10^5\) 级别一定要小心。


2020.8.17 日记

kero kero kero kero~
今天是风神录发行13周年纪念日,转发这条信息,你就能向守矢神社传达信仰,让诹访子继续存在,我试过,是假的,还会因为和交大校友撞日期而-1s,但今天真的是风神录发行13周年纪念日。

kero

过膝袜到啦!
吐槽一波 博客园的 [toc],标题行重复跳转就会爆炸。


2020.8.18

2020.8.18 题

P1368 工艺 /【模板】最小表示法

代码

一些概念

循环同构
当字符串 \(S\) 中可以选定一个位置 \(i\) 满足:

\[S[i:n]+S[1:i-1]=T \]

则称 \(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:i+k-1] = S[j:j+k-1] (k< n) \]

\(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\),有。

\[\max\{\operatorname{lcp}(i,j)\} = \max\{\operatorname{lcp}(l_i, i), \operatorname{lcp}(i, r_i)\} \]

则有:

\[ans = \max_{i=|S_1|+2}^{|S_1|+|S_2|+1}\max\{\operatorname{lcp}(l_i, i), \operatorname{lcp}(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)\) 同理。



「NOI2015」品酒大会

代码

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 日记

Ranko

女装到了,很有那味。
就等接头人回来了(


2020.8.18

2020.8.18 题

「SDOI2008」Sandy的卡片


代码

不考虑加一个数的限制条件,本题为很水的双串最长公共子串问题。

发现字串长度,数字范围都很小,考虑暴力枚举加的数。

直接做就可以了。


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 日记


写在最后

我永远喜欢露米娅,大妖精,琪露诺,小恶魔,帕秋莉,咲夜,蕾米莉亚,芙兰朵露,蕾蒂,橙,爱丽丝,莉莉白,露娜萨,梅露兰,莉莉卡,妖梦,幽幽子,蓝,紫,莉格露,米斯蒂娅,慧音,魔理沙,灵梦,因幡帝,铃仙,永琳,辉夜,妹红,文文,幽香,小町,四季,秋静叶,秋穰子,...?这里应该填谁。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!