zoukankan      html  css  js  c++  java
  • 「Luogu P2408」不同子串个数

    Description

    给你一个长为 (n) 的字符串,求不同的子串的个数。

    我们定义两个子串不同,当且仅当有这两个子串长度不一样,或者长度一样且有任意一位不一样。

    子串的定义:原字符串中连续的一段字符组成的字符串。

    Hint

    • 对于 (30\%) 的数据,(1le nle 10^3)
    • 对于 (100\%) 的数据,(1le nle 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( ext{start}))。(( ext{start}) 指初始状态的结点)

    状态转移方程显而易见:

    [f(x) = 1 + sumlimits_{ ext{Edge}(x ightarrow y) in ext{DAG}} f(y) ]

    其中,若 SAM 中存在一个转移 (delta(x, c) = y),那么 DAG 上就对应一条边 (x ightarrow y)

    注意空串不算,所以答案在输出前要减去一

    时间复杂度 (O(nlog |Sigma|)), 空间复杂度 (O(n))。(SAM 用 map 实现,dp 的过程用记忆化搜索实现)

    Code for Solution 1

    /*
     * Author : _Wallace_
     * Source : https://www.cnblogs.com/-Wallace-/
     * Problem : Luogu P2408 不同子串个数
     */
    #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 这个字符之后,答案会增大。

    ( ext{link}) 的定义可知,一个结点 (x) 对应的子串长度范围为 ([ ext{len}( ext{link}(x)) + 1, ext{len}(i)])

    于是,添加一个字符,答案会增加 ( ext{len}(p) - ext{len}( ext{link}(p))) 这么多。

    于是就做到了动态维护答案。

    注意,因为拆点而新建的结点对答案没有影响,不能算进去。

    时空复杂度同 Solution 1。

    Code for Solution 2

    /*
     * Author : _Wallace_
     * Source : https://www.cnblogs.com/-Wallace-/
     * Problem : Luogu P2408 不同子串个数
     */
    #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;
    }
    
  • 相关阅读:
    ceph(4)--Ceph 的基础数据结构
    ceph(3)--Ceph 物理和逻辑结构
    ceph(2)--Ceph RBD 接口和工具
    ceph(1)--安装和部署
    Ceph中文文档
    Linux系统介绍(五)常用命令
    Linux系统介绍(四)IO重定向与管道
    剑指offer:跳台阶
    剑指offer:斐波那契数列
    剑指offer:旋转数组的最小数字
  • 原文地址:https://www.cnblogs.com/-Wallace-/p/12934162.html
Copyright © 2011-2022 走看看