「Luogu P2408」不同子串个数
Posted -wallace-
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了「Luogu P2408」不同子串个数相关的知识,希望对你有一定的参考价值。
Description
给你一个长为 \(n\) 的字符串,求不同的子串的个数。
我们定义两个子串不同,当且仅当有这两个子串长度不一样,或者长度一样且有任意一位不一样。
子串的定义:原字符串中连续的一段字符组成的字符串。
Hint
- 对于 \(30\%\) 的数据,\(1\le n\le 10^3\)
- 对于 \(100\%\) 的数据,\(1\le n\le 10^5\)
Solution 1
本质不同的子串计数问题,后缀自动机(SAM)。
我们先复习一下 SAM 的一些性质:
- 一个关于字符串 \(s\) 的 SAM 包含了 \(s\) 所有子串 的信息。
- SAM 中的一条路径对应 \(s\) 的一个子串,反过来,\(s\) 的一个子串也对应 SAM 的一条路径。简而言之,SAM 上的路径与字符串的子串都是一一对应的。
- SAM 的一个状态对应一些字符串的集合,这个集合的元素都各自对应初始状态到该状态的一条路径。
根据这些性质,我们不难得出一个结论:一个字符串的所有本质不同的子串的个数,等于其 SAM 上初始状态到所有非初始状态的路径数。
众所周知 SAM 是一个 DAG(有向无环图),那么答案也是 这个 DAG 上以初始状态对应的结点为起点的路径数。
都扯到 DAG 了,不考虑 dp 一下?
设 \(f(x)\) 为以 \(x\) 为起点的路径条数,那么答案就是 \(f(\text{start})\)。(\(\text{start}\) 指初始状态的结点)
状态转移方程显而易见:
其中,若 SAM 中存在一个转移 \(\delta(x, c) = y\),那么 DAG 上就对应一条边 \(x\rightarrow y\)。
注意空串不算,所以答案在输出前要减去一
时间复杂度 \(O(n\log |\Sigma|)\), 空间复杂度 \(O(n)\)。(SAM 用 map
实现,dp 的过程用记忆化搜索实现)
Code for Solution 1
#include <iostream>
#include <map>
#include <string>
using namespace std;
const int N = 1e5 + 5;
namespace SAM {
const int T = N << 1;
struct Node {
map<char, int> ch;
int link, len;
} t[T];
int total, last;
inline void extend(char c) {
int p = last, np = last = ++total;
t[np].len = t[p].len + 1;
for (; p && !t[p].ch.count(c); p = t[p].link)
t[p].ch[c] = np;
if (!p) {
t[np].link = 1;
} else {
int q = t[p].ch[c];
if (t[q].len == t[p].len + 1) {
t[np].link = q;
} else {
int nq = ++total;
t[nq].ch = t[q].ch, t[nq].link = t[q].link;
t[nq].len = t[p].len + 1;
t[np].link = t[q].link = nq;
while (p && t[p].ch.count(c) && t[p].ch[c] == q)
t[p].ch[c] = nq, p = t[p].link;
}
}
}
void init(string& s) {
total = last = 1;
for (string::iterator p = s.begin(); p != s.end(); p++)
extend(*p);
}
long long f[T];
long long solve(int x);
};
long long SAM::solve(int x = 1) {
if (f[x]) return f[x];
f[x] = 1ll;
for (map<char, int>::iterator it = t[x].ch.begin(); it != t[x].ch.end(); it++)
f[x] += solve(it->second);
return f[x];
}
int n;
string s;
signed main() {
ios::sync_with_stdio(false);
cin >> n >> s;
SAM::init(s);
cout << SAM::solve() - 1 << endl;
return 0;
}
Solution 2
仍然是 SAM 。
上述算法是 离线 的,无法动态维护。而有一道题 Luogu P4070 [SDOI2016]生成魔咒 就要求使用在线算法。
当 SAM extend
这个字符之后,新产生了一个最长的子串,而 这个子串的 \(\text{end-pos}\) 只有自己一个。设这个新加入的结点为 \(p\)。
这个子串的一些后缀也是新产生的,但不是所有的后缀。
若我们知道这个子串前面少 \(k\) 个字符形成的后缀的 \(\text{end-pos}\) 也只有一个,那么答案就会增加 \(k + 1\)。
少一个,少两个,三个……一直到 \(\text{len}(p) - \text{len}(\text{link}(p))\) 个,发现此时不行了,因为 \(\text{end-pos}\) 已经不止自己一个了,少更多的亦是如此。
于是,添加一个字符,答案会增加 \(\text{len}(p) - \text{len}(\text{link}(p))\) 这么多。于是就做到了动态维护答案。
注意,因为拆点而新建的结点对答案没有影响,不能算进去。
时空复杂度同 Solution 1。
Code for Solution 2
#include <iostream>
#include <map>
#include <string>
using namespace std;
const int N = 1e5 + 5;
namespace SAM {
const int T = N << 1;
struct Node {
map<char, int> ch;
int link, len;
} t[T];
int total, last;
long long ans = 0ll;
inline void extend(char c) {
int p = last, np = last = ++total;
t[np].len = t[p].len + 1;
for (; p && !t[p].ch.count(c); p = t[p].link)
t[p].ch[c] = np;
if (!p) {
t[np].link = 1;
} else {
int q = t[p].ch[c];
if (t[q].len == t[p].len + 1) {
t[np].link = q;
} else {
int nq = ++total;
t[nq].ch = t[q].ch, t[nq].link = t[q].link;
t[nq].len = t[p].len + 1;
t[np].link = t[q].link = nq;
while (p && t[p].ch.count(c) && t[p].ch[c] == q)
t[p].ch[c] = nq, p = t[p].link;
}
}
ans += t[np].len - t[t[np].link].len;
}
void init(string& s) {
total = last = 1;
for (string::iterator p = s.begin(); p != s.end(); p++)
extend(*p);
}
};
int n;
string s;
signed main() {
ios::sync_with_stdio(false);
cin >> n >> s;
SAM::init(s);
cout << SAM::ans << endl;
return 0;
}
以上是关于「Luogu P2408」不同子串个数的主要内容,如果未能解决你的问题,请参考以下文章