2025.5.28【33OJ NOI 模拟赛 T3】字符串(AC自动机, 字符串后缀结构)

题意

传送门

题意:
给定 n n n 个字符串 s 1 , s 2 … , s n s_1,s_2 \dots ,s_n s1,s2,sn,保证它们互不相同。你需要求出满足如下条件的三元组 ( i , j , k ) (i, j, k) (i,j,k) 的数量:

  • 1 ≤ i , j , j ≤ n 1 \leq i, j, j \leq n 1i,j,jn,且 i , j , k i, j, k i,j,k 互不相同。
  • s i s_i si s j s_j sj 的子串, s j s_j sj s k s_k sk 的子串。
  • 不存在一个 j ′ ∉ { i , j , k } j' \notin \{i, j, k\} j/{i,j,k} 使得 s i s_i si s j ′ s_{j'} sj 的子串且 s j ′ s_{j'} sj s k s_k sk 的子串。

1 ≤ n ≤ 5 × 10 5 1 \leq n \leq 5 \times 10^5 1n5×105 1 ≤ ∑ ∣ s i ∣ ≤ 10 6 1 \leq \sum|s_i| \leq 10^6 1si106。保证所有 s i s_i si 仅有小写字母组成。

分析

枚举最长串 s k s_k sk,那么最短串 s i s_i si 需要是 s k s_k sk 的一个子串,我们将 子串 等价成 一个前缀的后缀
那么枚举 s k s_k sk 的一个前缀 [ 1 , p ] [1, p] [1,p],称前缀 p p p 的一个 后缀字符串 为能匹配 [ 1 , p ] [1, p] [1,p] 的后缀的字符串 s u s_u su。那么有结论:

  • 合法的最短串 s i s_i si s k s_k sk 中的任意出现位置上都必须是 最长后缀字符串次长后缀字符串

正确性显然。因为如果不是最长或次长,那么就至少存在两个 j j j 满足 s i s_i si s j s_j sj 的子串, s j s_j sj s k s_k sk 的子串。

建出所有 s i s_i si A C AC AC 自动机,预处理每个前缀的最长后缀字符串和次长后缀字符串,那么对 ( i , k ) (i, k) (i,k) 的枚举量为 O ( ∑ ∣ s i ∣ ) O(\sum |s_i|) O(si) O ( n ) O(n) O(n) 检验是否存在 有且仅有一个 合法的 j j j,复杂度 O ( n ∑ ∣ s i ∣ ) O(n \sum |s_i|) O(nsi)

注意到如果一个字符串是某个前缀的后缀字符串,并且它不是最长或次长,那么它一定在 次长字符串对应 f a i l fail fail 树结点的祖先链上,可以通过 d f s dfs dfs 序上差分,求子树和来判断一个最长或次长后缀字符串是否满足上述 必要条件

对于所有满足第一个必要条件的字符串 s i s_i si,如果 有且仅有一个 s j s_j sj 满足 s i s_i si s j s_j sj 的子串, s j s_j sj s k s_k sk 的子串,那么 ( i , k ) (i, k) (i,k) 就可以对答案贡献 1 1 1。问题是怎么快速判断一个 s i s_i si 是否存在一个合法的 j j j

只保留满足第一个条件的 s i s_i si,那么有结论:

  • 对于一个 s i s_i si,只需要从 保留的所有字符串中 考虑是否 有且仅有一个 合法的 j j j

这是因为如果一个字符串 s t s_{t} st s k s_k sk 的子串并且没有被保留下来,那么保留下来的字符串中一定至少有两个包含它。如果一个 s i s_i si s t s_t st 的子串,那么它也一定是那两个包含 s t s_t st 的串的子串,这样就已经不合法了。

但是这样还是不太好直接判断,因为你不可能对每个 i i i 都枚举保留下来的 j j j

s i s_i si 所有出现的区间取出来,那么可以注意到若 s i s_i si 是某个 s j s_j sj 的子串,那么 s j s_j sj 的所有出现区间里面都一定会至少包含一个 s i s_i si 对应的区间。
因为 s i s_i si 既然被保留下来了说明任意它出现的位置它都是最长或次长串,那么 s i s_i si s j s_j sj 的所有出现位置上都会保留下一个区间。同时也很容易知道如果 s i s_i si 不是 s j s_j sj 的子串,那么所有 s j s_j sj 的区间里都不可能包含一个 s i s_i si 的区间。

那么我们只需要判断出 包含 s i s_i si 的所有区间中至少一个的区间是否只有一种编号即可。这是简单的 二维偏序 问题。

每次扫描线的区间数量和为 O ( ∑ ∣ s i ∣ ) O(\sum |s_i|) O(si),总复杂度 O ( ∑ ∣ s i ∣ × log ⁡ ( ∑ ∣ s i ∣ ) ) O(\sum |s_i| \times \log(\sum |s_i|)) O(si×log(si))

CODE:

#include<bits/stdc++.h>
#define pb emplace_back
#define MP make_pair
using namespace std;
typedef pair< int, int > PII;
const int N = 5e5 + 10;
const int M = 1e6 + 10;
int n, res;
char str[M];
vector< char > s[N];
struct BIT {
	int c[M];
	inline int lowbit(int x) {return x & -x;}
	inline void add(int x, int y) {for(; x < M; x += lowbit(x)) c[x] += y;}
	inline int ask(int x) {int res = 0; for(; x; x -= lowbit(x)) res += c[x]; return res;}
} T, F;
struct ACAM {
	int tot, trie[M][26], fail[M], len[M];
	int et[M], mx[M][2]; // 最长长度, 次长长度不为自身
	int L[M], R[M], dfc;
	vector< int > E[M]; // fail 树
	inline void ins(char *s) {
		int l = strlen(s + 1);
		int p = 0;
		for(int i = 1; i <= l; i ++ ) {
			if(!trie[p][s[i] - 'a']) trie[p][s[i] - 'a'] = ++ tot, len[trie[p][s[i] - 'a']] = len[p] + 1;
			p = trie[p][s[i] - 'a'];
		}
		et[p] = 1;
	}
	void dfs(int x) {
		L[x] = ++ dfc; 
		for(auto v : E[x]) dfs(v);
		R[x] = dfc;
	}
	inline void build() {
		queue< int > q;
		for(int i = 0; i < 26; i ++ ) 
			if(trie[0][i]) q.push(trie[0][i]);
		while(!q.empty()) {
			int u = q.front(); q.pop();
			for(int i = 0; i < 26; i ++ ) {
				if(trie[u][i]) {
					fail[trie[u][i]] = trie[fail[u]][i];
					int x = fail[trie[u][i]], y = trie[u][i];
					if(et[x]) mx[y][0] = x;
					if(len[mx[x][0]] > len[mx[y][0]]) swap(mx[y][0], mx[y][1]), mx[y][0] = mx[x][0];
					else mx[y][1] = (len[mx[y][1]] >= len[mx[x][0]] ? mx[y][1] : mx[x][0]);
					mx[y][1] = (len[mx[y][1]] >= len[mx[x][1]] ? mx[y][1] : mx[x][1]);
					q.push(trie[u][i]);
				}
				else trie[u][i] = trie[fail[u]][i];
			}
		}
		for(int i = 1; i <= tot; i ++ ) E[fail[i]].pb(i);
		dfs(0);
	}
} AC;
struct seg {
	int l, r, idx;
} P[M * 2];
inline bool cmp(seg x, seg y) {return (x.l < y.l) || (x.l == y.l && x.r > y.r);}
int cov[M], ed[M], nd[M * 2];
inline int calc(int x) { // 计算第 s[x] 作为最长串的答案
	int p = 0, ct = 0, cn = 0; 
	for(int i = 0; i < s[x].size(); i ++ ) {
		p = AC.trie[p][s[x][i] - 'a'];
		int u = (i != s[x].size() - 1 ? (AC.et[p] ? AC.mx[p][0] : AC.mx[p][1]) : AC.mx[p][1]);
		T.add(AC.L[u], 1);
	}
	p = 0; 
	for(int i = 0; i < s[x].size(); i ++ ) {
		p = AC.trie[p][s[x][i] - 'a'];
		int a = AC.mx[p][0], b = AC.mx[p][1];
		if(i != s[x].size() - 1) {
			if(AC.et[p]) swap(a, b), a = p;
		}
		if(a && T.ask(AC.R[a]) - T.ask(AC.L[a]) == 0) P[++ ct] = (seg) {i + 1 - AC.len[a] + 1, i + 1, a}, nd[++ cn] = a;
		if(b && T.ask(AC.R[b]) - T.ask(AC.L[b]) == 0) P[++ ct] = (seg) {i + 1 - AC.len[b] + 1, i + 1, b}, nd[++ cn] = b;
	}
	sort(nd + 1, nd + cn + 1); cn = unique(nd + 1, nd + cn + 1) - (nd + 1);
	priority_queue< PII > q;
	sort(P + 1, P + ct + 1, cmp);
	for(int i = 1; i <= ct; i ++ ) {
		if(cov[P[i].idx] != -1) {			
			int c = F.ask(M - 1) - F.ask(P[i].r - 1);
			if(c > 1) cov[P[i].idx] = -1;
			else if(c == 1) {
				if(cov[P[i].idx] != 0) {
					if(cov[P[i].idx] != q.top().second) cov[P[i].idx] = -1;
				}
				else cov[P[i].idx] = q.top().second;
			}
		}
		if(ed[P[i].idx] != 0) F.add(ed[P[i].idx], -1);
		ed[P[i].idx] = max(ed[P[i].idx], P[i].r); F.add(ed[P[i].idx], 1);
		q.push(MP(P[i].r, P[i].idx));
	}
	int res = 0;
	for(int i = 1; i <= cn; i ++ ) {res += (cov[nd[i]] > 0); cov[nd[i]] = 0; F.add(ed[nd[i]], -1); ed[nd[i]] = 0;}
	p = 0;
	for(int i = 0; i < s[x].size(); i ++ ) {
		p = AC.trie[p][s[x][i] - 'a'];
		int u = (i != s[x].size() - 1 ? (AC.et[p] ? AC.mx[p][0] : AC.mx[p][1]) : AC.mx[p][1]);
		T.add(AC.L[u], -1);
	}
	return res;
}
int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) {
		scanf("%s", str + 1);
		AC.ins(str); int m = strlen(str + 1);
		for(int j = 1; j <= m; j ++ ) s[i].pb(str[j]); 
	}
	AC.build();
	for(int i = 1; i <= n; i ++ ) res += calc(i);
	cout << res << endl;
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值