嘘~ 正在从服务器偷取页面 . . .

洛谷P9089 「SvR-2」Work 题解


洛谷P9089 「SvR-2」Work 题解

题目链接:P9089 「SvR-2」Work

题意

给定 $n$ 个由小写字母组成的字符串,定义第 $i$ 个字符串的价值为其有意义的子串的数量(如果有多个本质相同的子串也统计多次),第 $i$ 个字符串的一个子串有意义,当且仅当这个子串能被分成若干个串,其中每个串都是这 $n$ 个字符串中任意一个字符串的任意一个后缀。

这里有一个 $n=4$ 的例子:

int
printf
scanf
ntnt

  • 对于 printf 这个字符串而言,intf 是有意义的,因为可以表示成 intf ,分别是 intscanf 的后缀,而 rint 则不是。

  • 对于 ntnt 这个字符串而言,ntnt 也是有意义的,因为可以表示成 ntnt,它们都是 int 同一个后缀,或者可以表示成 ntnt,是 ntnt 的一个后缀。

现在,小 Z 想知道这 $n$ 个字符串价值之和。

输入格式

第一行一个整数 $n$。

之后 $n$ 行,每行一个字符串。

输出格式

一行一个整数,表示价值之和。

数据范围

令 $s_i$ 表示第 $i$ 个字符串长度,$1\le n \le 5\times10^5$,$n\le \sum\limits \lvert s_i \rvert \le 10^6$。

先把所有串翻转一下,此时一个串 $S$ 有意义当且仅当它可以被分解成若干个已知串的前缀。

不妨定义一个串的匹配是它的后缀与某个 $s_i$ 的前缀相同,则这个后缀就是它的一个匹配。

则一个串 $S$ 有意义的充要条件是 $S$ 的每一个前缀都有匹配。

我们可以通过每次删掉一个串的最短匹配来验证这个串有意义,因为我们一定能找到这样的匹配。

令 $g_u$ 表示 $u$ 节点代表的字符串在 Trie 中出现过的最短后缀。

当 $\operatorname{fail}(u)$ 存在时 $g_u \gets g_{\operatorname{fail}(u)}$ ,否则 $g_u$ 的最短后缀就是它自己。

令 $f_i$ 表示(当前串)以 $i$ 结尾的后缀有多少个是合法的,易得 $f_i = f_{i - g_u} + 1$ 。

另外,我们可以用一种更为简洁的构建方式来构建 AC 自动机:(详见参考文献[1])

考虑 $x$ 是由 Trie 中的父亲 $f$ 通过 $c$ 转移边得到的。

检索 $y=\mathrm{fail}(f)$ ,若 $y$ 存在字符 $c$ 的转移边,则令 $x$ 指向 $\mathrm{tr}(y,c)$ 。否则, 令 $y \leftarrow \mathrm{fail}(y)$ 即不断跳 fail 检查是否有 $c$ 出边。若到了根节点依然没有 $c$ 出边,则令 $\mathrm{fail}(x)$ 指向初始节点。

对于每个串(在 Trie 中自顶向下),尝试寻找 fail 的 $y$ 在最终 fail 树中的深度变化,每次向下走一个儿子 $y$ 深度最多 $+1$ ,每次暴力跳 fail 深度都会 $-1$ ,均摊下来总的时间复杂度就是 $\mathcal{O}\left(\sum|s_i|\right)$.

那么有同学会说了,你这是在干什么呢?

众所周知,如果我们建字典图,时间复杂度就是 $\mathcal{O}\left(|\Sigma|\cdot \sum|s_i|\right)$ ,只不过这个 $|\Sigma|$ 通常是 $26$ 而已。

而这个构建方式,就是 OI wiki 上提到的:不连 trie 图,并且在构建 fail 指针的时候避免遍历到空儿子。

然后我久违的又去请教 Roundgod 了,他说一般没人会卡这个,并且主流写法都是建字典图。

时间复杂度 $\mathcal{O}(\sum |s_i|)$

代码:

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
typedef pair<int,int> pii;
void up(int &x,int y) { x < y ? x = y : 0; }
void down(int &x,int y) { x > y ? x = y : 0; }
#define N ((int)(1e6 + 15))

int n,f[N],g[N],dep[N]; string s[N];
int pos = 1,tot,fa[N],head[N],fail[N],tr[N][26];
struct Edge { int u,v,c,next; } e[N];
void addEdge(int u,int v,int c)
{
    tr[u][c] = v; fa[v] = u; dep[v] = dep[u] + 1;
    e[++pos] = {u,v,c,head[u]}; head[u] = pos;
}
void insert(string s)
{
    int u = 0, l = s.size();
    for(int i = 0; i < l; i++)
    {
        int c = s[i] - 'a';
        if(!tr[u][c]) addEdge(u, ++tot, c);
        u = tr[u][c];
    }
}
void build()
{
    queue<pii> q;
    for(int i = 0; i < 26; i++) if(tr[0][i]) q.push({tr[0][i], i});
    while(!q.empty())
    {
        int u,c,x; tie(u,c) = q.front(); q.pop();
        for(x = fail[fa[u]]; x && !tr[x][c]; x = fail[x]);
        if(x != fa[u]) fail[u] = tr[x][c];
        g[u] = fail[u] ? g[fail[u]] : dep[u];
        for(int i = head[u]; i; i = e[i].next) q.push({e[i].v, e[i].c});
    }
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    // freopen("check.in","r",stdin);
    // freopen("check.out","w",stdout);
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> s[i];
        reverse(s[i].begin(), s[i].end()); insert(s[i]);
    }
    build(); int res = 0;
    for(int o = 1; o <= n; o++)
    {
        int m = s[o].size(), u = 0; f[0] = 0;
        for(int i = 1; i <= m; i++)
        {
            u = tr[u][s[o][i - 1] - 'a'];
            f[i] = f[i - g[u]] + 1; res += f[i];
        }
    }
    cout << res << '\n';
    return 0;
}

参考文献

[1] 「题解」洛谷 P9089 「SvR-2」Work - do_while_true’s blog - 洛谷博客

[2] P9089「SvR-2」Work - FISHER_ 的博客 - 洛谷博客


文章作者: q779
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 q779 !
评论
  目录