「SCOI2012」喵星球上的点名

「SCOI2012」喵星球上的点名

题目链接
填一个很久以前用 \(\texttt{AC}\) 自动机没填上的坑。

关于本题,能够通过本题的算法很多,这里作者采用的是后缀数组+树状数组的做法。

首先有一个显然的结论:若 \(s_2\)\(s_1\) 的子串,则 \(s_1\) 一定存在一个后缀与 \(s_2\) 的最长公共前缀为 \(|s_2|\)

我们将读入的姓、名、询问串连成一个整体,形成一个字符串 \(s\),且在每一个姓、名、询问串中插入一个不存在文本中的字符,且保证询问串后插入的比姓名串后插入的字符字典序小

然后我们求出字符串 \(s\)\(\texttt{height}\) 数组,将两后缀的 \(\texttt{LCP}\) 转化为 \(\texttt{RMQ}\) 问题。

根据上面我们的构造方式可以得到:如果一个询问可以被响应,那么\(\texttt{sa}\) 数组中一定存在下标连续的一段,它们所代表的后缀的 \(\texttt{LCP}\) 等于询问串长度,且这一段的开头一定是以这个询问串开头的后缀。

原因也很简单,因为我们在询问串后插入的分割字符是比插入在姓名串后的字符小的,所以按照后缀大小排序后自然排在相对靠前的位置。

这启发我们可以通过二分查找右端点(左端点已经确定,就是询问串开头的后缀),将每个询问转化成一个区间,然后问题转化为:

  • 每个区间内不同归属的姓名串个数。
  • 每个姓名串被多少个区间覆盖。

问题一是一个经典问题,即区间内有多少种不同的颜色。可以用离线后用树状数组完成,这里给出一种具体做法(不唯一):将每个询问固定至其右端点上,从小到大枚举右端点,在树状数组中将每种颜色最靠近右端点(不超过右端点)的位置加 \(1\) 统计,这样可以保证最优。所以我们需要维护一个 \(\texttt{pre}\) 数组,表示当前位置颜色上一次出现的位置。

问题二在我们维护了 \(\texttt{pre}\) 数组过后也变得较为简单。首先将每个区间在树状数组上加 \(1\)。仍然将每个区间固定至其右端点,枚举右端点,计算每个点的贡献,即 \(sum(i)-sum(pre[i])\),然后将以当前位置为右端点的区间在树状数组上减 \(1\)

至此,本题可以在 \(O(|s|log_2|s|)\) 的复杂度内解决。

贴代码(人傻自带大常数)

#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+5;
int s[maxn];
int sa[maxn],rk[maxn],tp[maxn],cnt[maxn];
int nn,n,m,q;
int height[maxn],h[maxn];
int col[maxn],len[maxn],vis[maxn];
int mn[maxn][21],lg[maxn];
int tree[maxn];
int L[maxn],R[maxn],tot;
int lst[maxn],pre[maxn];
int ans1[maxn],ans2[maxn];
vector<pair<int,int> > v[maxn];
vector<int>v2[maxn];
inline void add(int x,int k){
	for(;x<=n&&x;x+=x&-x) tree[x]+=k;
}
inline int sum(int x){
	int ans=0;
	for(;x;x-=x&-x) ans+=tree[x];
	return ans;
}
inline void sort(){
	
	memset(cnt,0,sizeof (int)*(m+1)); 
	for(int i=1;i<=n;++i) ++cnt[rk[i]];
	for(int i=1;i<=m;++i) cnt[i]+=cnt[i-1];
	for(int i=n;i>=1;--i) sa[cnt[rk[tp[i]]]--]=tp[i];
}
inline void init(){
	lg[0]=-1;
	for(int i=1;i<=n;++i) mn[i][0]=height[i],lg[i]=lg[i>>1]+1;
	for(int j=1;j<=log2(n);++j)
		for(int i=1;i+(1<<j)-1<=n;++i)
			mn[i][j]=min(mn[i][j-1],mn[i+(1<<j-1)][j-1]);
}
inline int rmq(int l,int r){
	int s=lg[r-l+1];
	return min(mn[l][s],mn[r-(1<<s)+1][s]);
}
inline void SA(){
	m=220000;
	for(int i=1;i<=n;++i) rk[i]=s[i]+1,tp[i]=i;
	sort();
	for(int w=1,p=0;p<n;m=p,w<<=1){
		p=0;
		for(int i=n;i>n-w;--i) tp[++p]=i;
		for(int i=1;i<=n;++i) if(sa[i]>w) tp[++p]=sa[i]-w;
		sort();swap(tp,rk);
		rk[sa[1]]=p=1;
		for(int i=2;i<=n;++i)
			rk[sa[i]]=(tp[sa[i-1]]==tp[sa[i]]&&tp[sa[i-1]+w]==tp[sa[i]+w])?p:++p;
	}
	for(int i=1;i<=n;++i) rk[sa[i]]=i;
	int k=0;
	for(int i=1;i<=n;++i){
		if(rk[i]==1) continue;
		if(k) --k;
		int j=sa[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) ++k;
		height[rk[i]]=k;
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>nn>>q;
	for(int i=1;i<=nn;++i){
		int opt;cin>>opt;
		for(int j=1;j<=opt;++j) col[++n]=i,cin>>s[n],s[n]+=nn+q;
		s[++n]=-i+nn+q;
		cin>>opt;
		for(int j=1;j<=opt;++j) col[++n]=i,cin>>s[n],s[n]+=nn+q;
		s[++n]=-i+nn+q;
	}
	for(int i=1;i<=q;++i){
		cin>>len[i];vis[n+1]=i;
		for(int j=len[i];j;--j){
			cin>>s[++n],s[n]+=nn+q;
		}
		s[++n]=-i+q;
	}
	SA();
	init();
	for(int i=1;i<=n;++i){
		if(vis[sa[i]]){
			int l=i+1,r=n;
			int ans=i;
			while(l<=r){
				int mid=(l+r)>>1;
				if(rmq(i+1,mid)>=len[vis[sa[i]]]){
					ans=max(ans,mid),l=mid+1;
				}
				else r=mid-1;
			}
			v[ans].emplace_back(i,vis[sa[i]]);//二分求右焦点
			L[++tot]=i,R[tot]=ans+1;//这里保存的区间是[l,r+1],所以后面就不用+1了
		}
		if(col[sa[i]]){
			pre[i]=lst[col[sa[i]]];
			lst[col[sa[i]]]=i;
		}
	}
	for(int i=1;i<=n;++i){
		if(col[sa[i]]){
			add(pre[i],-1),add(i,1);
		}
		for(auto x:v[i])
			ans1[x.second]=sum(i)-sum(x.first-1);
	}//问题一
	memset(tree,0,sizeof tree);
	for(int i=1;i<=tot;++i) add(L[i],1),add(R[i],-1),v2[R[i]].emplace_back(L[i]);
	for(int i=1;i<=n;++i){
		for(auto x:v2[i])
			add(x,-1),add(i,1);
		ans2[col[sa[i]]]+=sum(i)-sum(pre[i]);
	}//问题二
	for(int i=1;i<=q;++i) cout<<ans1[i]<<'\n';
	for(int i=1;i<=nn;++i) cout<<ans2[i]<<' ';
	return 0;

posted @ 2020-02-06 15:50  Henry__Huang  阅读(219)  评论(0编辑  收藏  举报