后缀自动机 部分理解与思考

后缀自动机 SAM

学习可以参考oi-wiki和luogu的一些题解。

本问只是对一些应用上的思考。

学习材料

https://oi-wiki.org/string/sam/#_8 反正就是要反复看。

##报错调试的角度

1. 空间算错

因为空间是2N的,所以要考虑清楚最长的串多大。

我会设置一个专门的全局常量给SAM的数据数组。

有时候,SAM之外申请的数组很容易搞错。(尤其是拓扑排序用的数组)

2. last 和 size(total)

last的编号不一定等于状态个数,因为clone时新建点的编号是大于last的。

3. 关于初始化

看到一种做法是对数组空间随用随清的做法,和AC自动机类似。我觉得比较清晰。

// new node
int nuw(int id) {
    memset(nxt[id], 0, sizeof(nxt[id]));
    len[id] = link[id] = 0;
    return id;
}

快速手模后缀链接(Parent Tree)

本节的【后缀链接】均指代所有后缀链接构成的“Parent Tree”
一般情况下手模的时候,也是需要画出所有后缀链接。

自动机在构建的过程中会有clone操作,对连边进行修改。如果直接按照代码逻辑去模拟会有很多涂改,并不方便。

一般手模是从自动机状态的定义出发:(参考证明类教程)

  • 把原串的所有子串按照END_POS进行分组,每个组就是自动机里的一个状态,每个状态取最长的子串来辨识。
  • 后缀链接就是从一个状态连向它的最长后缀所在的状态。

还有一些小技巧:

  • 第一层的END_POS就是每种字母出现的位置分组。不过,注意他们的不一定就是1。比如下面这个例子中的{2,5},实际是用肉眼算出每个位置对应前缀的公共后缀。
  • 每个状态下面的分支都是对当前状态的子集进行划分,子状态的辨识字符串尽量长,并且保持同个状态里END_POS一致。
    例子1例子2

在比较短的串里,肉眼观察其实很快,多画几次就会熟悉了。我觉得手模样例也是解题找思路的重要一步。

应用1:子串出现次数(与对应长度)

luogu P3804 【模板】后缀自动机 (SAM)

给定一个只包含小写字母的字符串S,
请你求出 S 的所有出现次数不为 1 的子串的出现次数乘上该子串长度的最大值。

树(针对后缀链接)上每个节点,代表一些子串。
每个节点所包含的子串,在原串中出现的次数,等于这个点所在子树endpos的大小。
每个节点的len表示这个节点状态最长串的长度。
拓扑序遍历后缀链接就可以枚举出答案了。

// luogu-judger-enable-o2
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cctype>

using namespace std;
const int N = 1000010;
#define rg int
typedef long long ll;
/*
 * @notice:
 *  - tot计数从1开始,1号节点代表空节点(也就是根)。
 *
 * */

struct SAM {
    const int begin = 1;
    int nxt[N << 1][26];
    int link[N << 1];
    int len[N << 1];
    int siz[N << 1];

    int las = begin;
    int tot = 1;

    inline void init(const int n) {
        len[begin] = 0;
        link[begin] = 0;
        las = begin;
        tot = 1;
    }

    // start from 0
    inline void build(const char s[], const int &length) {
        for (int i = 0; i < length; ++i) {
            this->extend(s[i] - 'a');
        }
    }

    inline void extend(int c) {
        int p = las;
        int cur = ++tot;
        len[cur] = len[p] + 1;
        siz[cur] = 1;
        las = cur;

        while (p > 0 && !nxt[p][c]) {
            nxt[p][c] = cur;
            p = link[p];
        }

        if (p == 0) {
            link[cur] = 1;
        } else {
            int q = nxt[p][c];
            if (len[q] == len[p] + 1) {
                link[cur] = q;
            } else {
                int clone = ++tot;
                memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
                link[clone] = link[q];
                len[clone] = len[p] + 1;
                link[q] = link[cur] = clone;
                while (p > 0 && nxt[p][c] == q) {
                    nxt[p][c] = clone;
                    p = link[p];
                }
            }
        }
    }
} sam;

const int MAXN = 1000059;
char s[MAXN];
ll ans;
int n;
int stk[MAXN << 1], top;
int deg[MAXN << 1];

int main() {
    ans = 0;
    scanf("%s", s + 1);
    n = strlen(s + 1);
//    cerr << n << "  " << (s + 1) << endl;

    sam.build(s + 1, n);

    for (int i = 1; i <= sam.tot; ++i) {
        deg[sam.link[i]]++;
    }

    for (int j = sam.tot; j >= 1; --j) {
        if (deg[j] == 0) {
            stk[top++] = j;
        }
    }

    while (top) {
        int cur = stk[--top];
        int pre = sam.link[cur];

        if (sam.siz[cur] > 1) {
            ans = max(ans, 1ll * sam.len[cur] * sam.siz[cur]);
        }

        sam.siz[pre] += sam.siz[cur];
        deg[pre]--;

        if (deg[pre] == 0 && pre != 1) {
            stk[top++] = pre;
        }
    }

//    cerr << sam.tot << endl;
    printf("%lld", ans);
    return 0;
}

应用2:最小循环移位

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

给定一个字符串s。找出字典序最小的循环移位。

套路就是S+S包含s的所有是循环似移位。
在自动机的next图上按照顺序搜索长度为n的路径。
但是这种题要小心,自动机的空间应该是四倍范围。

// luogu-judger-enable-o2
#include<iostream>
#include<cstdio>
#include<map>
#include<vector>

using namespace std;
const int N = 6e5 + 59;
typedef long long ll;

/*
 * @notice:
 *  - tot计数从1开始,1号节点代表空节点(也就是根)。
 *
 * */

struct SAM {
    const int begin = 1;
    map<int, int> nxt[N << 1];
    int link[N << 1];
    int len[N << 1];
    int siz[N << 1];

    int las = begin;
    int tot = 1;

    inline void init(const int n) {
        len[begin] = 0;
        link[begin] = 0;
        las = begin;
        tot = 1;
    }

    // start from 0
    inline void append(const vector<int> &a) {
        for (auto ai : a) {
            extend(ai);
        }
    }

    inline void extend(int c) {
        int p = las;
        int cur = ++tot;
        len[cur] = len[p] + 1;
        siz[cur] = 1;
        las = cur;

        while (p > 0 && !nxt[p][c]) {
            nxt[p][c] = cur;
            p = link[p];
        }

        if (p == 0) {
            link[cur] = 1;
        } else {
            int q = nxt[p][c];
            if (len[q] == len[p] + 1) {
                link[cur] = q;
            } else {
                int clone = ++tot;
                nxt[clone] = nxt[q];
//                memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
                link[clone] = link[q];
                len[clone] = len[p] + 1;
                link[q] = link[cur] = clone;
                while (p > 0 && nxt[p][c] == q) {
                    nxt[p][c] = clone;
                    p = link[p];
                }
            }
        }
    }
} sam;

int n;
int stk[N], top = 0;

bool DFS(int u) {
    for (auto pi : sam.nxt[u]) {
        stk[top++] = pi.first;
        if (top == n) return true;
        if (DFS(pi.second)) return true;
        --top;
    }
    return false;
}

int main() {
    scanf("%d", &n);

    vector<int> a(n);

    for (auto &ai : a) {
        scanf("%d", &ai);
    }
    sam.append(a);
    sam.append(a);

    DFS(sam.begin);

    for (int i = 0; i < top; ++i) {
        printf("%d%c", stk[i], " \n"[i + 1 == top]);
    }

    return 0;
}

应用3:本质不同的子串个数

P4070 [SDOI2016]生成魔咒

按顺序输出所有前缀的本质不同子串的个数。

性质:一个串的本质不同子串个数等于 Σ [ l e n ( u ) − l e n ( l i n k ( u ) ) ] \Sigma{[len(u) - len(link(u))]} Σ[len(u)len(link(u))]
再简化一点,只要针对last维护答案即可。

// luogu-judger-enable-o2
#include<iostream>
#include<cstdio>
#include<map>
#include<vector>

using namespace std;
const int N = 1e5 + 59;
typedef long long ll;

/*
 * @notice:
 *  - tot计数从1开始,1号节点代表空节点(也就是根)。
 *
 * */

struct SAM {
    const int begin = 1;
    map<int, int> nxt[N << 1];
    int link[N << 1];
    int len[N << 1];
    int siz[N << 1];

    int las = begin;
    int tot = 1;

    inline void init(const int n) {
        len[begin] = 0;
        link[begin] = 0;
        las = begin;
        tot = 1;
    }

    // start from 0
    inline void append(const vector<int> &a) {
        for (auto ai : a) {
            extend(ai);
        }
    }

    inline void extend(int c) {
        int p = las;
        int cur = ++tot;
        len[cur] = len[p] + 1;
        siz[cur] = 1;
        las = cur;

        while (p > 0 && !nxt[p][c]) {
            nxt[p][c] = cur;
            p = link[p];
        }

        if (p == 0) {
            link[cur] = 1;
        } else {
            int q = nxt[p][c];
            if (len[q] == len[p] + 1) {
                link[cur] = q;
            } else {
                int clone = ++tot;
                nxt[clone] = nxt[q];
//                memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
                link[clone] = link[q];
                len[clone] = len[p] + 1;
                link[q] = link[cur] = clone;
                while (p > 0 && nxt[p][c] == q) {
                    nxt[p][c] = clone;
                    p = link[p];
                }
            }
        }
    }
} sam;

int n;

int main() {
    scanf("%d", &n);

    vector<int> a(n);
    sam.init(n);

    ll ans = 0;
    for (auto &ai : a) {
        scanf("%d", &ai);
        sam.extend(ai);
        ans += sam.len[sam.las] - sam.len[sam.link[sam.las]];
        printf("%lld\n", ans);
    }

    return 0;
}

广义后缀自动机

luogu一个模板
题解的文章中说的比较细致,但是也有点口语化。而OIwiki上的介绍比较正式,可以结合起来看。
它有两种构造方法,注意写的时候一些细节。

应用:统计多个串间每个串的独有子串

P4081 [USACO17DEC]Standing Out from the Herd P

输入n个串,对每个串统计有多少种不同子串是这个串独有的。

本来只想做狭义SAM的,但是看到题解里说广义SAM在线做只要每次加入串的时候重置last为root就可以了。
我觉得很不合理,于是去调查了一下,正确的做法是要额外特判的,如果直接这样做,会导致不明显的漏洞。(会创造许多无效状态污染整个自动机,详细看模板题的题解)

构造好自动机以后,在next图上遍历每个串,同时要遍历后缀链接,标记每个状态出现在哪个串中。(出现超过一次就可以认为无效了,比如-1)
统计个数是对应标记的len-len(link)求和。

但是我加了一个检查状态没有标记的断言居然生效了,让我怀疑这份代码的正确性了。

// luogu-judger-enable-o2
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<map>
#include<vector>
#include<cassert>
#include <algorithm>

using namespace std;
const int N = 1e5 + 59;
typedef long long ll;

/*
 * @notice:
 *  - tot计数从1开始,1号节点代表空节点(也就是根)。
 *
 * */

struct SAM {
    const int begin = 1;
    int nxt[N << 1][26];
    int link[N << 1];
    int len[N << 1];
//    int las = begin;
    int tot = 1;

    /*inline void init() {
        len[begin] = 0;
        link[begin] = 0;
        las = begin;
        tot = 1;
    }*/

    // start from 0
    inline void append(const string &s) {
        int last = begin;
        for (auto c : s) {
            last = extend(c - 'a', last);
        }
    }

    inline int extend(int c, int las) {
        if (nxt[las][c] && len[las] + 1 == len[nxt[las][c]]) {
            return nxt[las][c];
        }

        bool flag = false;
        int p = las;
        int cur = ++tot;
        len[cur] = len[p] + 1;
        las = cur;

        while (p > 0 && !nxt[p][c]) {
            nxt[p][c] = cur;
            p = link[p];
        }

        int clone;
        if (p == 0) {
            link[cur] = 1;
        } else {
            int q = nxt[p][c];
            if (len[q] == len[p] + 1) {
                link[cur] = q;
            } else {
                if (len[p] + 1 == len[cur]) {
                    flag = true;
                }
                clone = ++tot;
                memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
                link[clone] = link[q];
                len[clone] = len[p] + 1;
                link[q] = link[cur] = clone;
                while (p > 0 && nxt[p][c] == q) {
                    nxt[p][c] = clone;
                    p = link[p];
                }
            }
        }
        return flag ? clone : cur;
    }
} sam;

int n;
int tag[N << 1];
char str[N];
int res[N];
vector<string> a;

int main() {
    scanf("%d", &n);
    getchar();

    for (int i = 1; i <= n; ++i) {
        gets(str);
        a.emplace_back(string(str));
        sam.append(a.back());
    }

    int id = 0;
    for (auto s : a) {
//        cerr << s << endl;
        int cur = sam.begin;
        id++;
        for (auto c : s) {
            cur = sam.nxt[cur][c - 'a'];
            int tmp = cur;
            while (tmp > 1 && tag[tmp] != id && tag[tmp] != -1) {
                if (tag[tmp] == 0) tag[tmp] = id;
                else tag[tmp] = -1;
                tmp = sam.link[tmp];
            }
        }
    }

    for (int i = sam.begin + 1; i <= sam.tot; ++i) {
//        assert(tag[i] != 0);
        if (tag[i] != -1) {
            res[tag[i]] += sam.len[i] - sam.len[sam.link[i]];
        }
    }

    for (int i = 1; i <= n; ++i) {
        printf("%d\n", res[i]);
    }

    return 0;
}

实战1 :2019 ICPC 南昌站 网络赛 F

https://nanti.jisuanke.com/t/41353

https://blog.csdn.net/weixin_43823767/article/details/101149717

https://blog.csdn.net/m0_37809890/article/details/100639953

思路是推倒期望公式,统计前缀的本质不同子串。

题意

初始有一个长为L的字符串S。

现在执行M次操作,每次操作在S末尾添加一个字符c。

初始和每次操作后,求长N的随机串T的【美丽值】的期望,答案模998244353。

定义T的【美丽值】是所有子串的【价值】之和。

定义一个字符串str的【价值】如下:

如果它不是S子串,那么价值为0,否则为G(|str|),其中G是一个给定的K次多项式函数。

思路

因为是对S扩展,所以从S的所有子串对期望的贡献出发。

对于某个长度为 l e n len len的子串,其贡献的期望是:
f ( l e n ) = ( n − l e n + 1 ) ∗ G ( l e n ) 2 6 l e n f(len) = \frac{(n - len + 1) * G(len)}{26^{len}} f(len)=26len(nlen+1)G(len)
问题转化成对当前S求,所有本质不同子串的贡献和。

并且这个贡献是和长度有关的一个函数。

回顾本质不同串长度和的求法,SAM中每个状态都代表长度连续的一组子串等价类,类似的可以对 f f f求个前缀和,比如记作 F ( l e n ) F(len) F(len),只要预处理就能每次快速计算出答案了。

每个节点u的贡献可以就这样算:
F ( l e n [ u ] ) − F ( l e n [ l i n k [ u ] ] ) F(len[u]) - F(len[link[u]]) F(len[u])F(len[link[u]])

可能套路的点就在于,不断扩展一个字符,并且求子串长度有关的东西。

代码

// luogu-judger-enable-o2
#include<stdio.h>
#include<string.h>

using namespace std;
const int N = 6e5 + 59;
typedef long long ll;

struct SAM {
    const int begin = 1;

    int nxt[N << 1][26];
    int link[N << 1];
    int len[N << 1];

    int las = begin;    // last state
    int tot = 1;        // total states

    inline void init() {
        las = nuw(begin);
        tot = 1;
    }

    // start from zero
    inline void build(char s[], int length) {
        for (int i = 0; i < length; ++i) {
            this->extend(s[i] - 'a');
        }
    }

    // new node
    int nuw(int id) {
        memset(nxt[id], 0, sizeof(nxt[id]));
        len[id] = link[id] = 0;
        return id;
    }

    inline void extend(int c) {
        int p = las;
        int cur = nuw(++tot);
        len[cur] = len[p] + 1;
        las = cur;

        while (p > 0 && !nxt[p][c]) {
            nxt[p][c] = cur;
            p = link[p];
        }

        if (p == 0) {
            link[cur] = 1;
        } else {
            int q = nxt[p][c];
            if (len[q] == len[p] + 1) {
                link[cur] = q;
            } else {
                int clone = ++tot;
                memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
                link[clone] = link[q];
                len[clone] = len[p] + 1;
                link[q] = link[cur] = clone;
                while (p > 0 && nxt[p][c] == q) {
                    nxt[p][c] = clone;
                    p = link[p];
                }
            }
        }
    }
} sam;

const int MAXN = 3e5 + 59;
const ll MOD = 998244353;

ll fpow(ll a, ll b) {
    ll res = 1;
    a %= MOD;
    while (b) {
        if (b & 1) res = res * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return res;
}

int kase = 0;
int l, k, n, m;
char s[MAXN];
ll p[55];
ll a[MAXN + MAXN];
char ch[2];

int main() {
    scanf("%d", &kase);
    while (kase--) {
        scanf("%d%d%d%d", &l, &k, &n, &m);
        scanf("%s", s);
        for (int i = 0; i <= k; ++i) {
            scanf("%lld", p + i);
        }

        int slen = l + m;

        for (ll i = 1; i <= slen; ++i) {
            ll x = 1;
            a[i] = 0;
            if (i <= n) {
                for (ll j = 0; j <= k; ++j) {
                    a[i] = (a[i] + p[j] * x) % MOD;
                    x = x * i % MOD;
                }
                a[i] = (n - i + 1) * a[i] % MOD *
                       fpow(fpow(26, i), MOD - 2) % MOD;
            }
            a[i] = (a[i] + a[i - 1]) % MOD;
        }

        sam.init();
        sam.build(s, l);

        ll res = 0;
        for (int i = 2; i <= sam.tot; ++i) {
            res = (res + a[sam.len[i]] + MOD - a[sam.len[sam.link[i]]]) % MOD;
        }
        printf("%lld\n", res);
        while (m--) {
            scanf("%s", ch);
            sam.extend(ch[0] - 'a');
            int i = sam.las;
            res = (res + a[sam.len[i]] + MOD - a[sam.len[sam.link[i]]]) % MOD;
            printf("%lld\n", res);
        }
    }

    return 0;
}
/*
abbbabbb
 */

实战2 :2019 CCPC Final C (大算法题)

https://blog.csdn.net/m0_37809890/article/details/103515142

对我来说过难,skip了(冠军题)

实战3 :2019 CCPC 网络赛 C (大算法题)

https://blog.csdn.net/weixin_43823767/article/details/102924625

题意

对一个字符串S,询问Q次,每次求子串S[l,r]在S中第k次出现的位置。

思路

这不是就是SAM的parent树dfs序建主席树吗?(震声)

我完全了解了。

套路归套路,其实不难想:

应用SAM的Parent树的一个性质,结点u的子树大小就代表这个结点所代表的子串的出现次数。

然后因为dfs序就可以用主席树求子树上的第k大的位置,所以这道题主要做法就出来了。

但还有个问题是快速找到询问的串所在的最小节点。

因为如果每个子串都反复跳link是比较慢的,可以结合树上ST的操作优化跳link。

思路很明确但就是编码有很大难度。

我对主席树还不是很熟练,skip。

实战 4 :2017 ICPC 沈阳站 网络赛 A

https://blog.csdn.net/zhenlingcn/article/details/77926019

acm.hdu.edu.cn/showproblem.php?pid=6194

题意

给一个字符串,问出现K次的子串有多少个。

思路

统计每个节点子树endpos的数量,表示节点所代表的每个子串的出现的次数。

在做这个题的过程中,因为我代码经过了一些修整,导致错误地认为子树大小就代表出现次数。实则不然,因为clone节点不属于endpos

endpos就是那些代表了前缀的点。

代码

/**
 *  @Source: myself
 *  @Author: Tieway59
 *  @Complexity: $O(|S|)$
 *  @Description:
 *      求原串S中出现次数恰号是K的子串的种数
 *      !注意修改 build 中的字符类型
 *      !注意extend是int参数
 *      !注意SAM外数组的大小
 *
 *  @Example:
 *      2
 *      2
 *      abcabc
 *      3
 *      abcabcabcabc
 *
 *      6
 *      9
 *
 *  @Verification:
 *      2017 ICPC 沈阳站 网络赛 A
 *      acm.hdu.edu.cn/showproblem.php?pid=6194
 *
 */
#include<stdio.h>
#include<string.h>
#include<iostream>

using namespace std;
typedef long long ll;

const int MAXN = 1e5 + 59;
const ll MOD = 998244353;

const int SAMN = MAXN << 1;

struct SAM {
    const int begin = 1;

    int nxt[SAMN][26];
    int link[SAMN];
    int len[SAMN];
    int cnt[SAMN];

    int las = begin;    // last state
    int tot = 1;        // total states

    inline void init() {
        las = nuw(begin, false);
        tot = 1;
    }

    // start from zero
    inline void build(char s[], int length) {
        for (int i = 0; i < length; ++i) {
            extend(s[i] - 'a');
        }
    }

    // new node
    int nuw(int id, bool is_endpos) {
        memset(nxt[id], 0, sizeof(nxt[id]));
        len[id] = link[id] = 0;
        cnt[id] = int(is_endpos);
        return id;
    }

    inline void extend(int c) {
        int p = las;
        int cur = nuw(++tot, true);
        len[cur] = len[p] + 1;
        las = cur;

        while (p > 0 && !nxt[p][c]) {
            nxt[p][c] = cur;
            p = link[p];
        }

        if (p == 0) {
            link[cur] = 1;
        } else {
            int q = nxt[p][c];
            if (len[q] == len[p] + 1) {
                link[cur] = q;
            } else {
                int clone = nuw(++tot, false);
                memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
                link[clone] = link[q];
                len[clone] = len[p] + 1;
                link[q] = link[cur] = clone;
                while (p > 0 && nxt[p][c] == q) {
                    nxt[p][c] = clone;
                    p = link[p];
                }
            }
        }
    }
} sam;

int kase;
int n;
int k;
char s[MAXN];

int deg[SAMN];
int stk[SAMN], top;

int main() {
    scanf("%d", &kase);
    while (kase--) {
        scanf("%d", &k);
        scanf("%s", s);
        n = strlen(s);

        top = 0;
        sam.init();
        sam.build(s, n);
        memset(deg, 0, (sam.tot + 5) * sizeof(deg[0]));

        for (int i = sam.begin + 1; i <= sam.tot; ++i) {
            deg[sam.link[i]] += 1;
        }

        ll ans = 0;
        for (int u = sam.tot; u > sam.begin; --u) {
            if (deg[u] == 0 && u != sam.begin) {
                stk[top++] = u;
            }
        }

        while (top) {
            int v = stk[--top];
            int u = sam.link[v];

            if (sam.cnt[v] == k) {
                ans += sam.len[v] - sam.len[u];
            }

            sam.cnt[u] += sam.cnt[v];
            if (0 == (--deg[u]) && u != sam.begin) {
                stk[top++] = u;
            }
        }
        printf("%lld\n", ans);
    }

    return 0;
}

实战 5:2019 ICPC Latin American Regional Contests G

https://codeforc.es/gym/102428/problem/G

题意

给定一个串,然后有n个串,对每个串询问最少需要几个原串中的子串可以拼成目标串

思路

SAM有个性质在于,一个串T从SAM的初始状态开始匹配,能找到与T最长前缀相同的子串。

对于每个询问,从SAM的出是状态开始匹配,到找不到就回到初始状态,就可以保证每次都是取到最长的匹配,也就能保证匹配段数最少了。

代码

/**
 *  @Source: myself
 *  @Author: Tieway59
 *  @Complexity:
 *  @Description:
 *      给一个串S,询问Q个串ti最少需要几个S的字串可以拼成。
 *      |S| -> 2e5, sum(|ti|) -> 2e5
 *  @Example:
 *
 *  @Verification:
 *      https://codeforces.com/gym/102428/problem/G
 */

#include<stdio.h>
#include<string.h>
#include<iostream>

using namespace std;
typedef long long ll;

const int MAXN = 2e5 + 59;
const ll MOD = 998244353;

const int SAMN = MAXN << 1;

struct SAM {
    const int begin = 1;

    int nxt[SAMN][26];
    int link[SAMN];
    int len[SAMN];
//    int cnt[SAMN];

    int las = begin;    // last state
    int tot = 1;        // total states

    inline void init() {
        las = nuw(begin, false);
        tot = 1;
    }

    // start from zero
    inline void build(char s[], int length) {
        for (int i = 0; i < length; ++i) {
            extend(s[i] - 'A'); // char type
        }
    }

    // new node
    int nuw(int id, bool is_endpos) {
        memset(nxt[id], 0, sizeof(nxt[id]));
        len[id] = link[id] = 0;
//        cnt[id] = int(is_endpos);
        return id;
    }

    inline void extend(int c) {
        int p = las;
        int cur = nuw(++tot, true);
        len[cur] = len[p] + 1;
        las = cur;

        while (p > 0 && !nxt[p][c]) {
            nxt[p][c] = cur;
            p = link[p];
        }

        if (p == 0) {
            link[cur] = 1;
        } else {
            int q = nxt[p][c];
            if (len[q] == len[p] + 1) {
                link[cur] = q;
            } else {
                int clone = nuw(++tot, false);
                memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
                link[clone] = link[q];
                len[clone] = len[p] + 1;
                link[q] = link[cur] = clone;
                while (p > 0 && nxt[p][c] == q) {
                    nxt[p][c] = clone;
                    p = link[p];
                }
            }
        }
    }
} sam;


int n;
char s[MAXN], t[MAXN];

int main() {

    scanf("%s", s);
    scanf("%d", &n);

    sam.build(s, strlen(s));

    for (int _ = 0; _ < n; ++_) {
        scanf("%s", t);
        int len = strlen(t);
        int i = 0;
        int j = sam.begin;
        int res = 1;
//        printf("%s\n", t);

        while (i < len) {
            int ch = t[i] - 'A';
//            printf("%c %d\n", t[i], j);
            if (sam.nxt[j][ch] == 0) {
                j = sam.begin;

                if (sam.nxt[j][ch] == 0) {
                    res = -1;
                    break;
                } else {
                    res++;
                }
            }
            j = sam.nxt[j][ch];
            i = i + 1;
        }
        printf("%d\n", res);
    }

    return 0;
}
/*

SANTIAGO
3
TITA
SANTIAGO
NAS

 */

实战 6:2018 ICPC 焦作站 网络赛 H

https://nanti.jisuanke.com/t/A2018

题意

求原串S中出现次数在[A,B]之间的子串的个数

思路

统计cnt,拓扑序即可。

但是这道题数据范围其实不太明确,得赌一下1e5。

代码

/**
 *  @Source: myself
 *  @Author: Tieway59
 *  @Complexity: $O(|S|)$
 *  @Description:
 *      求原串S中出现次数在[A,B]之间的子串的种数
 *      !注意修改 build 中的字符类型
 *      !注意extend是int参数
 *      !注意SAM外数组的大小
 *
 *  @Example:
 *      AAA 2 3
 *      ABAB 2 2
 *
 *      2
 *      3
 *
 *  @Verification:
 *      2018 ICPC 焦作站 网络赛 H
 *      https://nanti.jisuanke.com/t/A2018
 *
 */
#include<stdio.h>
#include<string.h>
#include<iostream>

using namespace std;
typedef long long ll;

const int MAXN = 1e5 + 59;
const ll MOD = 998244353;

const int SAMN = MAXN << 1;

struct SAM {
    const int begin = 1;

    int nxt[SAMN][26];
    int link[SAMN];
    int len[SAMN];
    int cnt[SAMN];

    int las = begin;    // last state
    int tot = 1;        // total states

    inline void init() {
        las = nuw(begin, false);
        tot = 1;
    }

    // start from zero
    inline void build(char s[], int length) {
        for (int i = 0; i < length; ++i) {
            extend(s[i] - 'A'); // char type
        }
    }

    // new node
    int nuw(int id, bool is_endpos) {
        memset(nxt[id], 0, sizeof(nxt[id]));
        len[id] = link[id] = 0;
        cnt[id] = int(is_endpos);
        return id;
    }

    inline void extend(int c) {
        int p = las;
        int cur = nuw(++tot, true);
        len[cur] = len[p] + 1;
        las = cur;

        while (p > 0 && !nxt[p][c]) {
            nxt[p][c] = cur;
            p = link[p];
        }

        if (p == 0) {
            link[cur] = 1;
        } else {
            int q = nxt[p][c];
            if (len[q] == len[p] + 1) {
                link[cur] = q;
            } else {
                int clone = nuw(++tot, false);
                memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
                link[clone] = link[q];
                len[clone] = len[p] + 1;
                link[q] = link[cur] = clone;
                while (p > 0 && nxt[p][c] == q) {
                    nxt[p][c] = clone;
                    p = link[p];
                }
            }
        }
    }
} sam;


int A, B;
char s[MAXN];
int stk[SAMN], top;    // 小心范围
int deg[SAMN];            // 小心范围

int main() {

    while (scanf("%s %d %d", s, &A, &B) != EOF) {
        sam.init();
        sam.build(s, strlen(s));
        top = 0;

        for (int i = 0; i <= sam.tot; ++i) {
            deg[i] = 0;
        }
        for (int i = 2; i <= sam.tot; ++i) {
            deg[sam.link[i]]++;
        }
        for (int i = 2; i <= sam.tot; ++i) {
            if (deg[i] == 0) {
                stk[++top] = i;
            }
        }

        ll ans = 0;

        while (top) {
            int u = stk[top--];

            sam.cnt[sam.link[u]] += sam.cnt[u];
            if (0 == (--deg[sam.link[u]]) &&
                sam.link[u] != sam.begin) {
                stk[++top] = sam.link[u];
            }

            if (A <= sam.cnt[u] && sam.cnt[u] <= B) {
                ans += sam.len[u] - sam.len[sam.link[u]];
            }

        }
        printf("%lld\n", ans);
    }

    return 0;
}

代办

广义后缀自动机,我想分一篇来做了。

https://blog.csdn.net/qq_43544481/article/details/103451148

https://www.cnblogs.com/SovietPower/p/10772320.html#hberos-file-suggestion%E5%90%8E%E7%BC%80%E8%87%AA%E5%8A%A8%E6%9C%BA

2019-ACM-ICPC 徐州网络赛 M. Longest subsequence 序列自动机

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值