package copypasta import ( . "fmt" "sort" "strings" ) /* 后缀自动机 Suffix automaton (SAM) 如果我们既知道前缀信息,又知道后缀信息,就可以做字符串匹配 将字符串的所有子串压缩后的结果 【推荐】https://oi-wiki.org/string/sam/ 证明了 SAM 的状态数不会超过 2n-1 (n>=2),最坏情况下为 abbb...bbb 【推荐】可视化工具:SAM Drawer https://yutong.site/sam/ https://mivik.gitee.io/sam-visualizer/ https://en.wikipedia.org/wiki/Suffix_automaton https://www.bilibili.com/video/av756051240/ https://baobaobear.github.io/post/20200220-sam/ https://ouuan.github.io/post/%E5%90%8E%E7%BC%80%E8%87%AA%E5%8A%A8%E6%9C%BAsam%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ https://codeforces.com/blog/entry/20861 《后缀自动机》,陈立杰 《后缀自动机在字典树上的拓展》,刘研绎 《后缀自动机及其应用》,张天扬 炫酷后缀树魔术 https://eternalalexander.github.io/2019/10/31/%E5%90%8E%E7%BC%80%E6%A0%91%E7%AE%80%E4%BB%8B/ 后缀平衡树 https://www.luogu.com.cn/blog/CHiCO/CHiCO-Suffix-Balance-Tree 【推荐】后缀自动机一·基本概念 http://hihocoder.com/problemset/problem/1441 后缀自动机二·重复旋律5 http://hihocoder.com/problemset/problem/1445 后缀自动机三·重复旋律6 http://hihocoder.com/problemset/problem/1449 SPOJ NSUBSTR https://www.luogu.com.cn/problem/SP8222 对每个 i,求长为 i 的且出现次数最多的子串的出现次数,可以在 dfs rev 的同时求出 maxCnt[o.len].max(o.cnt),最后倒序更新一下 maxCnt 后缀自动机四·重复旋律7 http://hihocoder.com/problemset/problem/1457 后缀自动机五·重复旋律8 http://hihocoder.com/problemset/problem/1465 后缀自动机六·重复旋律9 http://hihocoder.com/problemset/problem/1466 https://ac.nowcoder.com/acm/contest/37092#question todo 题单 https://www.luogu.com.cn/training/3202 模板题:子串出现次数*子串长度的最大值 https://www.luogu.com.cn/problem/P3804 多串最长公共子串(另见后缀数组)SPOJ LCS2 https://www.luogu.com.cn/problem/SP1812 - https://loj.ac/p/171 - LC1923 https://leetcode.cn/problems/longest-common-subpath/ 第 k 小子串(也可以用后缀数组做,见题解区)SPOJ SUBLEX https://www.luogu.com.cn/problem/SP7258 - TJOI15 弦论 https://www.luogu.com.cn/problem/P3975 https://www.luogu.com.cn/problem/P4070 动态本质不同子串个数(也可以用后缀数组做,见题解区) https://www.luogu.com.cn/problem/P6292 区间本质不同子串个数(与 LCT 结合) SPOJ NSUBSTR2 https://www.luogu.com.cn/problem/SP8747 动态子串出现次数(与 LCT 结合) todo https://codeforces.com/problemset/problem/235/C 2700 循环同构(也可以用后缀数组做) todo LC2730 变形:相邻字符改成相邻子串(连续重复子串) https://leetcode.cn/problems/find-the-longest-semi-repetitive-substring/ */ // 如果超时/超内存,改用预分配内存池 + func init() { debug.SetGCPercent(-1) } 的写法(如下) // 如果仍然超内存且环境为 64 位,则需要把指针改成 uint32 下标的写法 https://www.luogu.com.cn/record/76046834 https://www.luogu.com.cn/record/76047438 //const mx int = 1e7 //var samNodes [2 * mx]*node type next [26]*node // map[int]*node type node struct { // 也叫 state // len 为该节点(endpos 等价类)中最长的子串长度 // fa.len+1 为该节点(endpos 等价类)中最短的子串长度 // 等价类大小为 len-fa.len // 等价类中的每个子串都是其最长子串的一系列连续后缀,即长度组成了区间 [fa.len+1,len] // 这一系列连续后缀之后更短的后缀,要去 fa, fa.fa, fa.fa.fa, ... 上找 fa *node ch next len int i int // 字符在原串的下标 // EXTRA: 所有 node->node.fa 的反向边 rev 构成一棵以 sam.nodes[0] 为根的树(sam.nodes[0] 表示空串) // 性质:(结合 AABABA 的 rev 理解 https://public.noi.top/image/1583315246012.png) // 1. 每个节点中的子串,是【紧跟】其儿子节点的子串的后缀 // 2. 每个节点中的子串,都是以该节点为根的子树中的所有子串的后缀 // 3. 两个节点的 LCA 对应着这两个节点的子串的最长公共后缀 // 4. 叶子节点一定包含原串的前缀(根据性质 1 反证可得),但原串的前缀不一定在叶子节点中(比如前缀 A 是前缀 AA 的后缀) // 5. 由于【子串】等价于【前缀的后缀】,因此求子串在原串中的出现次数, // 可以先通过在 SAM 上找到该子串所处的节点,然后求以该节点为根的子树中,多有少个包含原串前缀的节点(性质 2) // 这可以通过在 rev 上统计子树信息来预处理得到 rev []*node // debug 用 cnt int debugSuffixes []string } type sam struct { nodes []*node last *node originS string // 可选 } // 构建字符串 s 的后缀自动机 // 基础用法见下面的 index func newSam(s string) *sam { m := &sam{} m.originS = s m.last = m.newNode(nil, next{}, 0, -1) m.buildSam(s) m.buildRev() return m } func (m *sam) newNode(fa *node, ch next, length, i int) *node { // 如果 next 是 map 则需要 clone // ch = maps.Clone(ch) o := &node{fa: fa, ch: ch, len: length, i: i} m.nodes = append(m.nodes, o) return o } // 注:这里可以不用传入 i,下面的 i 改为 m.last.i+1 即可 func (m *sam) append(i, c int) { last := m.newNode(m.nodes[0], next{}, m.last.len+1, i) last.cnt = 1 // 或者标记这是个前缀节点 for o := m.last; o != nil; o = o.fa { p := o.ch[c] if p == nil { o.ch[c] = last continue } if o.len+1 == p.len { last.fa = p } else { np := m.newNode(p.fa, p.ch, o.len+1, p.i) p.fa = np for ; o != nil && o.ch[c] == p; o = o.fa { o.ch[c] = np } last.fa = np } break } m.last = last } func (sam) ord(c byte) byte { return c - 'a' } // 'A' func (sam) chr(v byte) byte { return v + 'a' } func (m *sam) buildSam(s string) { for i, b := range s { m.append(i, int(m.ord(byte(b)))) } } func (m *sam) buildRev() { for _, o := range m.nodes[1:] { o.fa.rev = append(o.fa.rev, o) } } // 返回一个最长的子串,其长度乘出现次数最大 // 如果有多个长度乘出现次数相同的子串,返回其中字典序最小的 func (m *sam) example() (ans string) { mx := -1 var dfs func(*node) int dfs = func(v *node) int { for _, w := range v.rev { v.cnt += dfs(w) } res := v.len * v.cnt sub := m.originS[v.i+1-v.len : v.i+1] // 对应的子串 if res > mx || res == mx && sub < ans { // 字符串比较大小可以用后缀数组优化 mx = res ans = sub } return v.cnt } dfs(m.nodes[0]) return } // 等价于 strings.Index(s, substr) // 时间复杂度为 O(len(substr)) // LC2564 https://leetcode.cn/problems/substring-xor-queries/ func (m *sam) index(substr string) int { o := m.nodes[0] for _, b := range substr { b := m.ord(byte(b)) if o.ch[b] == nil { return -1 } o = o.ch[b] } return o.i - len(substr) + 1 } // 返回 s 在 sam 中的最长前缀 // 特别地,若 s 在 sam 中,则返回 s 的长度 // JSOI12 https://www.luogu.com.cn/problem/P5231 func (m *sam) longestPrefix(s string) int { o := m.nodes[0] for i, b := range s { b := m.ord(byte(b)) if o.ch[b] == nil { return i } o = o.ch[b] } return len(s) } // 多串 LCS 则去掉代码中的 // // 最后答案为 max(m.nodes[i].ans) func (m *sam) lcs(s string) (ans int) { //for _, o := range m.nodes { // o.maxCommon = 0 //} root := m.nodes[0] o, common := root, 0 for _, b := range s { // 下面的结构形式十分类似 KMP b := m.ord(byte(b)) for o != root && o.ch[b] == nil { o = o.fa common = o.len } if o.ch[b] != nil { o = o.ch[b] common++ if common > ans { ans = common } } //o.maxCommon = max(o.maxCommon, common) } //m.dfs(root) //for _, o := range m.nodes { // o.ans = min(o.ans, o.maxCommon) // newNode 时 ans 初始化成 len //} return } // debug 用 // 可以用 sam_test.go 跑跑看 func (m *sam) printSAM() { root := m.nodes[0] // 如果 append 了新的元素导致 rev 树的结构变化,可以直接重建 for _, o := range m.nodes { o.rev = nil o.debugSuffixes = nil } m.buildRev() var makeSuf func(*node, string) makeSuf = func(v *node, s string) { v.debugSuffixes = append(v.debugSuffixes, s) for i, w := range v.ch { if w != nil { makeSuf(w, s+string(m.chr(byte(i)))) } } } makeSuf(root, "") var dfs func(*node, int) dfs = func(v *node, depth int) { isPrefixNode := v.cnt == 1 for _, w := range v.rev { dfs(w, depth+1) v.cnt += w.cnt } sort.Slice(v.debugSuffixes, func(i, j int) bool { return len(v.debugSuffixes[i]) > len(v.debugSuffixes[j]) }) // 以 rev 树的形式打印所有【本质不同子串】(打印结果需要从下往上看) // 方括号表示原串的前缀,圆括号表示非前缀 // 如果 SAM 构建正确,那么 debugSuffixes 必然是 debugSuffixes[0] 的一系列连续后缀 if depth > 0 { var suf string if isPrefixNode { // 前缀节点 suf = Sprintf("[%s]", v.debugSuffixes[0]) // 由于上面从长到短排序了,这里最长的一定是前缀 if len(v.debugSuffixes) > 1 { suf += Sprintf(" (%s)", strings.Join(v.debugSuffixes[1:], " ")) } } else { // 非前缀节点 suf = Sprintf("(%s)", strings.Join(v.debugSuffixes, " ")) } Print(strings.Repeat(" ", depth-1)) Println(v.cnt, suf) } } dfs(root, 0) } // 广义 SAM // https://oi-wiki.org/string/general-sam/ // // todo https://www.luogu.com.cn/problem/P6139 // https://codeforces.com/problemset/problem/1437/G // 有一组互不相同的字符串list,求在给定字符串s中,有几个子串出现在list中。 s比较小,但list可能很大。 // 不在其它字符串中出现的最短字典序最小子串 https://leetcode.cn/problems/shortest-uncommon-substring-in-an-array/