「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}\) 指初始状态的结点)

状态转移方程显而易见:

\[f(x) = 1 + \sum\limits_{\text{Edge}(x\rightarrow y) \in \text{DAG}} f(y) \]

其中,若 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」不同子串个数的主要内容,如果未能解决你的问题,请参考以下文章

后缀自动机模板——不同子串个数p2408

P2408 不同子串个数

P4070 [SDOI2016]生成魔咒

[TyvjP1515] 子串统计 [luoguP2408] 不同子串个数(后缀数组)

字符串-后缀树和后缀数组详解

字符串-后缀树和后缀数组详解