bzoj4556【TJOI2016&HEOI2016】字符串

4556: [Tjoi2016&Heoi2016]字符串

Time Limit: 20 Sec   Memory Limit: 128 MB
Submit: 195   Solved: 103
[ Submit][ Status][ Discuss]

Description

佳媛姐姐过生日的时候,她的小伙伴从某东上买了一个生日礼物。生日礼物放在一个神奇的箱子中。箱子外边写了
一个长为n的字符串s,和m个问题。佳媛姐姐必须正确回答这m个问题,才能打开箱子拿到礼物,升职加薪,出任CE
O,嫁给高富帅,走上人生巅峰。每个问题均有a,b,c,d四个参数,问你子串s[a..b]的所有子串和s[c..d]的最长公
共前缀的长度的最大值是多少?佳媛姐姐并不擅长做这样的问题,所以她向你求助,你该如何帮助她呢?

Input

输入的第一行有两个正整数n,m,分别表示字符串的长度和询问的个数。接下来一行是一个长为n的字符串。接下来
m行,每行有4个数a,b,c,d,表示询问s[a..b]的所有子串和s[c..d]的最长公共前缀的最大值。1<=n,m<=100,000,
字符串中仅有小写英文字母,a<=b,c<=d,1<=a,b,c,d<=n

Output

 对于每一次询问,输出答案。

Sample Input

5 5
aaaaa
1 1 1 5
1 5 1 1
2 3 2 3
2 4 2 3
2 3 2 4

Sample Output

1
1
2
2
2



方法一:后缀自动机+二分答案+倍增+可持久化线段树

后缀自动机的性质是,所有接收状态都表示一个后缀。但题目要求最长公共前缀,所以要将字符串翻转。(字符串翻转的小技巧要掌握...)

于是问题转化为:给定两个子串a和b,求的a所有子串和b的最长公共后缀的最大值。

然后二分答案,问题转化为判断mid是否合法。

判断方法:每个点用一个权值线段树记录right集合。在parent树上倍增,找到mx值第一个包含mid的地方,然后用该点的线段树判断。

这里又遇到另一个问题,线段树的合并。可以证明线段树的合并复杂度为O(n*logn)。

具体证明:http://wenku.baidu.com/link?url=5HN1zM0kty7nAycmsZAYdRNSNLEb_Keepc-e-BNS3fLWc-0e4ON_bdvMtUD_w01ZIkWfbuKGh2LiXBXkbZ04vPJJTXdd7Dlm2OSO-_8gMgG

最终复杂度为O(n*log^2n)。


#include
#include
#include
#include
#include
#include
#define F(i,j,n) for(int i=j;i<=n;i++)
#define D(i,j,n) for(int i=j;i>=n;i--)
#define ll long long
#define N 200005
#define M 4000005
using namespace std;
int n,m;
int pos[N];
int last=1,cnt=1,a[N][26],fa[N][20],mx[N],c[N],q[N];
int tot,rt[N],ls[M],rs[M];
char s[N];
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
void insert(int &k,int l,int r,int x)
{
	k=++tot;
	if (l==r) return;
	int mid=(l+r)>>1;
	if (x<=mid) insert(ls[k],l,mid,x);
	else insert(rs[k],mid+1,r,x);
}
int merge(int x,int y)//线段树的合并 
{
	if (!x||!y) return x+y;
	int z=++tot;
	ls[z]=merge(ls[x],ls[y]);rs[z]=merge(rs[x],rs[y]);
	return z;
}
bool find(int k,int l,int r,int L,int R)
{
	if (!k) return 0;
	if (l==L&&r==R) return 1;
	int mid=(l+r)>>1;
	if (R<=mid) return find(ls[k],l,mid,L,R);
	else if (L>mid) return find(rs[k],mid+1,r,L,R);
	else return find(ls[k],l,mid,L,mid)||find(rs[k],mid+1,r,mid+1,R);
}
void add(int x,int id)
{
	int p=last,np=++cnt;last=np;
	mx[np]=mx[p]+1;pos[id]=np;insert(rt[np],1,n,id);
	while (p&&!a[p][x]) a[p][x]=np,p=fa[p][0];
	if (!p) fa[np][0]=1;
	else
	{
		int q=a[p][x];
		if (mx[q]==mx[p]+1) fa[np][0]=q;
		else
		{
			int nq=++cnt;mx[nq]=mx[p]+1;
			memcpy(a[nq],a[q],sizeof(a[q]));
			fa[nq][0]=fa[q][0];fa[np][0]=fa[q][0]=nq;
			while (a[p][x]==q) a[p][x]=nq,p=fa[p][0];
		}
	}
}
void calc()
{
	F(i,1,cnt) c[mx[i]]++;
	F(i,1,cnt) c[i]+=c[i-1];
	F(i,1,cnt) q[c[mx[i]]--]=i;
	D(i,cnt,1)
	{
		int x=q[i],f=fa[x][0];
		rt[f]=merge(rt[f],rt[x]);
	}
	F(j,1,18) F(i,1,cnt) fa[i][j]=fa[fa[i][j-1]][j-1];
}
bool check(int mid,int x,int l,int r)
{
	D(i,18,0) if (mx[fa[x][i]]>=mid) x=fa[x][i];
	return find(rt[x],1,n,l,r);
}
int main()
{
	n=read();m=read();
	scanf("%s",s+1);reverse(s+1,s+n+1);
	F(i,1,n) add(s[i]-'a',i);
	calc();
	F(i,1,m)
	{
		int a=n-read()+1,b=n-read()+1,c=n-read()+1,d=n-read()+1;
		swap(a,b);swap(c,d);
		int l=1,r=min(d-c+1,b-a+1),mid,ans=0;
		while (l<=r)
		{
			mid=(l+r)>>1;
			if (check(mid,pos[d],a+mid-1,b)) ans=mid,l=mid+1;
			else r=mid-1;
		}
		printf("%d\n",ans);
	}
	return 0;
}



方法二:后缀数组

这个方法感觉是暴力,但是跑得飞快...

每次在后缀数组上暴力向前和向后找,当height[i]≤ans时停止搜索,相当于一个小的剪枝。


#include
#include
#include
#include
#include
#include
#define F(i,j,n) for(int i=j;i<=n;i++)
#define D(i,j,n) for(int i=j;i>=n;i--)
#define ll long long
#define N 200005
#define inf 1000000000
using namespace std;
int n,m;
int x[N],y[N],c[N],sa[N],rnk[N],h[N];
char s[N];
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
void build_sa()
{
	int num=26,t;
	F(i,1,n) c[x[i]]++;
	F(i,2,num) c[i]+=c[i-1];
	D(i,n,1) sa[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1)
	{
		t=0;
		F(i,n-k+1,n) y[++t]=i;
		F(i,1,n) if (sa[i]>k) y[++t]=sa[i]-k;
		memset(c,0,sizeof(c));
		F(i,1,n) c[x[i]]++;
		F(i,2,num) c[i]+=c[i-1];
		D(i,n,1) sa[c[x[y[i]]]--]=y[i];
		swap(x,y);
		t=1;x[sa[1]]=1;
		F(i,2,n) x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?t:++t;
		if (t>=n) break;
		num=t;
	}
}
void get_h()
{
	int tmp=0,x;
	F(i,1,n) rnk[sa[i]]=i;
	F(i,1,n)
	{
		x=sa[rnk[i]-1];
		if (tmp) tmp--;
		while (s[x+tmp]==s[i+tmp]) tmp++;
		h[rnk[i]]=tmp;
	}
}
int main()
{
	n=read();m=read();
	scanf("%s",s+1);
	F(i,1,n) x[i]=s[i]-'a'+1;
	build_sa();
	get_h();
	F(i,1,m)
	{
		int a=read(),b=read(),c=read(),d=read();
		int pos=rnk[c],ans=0,mn=inf;
		if (a<=c&&c<=b) ans=min(d-c+1,b-c+1);
		D(i,pos,2)
		{
			if (h[i]<=ans) break;
			mn=min(mn,h[i]);
			if (sa[i-1]>=a&&sa[i-1]<=b) ans=max(ans,min(mn,min(d-c+1,b-sa[i-1]+1)));
		}
		mn=inf;
		F(i,pos+1,n)
		{
			if (h[i]<=ans) break;
			mn=min(mn,h[i]);
			if (sa[i]>=a&&sa[i]<=b) ans=max(ans,min(mn,min(d-c+1,b-sa[i]+1)));
		}
		printf("%d\n",ans);
	}
	return 0;
}


你可能感兴趣的:(二分,好题,倍增,线段树,后缀数组,后缀自动机,可持久化数据结构,OIer的狂欢)