(v4 更新)0x20 字符串

0x21 字符串 I

哈希

字符串哈希

字符串哈希:字符串哈希要求数据有序。将字符串 \(s\) 视为一个 \(\mathrm{base}\) 进制数,即

\[f(s) \equiv \sum_{i = 1}^{l} s_i \times \mathrm{base}^{l - i} \pmod M \]

其中模数 \(M\) 尽量为大质数,并且尽量对两个大质数模数 \(M_1, M_2\) 各求一次哈希(双哈希

静态字符串区间哈希

\[\mathrm{pre}_i = \mathrm{pre}_{i - 1} \times \mathrm{base} + s_i \]

\[f(s[l : r]) = \mathrm{pre}_r - \mathrm{pre}_{l - 1} \times \mathrm{base}^{r - l + 1} \]

动态字符串区间哈希:区间哈希值可以合并。

设左右两个区间的长度与哈希值分别为 \((l_1, h_1), (l_2, h_2)\),将它们拼接形成大区间的长度与哈希值为

\[(l_1 + l_2, h_1 \times \mathrm{base}^{l_2} + h_2) \]

可以使用线段树动态维护。

卡自然溢出哈希(Hash Killer I)

\(\mathrm{base}\) 为偶数:此时 \(\mathrm{base}^{64} \equiv 0 \pmod{2^{64}}\)。所以两个长度 \(> 64\),且后 \(64\) 位相同的不完全相同字符串就会哈希冲突。

\(\mathrm{base}\) 为奇数:使用 Thue Morse 序列攻击。对于二进制串 \(s\),设 \(\overline{s}\) 表示 \(s\) 按位取反后得到的串。

Thue Morse 序列:\(t_0 = \mathtt{0}\)\(t_i = t_{i - 1} + \overline{t_{i - 1}}\)。此时

\[f(t_i) = \mathrm{base}^{2^{i - 1}} f(t_{i - 1}) + f(\overline{t_{i - 1}}) \\ f(\overline{t_i}) = \mathrm{base}^{2^{i - 1}} f(\overline{t_{i - 1}}) + f(t_{i - 1}) \]

\(h_i = f(t_i) - f(\overline{t_i})\),有 \(h_i = \left(\mathrm{base}^{2^{i - 1}} - 1\right)h_{i - 1}\)。由于

\[\mathrm{base}^{2^{i - 1}} - 1 = (\mathrm{base}^{2^{i - 2}} + 1)(\mathrm{base}^{2^{i - 3}} + 1)\cdots(b + 1)(b - 1) \]

其中每一项都是偶数,所以 \(2^i \mid \mathrm{base}^{2^{i - 1}} - 1\)。于是 \(2^{\frac{i(i + 1)}{2}} \mid h_i\),因此 \(t_{11}\)\(\overline{t_{11}}\) 就会哈希冲突。

Thue Morse 序列的规律:第 \(x\) 位上的值等于 \(\mathrm{popcount}(x) \bmod 2\)

卡大质数哈希(Hash Killer II):使用生日悖论攻击。

集合哈希

集合哈希:集合哈希要求数据无序。将集合的元素通过某种映射规则转化后,再通过某种具有交换律的运算结合。

可用的映射规则:以打乱原来可能有的某些性质。

  • 查表映射:将原数值 \(x\) 随机赋一个新数值 \(c_x\) 并存入表中。
  • 位运算映射:将原数值 \(x\) 经过 \(\boldsymbol{\mathrm{xorshift}}\) 得到一个新数值。

可用的结合方式

  • 使用加法结合:

\[f(S) = \sum_{x \in S} \mathrm{shift}(x) \]

  • 使用异或结合(异或哈希):此时出现偶数次元素无贡献,出现奇数次元素只贡献一次

\[f(S) = \bigoplus_{x \in S} \mathrm{shift}(x) \]

异或哈希判完全平方数:定义完全异或性函数 \(f\),对于任意正整数 \(x, y\) 都有 \(f(xy) = f(x) \oplus f(y)\)

先对 \(1 \sim n\) 进行一次线性筛。给其中的所有质数 \(p\) 对应的 \(f(p)\) 赋一个范围较大的随机数,其余合数的 \(f\) 值通过线性筛得出。

此时 \(x\) 为完全平方数,当且仅当 \(f(x) = 0\)

当待判断的数,包含的质因子在 \(1 \sim n\) 时,就可以根据所有质因子的 \(f\) 函数的异或和,求出该数的 \(f\) 函数。

kmp

0x21 kmp.cpp

// net[i] : s[1:i] 真前缀与真后缀匹配的最大长度(s[1:i] 的 border)
auto kmp(const auto &s) {
    int n = s.size() - 1; // 下标从 1 开始
    std::vector<int> net(n + 1);
    net[1] = 0;
    for (int i = 2, j = 0; i <= n; i ++) {
        while (j > 0 && s[j + 1] != s[i]) j = net[j];
        if (s[j + 1] == s[i]) j ++;
        net[i] = j;
    }
    return net;
}

// f[i] : t[1:i] 后缀与 s 前缀匹配的最大长度
auto kmp_match(const auto &s, const auto &t, const auto &net) {
    int n = s.size() - 1, m = t.size() - 1; // 下标从 1 开始
    std::vector<int> f(m + 1);
    for (int i = 1, j = 0; i <= m; i ++) {
        while (j > 0 && (j == n || s[j + 1] != t[i])) j = net[j];
        if (s[j + 1] == t[i]) j ++;
        f[i] = j;
    }
    return f;
}

Z Algorithm

0x21 Z-Algorithm.cpp

// Z[i] : lcp(s, s[i:n])
auto Zalgo(const auto &s) {
    int n = s.size() - 1; // 下标从 1 开始
    std::vector<int> Z(n + 1);
    Z[1] = n;
    for (int i = 2, l = 0, r = 0; i <= n; i ++) {
        Z[i] = i <= r ? std::min(Z[i - l + 1], r - i + 1) : 0;
        while (i + Z[i] <= n && s[1 + Z[i]] == s[i + Z[i]]) {
            Z[i] ++;
        }
        if (i + Z[i] - 1 > r) {
            l = i, r = i + Z[i] - 1;
        }
    }
    return Z;
}

// p[i] : lcp(s, t[i:m])
auto Zalgo_match(const auto &s, const auto &t, const auto &Z) {
    int m = t.size() - 1; // 下标从 1 开始
    std::vector<int> p(m + 1);
    for (int i = 1, l = 0, r = 0; i <= m; i ++) {
        p[i] = i <= r ? std::min(Z[i - l + 1], r - i + 1) : 0;
        while (i + p[i] <= m && s[1 + p[i]] == t[i + p[i]]) {
            p[i] ++;
        }
        if (i + p[i] - 1 > r) {
            l = i, r = i + p[i] - 1;
        }
    }
    return p;
}

manacher

0x21 manacher.cpp

// 初始化为形如 #a#b#c# 的字符串
auto manacher_init(const std::string &s) {
    std::string t = " #";
    for (char ch : s) {
        t += ch, t += "#";
    }
    return t;
}

// p[i] : 以 i 为中心的回文半径
auto manacher(const auto &s) {
    int n = s.size() - 1; // 下标从 1 开始
    std::vector<int> p(n + 1);
    p[1] = 1;
    for (int i = 2, k = 1; i <= n; i ++) {
        if (k + p[k] - 1 < i) {
            p[i] = 1;
        } else {
            p[i] = std::min(k + p[k] - i, p[k * 2 - i]);
        }
        while (i - p[i] > 0 && i + p[i] <= n && s[i - p[i]] == s[i + p[i]]) {
            p[i] ++;
        }
        if (i + p[i] > k + p[k]) {
            k = i;
        }
    }
    return p;
}
// 判断区间 [l, r] 是否回文(转换成 #a#b#c# 形式后):p[l + r] - 1 >= r - l + 1

最小表示法

0x21 最小表示法.cpp

// std::rotate(s.begin(), s.begin() + minrep(s), s.end())
int minrep(const auto &s) {
    int n = s.size(), k = 0, i = 0, j = 1;
    while (k < n && i < n && j < n) {
        if (s[(i + k) % n] == s[(j + k) % n]) {
            k ++;
        } else {
            s[(i + k) % n] > s[(j + k) % n] ? i += k + 1 : j += k + 1;
            if (i == j) i ++;
            k = 0;
        }
    }
    return std::min(i, j);
}

0x22 字符串 II

Trie

神功早已大成!日后补全功法。

AC 自动机

神功早已大成!日后补全功法。

SA

0x22 SA.cpp

// 后缀数组
namespace SA {
    int m;
    int sa[N], rk[N], height[N];
    int cnt[N], id[N], px[N];
    int tmp_rk[N];

    bool same(int x, int y, int k) {
        int p = x + k <= n ? tmp_rk[x + k] : -1,
            q = y + k <= n ? tmp_rk[y + k] : -1;
        return tmp_rk[x] == tmp_rk[y] && p == q;
    }

    void build() { // 注意特判 n = 1
        m = 256;
        for (int i = 1; i <= n; i ++) rk[i] = s[i];
        for (int i = 0; i <= m; i ++) cnt[i] = 0;
        for (int i = 1; i <= n; i ++) cnt[rk[i]] ++;
        for (int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
        for (int i = n; i >= 1; i --) sa[cnt[rk[i]] --] = i;

        for (int k = 1, p = 0; k < n; k <<= 1, m = p) {
            p = 0;
            for (int i = n - k + 1; i <= n; i ++) id[++ p] = i;
            for (int i = 1; i <= n; i ++)
                if (sa[i] > k) id[++ p] = sa[i] - k;

            for (int i = 0; i <= m; i ++) cnt[i] = 0;
            for (int i = 1; i <= n; i ++) cnt[px[i] = rk[id[i]]] ++;
            for (int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
            for (int i = n; i >= 1; i --) sa[cnt[px[i]] --] = id[i];

            for (int i = 1; i <= n; i ++) tmp_rk[i] = rk[i];

            p = 0;
            for (int i = 1; i <= n; i ++) rk[sa[i]] = same(sa[i - 1], sa[i], k) ? p : ++ p;
        }

        for (int i = 1, h = 0; i <= n; i ++) {
            if (h) h --;
            while (s[i + h] == s[sa[rk[i] - 1] + h]) h ++;
            height[rk[i]] = h;
        }
    }
}

height 数组

\[\mathrm{height}_i = \mathrm{LCP}(\mathrm{sa}_{i - 1}, \mathrm{sa}_i) \]

height 数组求后缀 LCP:后缀 \(\mathrm{sa}_i, \mathrm{sa}_j(i < j)\) 的最长公共前缀为

\[\mathrm{LCP}(\mathrm{sa}_i, \mathrm{sa}_j) = \min_{i < k \leq j} \{ \mathrm{height}_k \} \]

SAM

SAM:SAM 是一个状态自动机,形态为 DAG。有一个起点,若干个终点。

原串的所有子串,与 SAM 上从起点开始,以任意点结束的所有路径一一对应,不重不漏。

SAM 边的类型:SAM 主要考虑两种边。

  • 普通转移边:表示在当前状态所表示的串后面添加一个字符。
  • 后缀链接 \(\mathrm{link}_i\):表示将当前状态所表示的最短串的首字母删除。这类边构成一棵树,称作 parent 树(不一定满足 \(\mathrm{link}_i < i\))。

SAM 点数:点数 \(\leq 2n - 1\)

SAM 边数:边数 \(\leq 3m - 4\)

endpos 集合:子串 \(s\) 的所有终止位置的集合,记做 \(\mathrm{endpos}(s)\)

  • SAM 中的每一个状态,都对应一个 endpos 等价类。
  • SAM 中的每一个状态(endpos 等价类),包含的子串长度构成一个连续段,且短串是长串的后缀。
  • \(s_1\)\(s_2\) 的后缀,则 \(\mathrm{endpos}(s_1) \supseteq \mathrm{endpos}(s_2)\);否则 \(\mathrm{endpos}(s_1) \bigcap \mathrm{endpos}(s_2) = \varnothing\)

endpos 集合的维护:在 parent 树上使用可持久化线段树合并维护。

由于在 parent 树上,节点 \(p\) 的 endpos 集合为 \(\mathrm{subtree}(p)\) 中所有节点 endpos 集合的并集。为此,我们在 \(s[1 : i]\) 对应状态的 endpos 集合里插入位置 \(i\),然后使用数据结构合并子树的 endpos 集合。

0x22 SAM.cpp

// 后缀自动机
namespace SAM {
    const int SIZE = N * 2;

    int nodeCount, Last;
    struct node {
        int trans[26];
        int link, maxl;
    } t[SIZE];

    int create() {
        int p = ++ nodeCount;
        for (int c = 0; c < 26; c ++) t[p].trans[c] = 0;
        t[p].link = t[p].maxl = 0;
        return p;
    }

    void init() {
        nodeCount = 0, Last = create(); // 根节点为 1
    }

    void extend(int c) {
        int p = Last, np = Last = create();

        t[np].maxl = t[p].maxl + 1;
        for (; p && t[p].trans[c] == 0; p = t[p].link) {
            t[p].trans[c] = np;
        }

        if (!p) {
            t[np].link = 1;
        } else {
            int q = t[p].trans[c];
            if (t[q].maxl == t[p].maxl + 1) {
                t[np].link = q;
            } else {
                int nq = create();
                t[nq] = t[q], t[nq].maxl = t[p].maxl + 1;
                t[np].link = t[q].link = nq;
                for (; p && t[p].trans[c] == q; p = t[p].link) {
                    t[p].trans[c] = nq;
                }
            }
        }
    }
}

SAM 子串定位\(s[l : r]\) 在 SAM 中的对应状态):

先定位 \(s[1 : r]\) 在 SAM 中的对应状态(在构造 SAM 的过程中记录一下),然后在 parent 树上倍增,找到深度最浅的满足 \(\mathrm{maxl} \geq r - l + 1\) 的状态。

SAM 子串匹配给定模式串 \(S\) 与文本串 \(T\),对每个 \(r\) 求最小的 \(l\) 使得 \(T[l : r]\)\(S\) 的子串):

先对 \(S\) 建出 SAM。采用增量法,令 \(r\) 从左往右扫。维护当前匹配到的子串长度 \(\mathrm{len}\),以及在 SAM 上的对应状态 \(p\)

\(r\) 扩展时,不断地跳后缀链接(这里令 \(\mathrm{len}\) 变为对应状态的 \(\mathrm{maxl}\)),直到跳到根节点或存在相应的转移边为止。若存在相应的转移边,则走该转移边(这里令 \(\mathrm{len} \gets \mathrm{len} + 1\))。

int p = 1, len = 0; // 当前匹配到的状态,以及长度
for (int i = 1; i <= m; i ++) {
    int c = T[i] - 'a';
    while (p > 1 && !SAM::t[p].trans[c]) {
        p = SAM::t[p].link, len = SAM::t[p].maxl;
    }
    if (SAM::t[p].trans[c]) {
        p = SAM::t[p].trans[c], len ++;
    }
    // 此时的 p 与 len,就是以 i 为结尾时匹配到的最长状态,以及对应的长度
}

子 SAM给定模式串 \(S\) 与文本串 \(T\),让 \(T\)\(S[l : r]\) 中进行匹配):

判断由 \(S[l : r]\) 组成的子 SAM 是否存在相应的转移边,首先在原 SAM 就要存在相应的转移边,并且原 SAM 中新状态的 endpos 集合在区间 \([l + \mathrm{len}' - 1, r]\) 中至少要有一个元素。

失配时,应先尝试 \(\mathrm{len} \gets \mathrm{len} - 1\) 然后继续匹配,而非直接跳失配链接。

SAM 前端删除在一个状态的前面删除字符):

若长度等于当前状态的 \(\mathrm{minl}\),则跳后缀链接。

SAM 前端插入在一个状态的前面添加字符):

维护每个状态 \(u\) 对应 endpos 集合的任意一个元素 \(\mathrm{pos}_u\),就可以做到在原串中定位状态 \(u\)

  • 若长度等于当前状态的 \(\mathrm{maxl}\):则相当于在 parent 树上向某一个儿子走。在构建 parent 树的时候,预处理每个状态的极长串前加一个字符 \(c\),能走到哪个儿子即可(枚举 \(u\) 以及其儿子 \(v\),根据 \(\mathrm{pos}_v\) 在原串中定位,即可得知从 \(u\)\(v\) 需要添加什么字符)。
  • 若长度小于当前状态的 \(\mathrm{maxl}\):相当于要考虑当前状态是否能够容纳新串,根据 \(\mathrm{pos}_u\) 在原串中定位,判断新加的字符是否与原串对应位置上的字符匹配。

0x22 SAM 前端操作.cpp

// 后缀自动机(前端操作)
namespace SAM {
    const int SIZE = N * 2;
    int strL, str[N];

    int nodeCount, Last;
    struct node {
        int trans[26];
        int link, maxl, pos;
    } t[SIZE];

    std::vector<int> son[SIZE];
    int net[SIZE][26];

    int create() {
        int p = ++ nodeCount;
        for (int c = 0; c < 26; c ++) t[p].trans[c] = net[p][c] = 0;
        t[p].link = t[p].maxl = t[p].pos = 0;
        son[p].clear();
        return p;
    }

    void init() {
        nodeCount = 0, Last = create(); // 根节点为 1
        strL = 0;
    }

    void extend(int c) {
        int p = Last, np = Last = create();

        str[++ strL] = c, t[np].pos = strL;
        t[np].maxl = t[p].maxl + 1;
        for (; p && t[p].trans[c] == 0; p = t[p].link) {
            t[p].trans[c] = np;
        }

        if (!p) {
            t[np].link = 1;
        } else {
            int q = t[p].trans[c];
            if (t[q].maxl == t[p].maxl + 1) {
                t[np].link = q;
            } else {
                int nq = create();
                t[nq] = t[q], t[nq].maxl = t[p].maxl + 1;
                t[np].link = t[q].link = nq;
                for (; p && t[p].trans[c] == q; p = t[p].link) {
                    t[p].trans[c] = nq;
                }
            }
        }
    }

    void dfs_init(int u) {
        for (int v : son[u]) {
            net[u][str[t[v].pos - t[u].maxl]] = v;
            dfs_init(v);
        }
    }

    void build_tree() {
        for (int i = 2; i <= nodeCount; i ++) {
            son[t[i].link].push_back(i);
        }
        dfs_init(1);
    }

    void walk_front(int &p, int &len, int c) {
        if (len == t[p].maxl) {
            p = net[p][c];
        } else if (str[t[p].pos - len] != c) {
            p = 0;
        }
        len = p ? len + 1 : 0;
    }
}

SAM 补节点在 parent 树中,补充询问对应的状态):

parent 树是反串的后缀树,而后缀树又是后缀 trie 虚树化(仅保留所有后缀在后缀 trie 上的终止节点,以及两两 LCA)。

由于询问串 \(s[l : r]\) 在 SAM 上的状态可能不完全覆盖 \(\mathrm{minl} \sim \mathrm{maxl}\)(为该区间的一个前缀),于是我们离线将所有询问串在 SAM 上的状态找出来,然后将状态划分,建立有关询问串的虚拟节点(补充有关询问串的虚树结构)。此时所有询问串一定完整地覆盖了从当前状态到根的路径,且总节点数不超过 \(2n + m\)

简化了 parent 树的结构,降低了讨论难度。

广义 SAM

0x22 离线广义 SAM.cpp

// (离线)广义 SAM
namespace GSAM {
    const int SIZE = N * 2;
    int trieCount, trie[N][26], site[N];

    int nodeCount;
    struct node {
        int trans[26];
        int link, maxl;
    } t[SIZE];

    int newtrie() {
        int p = ++ trieCount;
        for (int c = 0; c < 26; c ++) trie[p][c] = 0;
        site[p] = 0;
        return p;
    }
    int newnode() {
        int p = ++ nodeCount;
        for (int c = 0; c < 26; c ++) t[p].trans[c] = 0;
        t[p].link = t[p].maxl = 0;
        return p;
    }

    void init() {
        trieCount = 0, newtrie(); // trie 根节点为 1
        nodeCount = 0, newnode(); // GSAM 根节点为 1
    }

    void insert(const std::string &s) {
        int p = 1;
        for (char ch : s) {
            int v = ch - 'a';
            if (!trie[p][v]) trie[p][v] = newtrie();
            p = trie[p][v];
        }
    }

    int extend(int Last, int c) {
        int p = Last, np = newnode();
        
        t[np].maxl = t[p].maxl + 1;
        for (; p && t[p].trans[c] == 0; p = t[p].link) {
            t[p].trans[c] = np;
        }

        if (!p) {
            t[np].link = 1;
        } else {
            int q = t[p].trans[c];
            if (t[q].maxl == t[p].maxl + 1) {
                t[np].link = q;
            } else {
                int nq = newnode();
                t[nq] = t[q], t[nq].maxl = t[p].maxl + 1;
                t[np].link = t[q].link = nq;
                for (; p && t[p].trans[c] == q; p = t[p].link) {
                    t[p].trans[c] = nq;
                }
            }
        }
        return np;
    }

    void build() {
        std::queue<int> q;
        q.push(1), site[1] = 1;

        while (q.size()) {
            int u = q.front(); q.pop();
            for (int i = 0; i < 26; i ++) {
                if (trie[u][i]) {
                    site[trie[u][i]] = extend(site[u], i);
                    q.push(trie[u][i]);
                }
            }
        }
    }
}

0x22 在线广义 SAM.cpp

// (在线)广义 SAM
namespace GSAM {
    const int SIZE = N * 2;

    int nodeCount, Last;
    struct node {
        int trans[26];
        int link, maxl;
    } t[SIZE];

    int create() {
        int p = ++ nodeCount;
        for (int c = 0; c < 26; c ++) t[p].trans[c] = 0;
        t[p].link = t[p].maxl = 0;
        return p;
    }

    void init() {
        nodeCount = 0, Last = create(); // GSAM 根节点为 1
    }

    void extend(int c) {
        int p = Last;
        if (t[p].trans[c]) {
            int q = t[p].trans[c];
            if (t[q].maxl == t[p].maxl + 1) {
                Last = q;
            } else {
                int nq = create();
                t[nq] = t[q], t[nq].maxl = t[p].maxl + 1;
                t[q].link = nq;
                for (; p && t[p].trans[c] == q; p = t[p].link) {
                    t[p].trans[c] = nq;
                }
                Last = nq;
            }
        } else {
            int np = Last = create();

            t[np].maxl = t[p].maxl + 1;
            for(; p && t[p].trans[c] == 0; p = t[p].link) {
                t[p].trans[c] = np;
            }

            if (!p) {
                t[np].link = 1;
            } else {
                int q = t[p].trans[c];
                if (t[q].maxl == t[p].maxl + 1) {
                    t[np].link = q;
                } else {
                    int nq = create();
                    t[nq] = t[q], t[nq].maxl = t[p].maxl + 1;
                    t[np].link = t[q].link = nq;
                    for (; p && t[p].trans[c] == q; p = t[p].link) {
                        t[p].trans[c] = nq;
                    }
                }
            }
        }
    }
}
// 在插入一个串之前,需要令 GSAM::Last = 1

PAM

PAM:PAM 是一个状态自动机,形态为由两棵树构成的森林。有两个起点(偶根为 \(0\),奇根为 \(1\)),若干个终点。

原串的所有回文子串,与 PAM 上的所有状态一一对应,不重不漏。

PAM 边的类型

  • 普通转移边:表示在当前状态所表示的串前后各加一个字符。
  • 后缀链接 \(\mathrm{link}_i\):表示当前状态所表示的串的最长回文后缀。这类边构成一棵树,称作 parent 树。

偶根的 link 指向奇根,而我们不关心奇根的 link(因为奇根不可能失配,奇根转移出的下一个状态是单个字符,必定为回文串)。

PAM 点数(本质不同回文子串个数):点数 \(\leq n\)

因为在任何串后面加一个字符,运用反证法可以得知新增的本质不同回文子串个数至多增加 \(1\)

0x22 PAM.cpp

// 回文自动机
namespace PAM {
    int strL, str[N];

    int nodeCount, Last;
    struct node {
        int trans[26];
        int link, len;
    } t[N];

    int create() {
        int p = ++ nodeCount;
        for (int c = 0; c < 26; c ++) t[p].trans[c] = 0;
        t[p].link = t[p].len = 0;
        return p;
    }

    void init() {
        str[strL = 0] = -1;
        nodeCount = -1, Last = create(), Last = create(); // 偶根为 0,奇根为 1
        t[0].len = 0, t[0].link = 1;
        t[1].len = -1;
    }

    int find(int p) {
        while (str[strL - t[p].len - 1] != str[strL]) {
            p = t[p].link;
        }
        return p;
    }

    void extend(int c) {
        str[++ strL] = c;
        int p = find(Last);
        if (!t[p].trans[c]) {
            int np = create();
            t[np].len = t[p].len + 2;
            t[np].link = t[find(t[p].link)].trans[c];
            t[p].trans[c] = np;
        }
        Last = t[p].trans[c];
    }
}

PAM 的 half 指针:表示当前状态所表示的串,长度小于等于该串一半的最长回文后缀。

0x22 PAM half.cpp

// 回文自动机(half 指针)
namespace PAM {
    int strL, str[N];

    int nodeCount, Last;
    struct node {
        int trans[26];
        int link, half, len;
    } t[N];

    int create() {
        int p = ++ nodeCount;
        for (int c = 0; c < 26; c ++) t[p].trans[c] = 0;
        t[p].link = t[p].half = t[p].len = 0;
        return p;
    }

    void init() {
        str[strL = 0] = -1;
        nodeCount = -1, Last = create(), Last = create(); // 偶根为 0,奇根为 1
        t[0].len = 0, t[0].link = 1;
        t[1].len = -1;
    }

    int find(int p) {
        while (str[strL - t[p].len - 1] != str[strL]) {
            p = t[p].link;
        }
        return p;
    }
    int find_half(int p, int L) {
        while (t[p].len + 2 > L || str[strL - t[p].len - 1] != str[strL]) {
            p = t[p].link;
        }
        return p;
    }

    void extend(int c) {
        str[++ strL] = c;
        int p = find(Last);
        if (!t[p].trans[c]) {
            int np = create();
            t[np].len = t[p].len + 2;
            t[np].link = t[find(t[p].link)].trans[c];
            t[np].half = t[np].len > 2 ? t[find_half(t[p].half, t[np].len / 2)].trans[c] : t[np].link;
            t[p].trans[c] = np;
        }
        Last = t[p].trans[c];
    }
}
posted @ 2022-12-19 10:24  Calculatelove  阅读(228)  评论(0)    收藏  举报