CF547E Mike and Friends

题意:

codeforces链接

给你 \(n\) 个字符串 \(s_1,s_2...s_n\)\(q\) 组询问,输入 \(l,r,k\),问你串 \(s_k\) 在串 \(s_1\)\(s_n\) 中出现了几次(一个串中出现多次算多次)。\((|\sum s|\leq2e5,q\leq5e5)\)


1、后缀数组做法:

用后缀数组还是比较套路的一个东西,思考的过程会比较顺畅,基本走到哪里都能走通。

将所有串中间加上特殊字符隔开,后缀数组排个序,求出 \(height\) 数组。我们找到每个询问中 \(s_k\) 排好序后第一个字符的位置,向左右两边找 height ,找出满足height大于等于s_k长度的一段区间,这些区间中的每个字符串就都包含s_k这个字符串了。

然额我们要求的并不是s_k在所有的串中出现了几次,而是在l到r串中出现了几次,我们将排好序的每个字符记上它的颜色,就是来自哪个串,问题转化为求一个区间中颜色在l到r之间的元素有多少。

主席树维护颜色的前缀和,然后做一下差分。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define QWQ cout<<"QwQ"<<endl;
#define ll long long
#include <vector>
#include <queue>
#include <stack>
#include <map>
using namespace std;
const int N=421010;
const int qwq=N*22;
const int inf=0x3f3f3f3f;

int T,Q;
int n,m,obgg=33;
char s[N];
int a[N],len[N],wei[N];
int sa[N],rk[N],tp[N],c[N],h[N];
int f[N][22],ob[N];
int rt[N],tree[qwq],ls[qwq],rs[qwq],tot;

inline int read() {
	int lyy = 0, zbk = 1; char z7z = getchar();
	while(z7z<'0' || z7z>'9') { if(z7z=='-') zbk = -1; z7z = getchar(); }
	while(z7z>='0'&&z7z<='9') { lyy = lyy * 10 + z7z - '0'; z7z = getchar(); }
	return lyy * zbk;
}

void insert(int &u,int v,int l, int r,int we) {
	u = ++tot; tree[u] = tree[v] + 1;
	ls[u] = ls[v]; rs[u] = rs[v];
	if(l==r) return ;
	int mid = l+r >> 1;
	if(we<=mid) insert(ls[u], ls[v], l, mid, we);
	else        insert(rs[u], rs[v], mid+1, r, we);
}

int query(int u,int l,int r,int x,int y) {
	if(!u) return 0;
	if(x<=l && r<=y) return tree[u];
	int mid = l+r >> 1, res = 0;
	if(x<=mid) res += query(ls[u], l, mid, x, y);
	if(y>mid)  res += query(rs[u], mid+1, r, x, y);
	return res;
}

void Qsort() {
	for(int i=1;i<=n;i++) c[rk[i]]++;
	for(int i=1;i<=m;i++) c[i] += c[i-1];
	for(int i=n;i>=1;i--) sa[ c[rk[tp[i]]]-- ] = tp[i];
	for(int i=1;i<=m;i++) c[i] = 0;
}

void SA() {
	m = obgg+10;
	for(int i=1;i<=n;i++) rk[i] = a[i], tp[i] = i;
	Qsort();
	for(int k=1; ; k<<=1) {
		int p = 0;
		for(int i=1;i<=k;i++) tp[++p] = n - k + i;
		for(int i=1;i<=n;i++) if(sa[i]>k) tp[++p] = sa[i] - k;
		Qsort();
		swap(rk,tp);
		rk[sa[1]] = p = 1;
		for(int i=2;i<=n;i++) {
			if(tp[sa[i]]==tp[sa[i-1]] && tp[sa[i]+k]==tp[sa[i-1]+k])
				rk[ sa[i] ] = p;
			else
				rk[ sa[i] ] = ++p;
		}
		m = p;
		if(p==n) break;
	}
	for(int i=1,l=0,o; i<=n; h[rk[i++]]=l)
		for(l=(l?l-1:0), o=sa[ rk[i]-1 ]; a[o+l]==a[i+l]; ++l) ;
	for(int i=1;i<=n;i++) f[i][0] = h[i];
	for(int k=1;k<=20;k++)
		for(int i=1;i+(1<<k)-1<=n;i++)
			f[i][k] = min(f[i][k-1], f[ i+(1<<(k-1)) ][k-1]);
}

inline int MIN(int i,int j) { int k = ob[j-i+1]; return min(f[i][k],f[j-(1<<k)+1][k]); }

int qiu(int x,int H,int tr,int tl) {
	int L,R;
	int l = 1, r = x-1, mid, res = x;
	while(l<=r) {
		mid = l+r >> 1;
		if(MIN(mid+1,x) >= H) res = mid, r = mid-1;
		else l = mid+1;
	} L = res;
	l = x+1; r = n; res = x;
	while(l<=r) {
		mid = l+r >> 1;
		if(MIN(x+1,mid) >= H) res = mid, l = mid+1;
		else r = mid-1;
	} R = res;
	return query(rt[tr], 1, n, L, R) - query(rt[tl], 1, n, L, R);
}

int main() {
	int x,y,z;
	for(int i=2;i<=N-100;i++) ob[i] = ob[i>>1] + 1;
	T = read(); Q = read();
	for(int i=1;i<=T;i++) {
		scanf("%s",s+1); len[i] = strlen(s+1); wei[i] = n+1;
		for(int j=1;j<=len[i];j++) a[++n] = s[j] - 'a' + 1;
		a[++n] = ++obgg;
	} wei[T+1] = n+1;
	SA();
	for(int i=1;i<=n;i++) insert(rt[i], rt[i-1], 1, n, rk[i]);
	while(Q--) {
		x = read(); y = read(); z = read();
		cout<<qiu(rk[ wei[z] ], len[z], wei[y+1]-1, wei[x]-1)<<"\n";
	}
	return 0;
}

数组大小一定要记得开够!这种毒瘤字符串题一般不会卡空间的。要注意特殊字符也要占位置所以不能只开 \(2e5\)

2、后缀自动机做法:

拿所有的串建一颗广义后缀自动机,广义自动机的一个民间简单写法就是每次建完一个串转另一个串时重新从根开始建(因为我不会写标准的)。因为我们考虑像后缀数组一样加特殊字符隔开的话,每次特殊字符也会失配到根,所以效果一样。如果不加一些判断的话某些情况会出锅,但大部分比较简单的不会。

但巧了,这题会卡掉这种无脑的建图。过会儿会说明。

我们记录每个串建完后最后所在的点,这个点所在 \(fail\) 树上的位置,其子树中所有点表示的串都以这个串为后缀,就是包含这个串的所有串都在它子树中。像后缀数组一样的道理,我们将每个不同的串看成不同的颜色,我们只需统计一个点的子树中有多少不同的颜色,使用线段树合并可以在 \(nlogn\) 时间内求出 \(fail\) 树上每个点子树中不同颜色的个数。

线段树合并可以 \(DFS\) 一遍维护所有树的信息,但是得在 \(merge\) 的时候加新节点避免混用,比较卡空间。我们可以将每个点的询问离线下来,每次合并完一个点就统计了这个点的所有问题,这样不用考虑线段树合并时节点混用的情况。

在写代码时注意到我广义 \(sam\) 写法上的问题:这样建边可能会导致 \(fail\) 树上存在父亲与儿子表示同一个串的情况,它们的 \(len\) 相等。但是如果我们将串的位置记成了儿子,那么它父亲的其他儿子的信息就不能被统计。所以当一个点和父亲的 \(len\) 相同时我们把它的位置调整到父亲。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define QWQ cout<<"QwQ"<<endl;
#define ll long long
#include <vector>
#include <queue>
#include <stack>
#include <map>
using namespace std;
const int N=501010;
const int qwq=N*22;
const int inf=0x3f3f3f3f;

int n,m;
int cnt=1, last=1, tot;
int wei[N];
char s[N];
vector <int> e[N];
int ch[N][26],fa[N],len[N];
int rt[N],tree[qwq],L[qwq],R[qwq];
struct Q{ int id,x,y; };
vector <Q> d[N];
int ans[N];

inline int read() {
	int lyy = 0, zbk = 1; char z7z = getchar();
	while(z7z<'0' || z7z>'9') { if(z7z=='-') zbk = -1; z7z = getchar(); }
	while(z7z>='0'&&z7z<='9') { lyy = lyy * 10 + z7z - '0'; z7z = getchar(); }
	return lyy * zbk;
}

void insert(int &u,int l,int r,int we) {
	u = ++tot; tree[u] = 1;
	if(l==r) return ;
	int mid = l+r >> 1;
	if(we<=mid) insert(L[u], l, mid, we);
	else        insert(R[u], mid+1, r, we);
}

int merge(int u,int v,int l,int r) {
	if(!u || !v) return u|v;
	if(l==r) { tree[u] = tree[u] + tree[v]; return u; }
	int mid = l+r >> 1;
	L[u] = merge(L[u], L[v], l, mid);
	R[u] = merge(R[u], R[v], mid+1, r);
	tree[u] = tree[L[u]] + tree[R[u]];
	return u;
}

int query(int u,int l,int r,int x,int y) {
	if(!u) return 0;
	if(x<=l && r<=y) return tree[u];
	int mid = l+r >> 1, res = 0;
	if(x<=mid) res += query(L[u], l, mid, x, y);
	if(y>mid)  res += query(R[u], mid+1, r, x, y);
	return res;
}

inline void add(int c,int id) {
	int u = last; last = ++cnt; insert(rt[cnt], 1, n, id);
	len[cnt] = len[u] + 1;
	for(; u&&!ch[u][c]; u=fa[u]) ch[u][c] = cnt;
	if(!u) fa[cnt] = 1;
	else {
		int zjt = ch[u][c];
		if(len[zjt]==len[u]+1) fa[cnt] = zjt;
		else {
			int ob = cnt+1;
			memcpy(ch[ob],ch[zjt],sizeof(ch[zjt]));
			fa[ob] = fa[zjt]; len[ob] = len[u] + 1;
			fa[zjt] = fa[cnt] = ob;
			for(; u&&ch[u][c]==zjt; u=fa[u]) ch[u][c] = ob;
			cnt++;
		}
	}
}

void DFS(int u) {
	for(int i=0;i<e[u].size();i++) {
		int v = e[u][i];
		DFS(v);
		rt[u] = merge(rt[u], rt[v], 1, n);
	}
	for(int i=0;i<d[u].size();i++) {
		int id = d[u][i].id, x = d[u][i].x, y = d[u][i].y;
		ans[id] = query(rt[u], 1, n, x, y);
	}
}

int main() {
	int x,y,z;
	n = read(); m = read();
	for(int i=1;i<=n;i++) {
		scanf("%s",s+1); int le = strlen(s+1);
		last = 1;
		for(int j=1;j<=le;j++) add(s[j]-'a',i);
		wei[i] = last; if(len[last]==len[fa[last]]) wei[i] = fa[last];
	}
	for(int i=2;i<=cnt;i++) e[fa[i]].push_back(i);
	for(int i=1;i<=m;i++) {
		x = read(); y = read(); z = read();
		d[ wei[z] ].push_back( (Q){i,x,y} );
	}
	DFS(1);
	for(int i=1;i<=m;i++) cout<<ans[i]<<"\n";
	return 0;
}

对于广义sam还不是很了解,所以很多题得左撞右撞才补锅,等以后了解了之后再来总结吧。


后缀数组可以把问题转化到序列上,后缀自动机可以转化到树上,擅长哪个就用哪个吧。

3、AC自动机做法:

太懒了不想写了。。。可做,去看看别的地方的题解吧。

posted @ 2020-05-07 11:14  maple276  阅读(123)  评论(0编辑  收藏  举报