Noi2012四川省选day1 喵星球上的点名_(后缀数组) 代码及解析(详细)

喵星球上的点名 
 
【题目描述】 
a180285幸运地被选做了地球到喵星球的留学生。他发现喵星人在上课前的点名现象非
常有趣。 
  假设课堂上有N个喵星人,每个喵星人的名字由姓和名构成。喵星球上的老师会选择
M个串来点名,每次读出一个串的时候,如果这个串是一个喵星人的姓或名的子串,那么
这个喵星人就必须答到。 
然而,由于喵星人的字码过于古怪,以至于不能用ASCII码来表示。为了方便描述,
a180285决定用数串来表示喵星人的名字。 
现在你能帮助a180285统计每次点名的时候有多少喵星人答到,以及M次点名结束后
每个喵星人答到多少次吗?   
【输入】 
  现在定义喵星球上的字符串给定方法: 
  先给出一个正整数L,表示字符串的长度,接下来L个整数表示字符串的每个字符。 
  输入的第一行是两个整数N和M。 
  接下来有N行,每行包含第i 个喵星人的姓和名两个串。姓和名都是标准的喵星球上的
字符串。 
  接下来有M行,每行包含一个喵星球上的字符串,表示老师点名的串。 
【输出】 
对于每个老师点名的串输出有多少个喵星人应该答到。 
然后在最后一行输出每个喵星人被点到多少次。 
【样例输入】 
2 3 
6 8 25 0 24 14 8 6 18 0 10 20 24 0 
7 14 17 8 7 0 17 0 5 8 25 0 24 0 
4 8 25 0 24 
4 7 0 17 0 
4 17 0 8 25 
【样例输出】 



1 2 
【提示】 
事实上样例给出的数据如果翻译成地球上的语言可以这样来看 
2 3 
izayoi sakuya 
orihara izaya 
izay 
hara 
raiz 
【数据范围】 
 
  4 
对于30%的数据,保证: 
1 <= M,N<=2
  喵星人的名字总长不超过4000,点名串的总长不超过2000, 
对于100%的数据,保证: 
1<=N<=20000 
1<=M<=50000
喵星人的名字总长和点名串的总长分别不超过

100000 保证喵星人的字符串中作为字符存在的数不超过10000

【分析】

将姓名串与点名串合并为一个长串,处理出后缀数组sa,名次数组Rank及最长公共前缀Height

通过使点名串结尾大于姓名串结尾来保证具有相同前缀的点名串和姓名串中姓名串名次比点名串靠前

每次从点名串的名次向前扫,若Height==点名串长度,则找到一个答案!

详见代码(博主蒟蒻,写了一下午。。。)


#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int n,m,S[320000+10],ca[50000+10],indd=0,sa[320000+10],Rank[320000+10],Height[320000+10];
int ma=0,summ,numm[20000+10],len[50000+10];
bool hash[20000+10];
const int maxn=10001;
//sa后缀数组:sa[i]=k->字典序为第i位的后缀为k
//Rank名次数组:Rank[i]=k->后缀i的字典序为第k位(Rank与sa相互映射)
//Height最长公共前缀:Height[i]=k->sa[i]与sa[i-1]的最长公共前缀 

int t[320000+10],t2[320000+10],c[320000+10];
void build_Sa()//构造sa,Rank,Height
{
	int i,*x=t,*y=t2;//x->当前的串 
	for(i=0;i<ma;i++)c[i]=0;//基数排序——start 
	for(i=0;i<indd;i++)c[x[i]=S[i]]++;
	for(i=1;i<ma;i++)c[i]+=c[i-1];
	for(i=indd-1;i>=0;i--)sa[--c[x[i]]]=i;//基数排序——end(单个字符的字典序排序) 
	for(int k=1;k<=indd;k<<=1)//倍增算法的二元组间距 
	{
		int p=0;//利用sa对第二关键字排序 ——start 
		for(i=indd-k;i<indd;i++)y[p++]=i;
		for(i=0;i<indd;i++)if(sa[i]>=k)y[p++]=sa[i]-k;// ——end 
		for(i=0;i<ma;i++)c[i]=0;//基数排序——start 
		for(i=0;i<indd;i++)c[x[y[i]]]++;
		for(i=1;i<ma;i++)c[i]+=c[i-1];
		for(i=indd-1;i>=0;i--)sa[--c[x[y[i]]]]=y[i];//——end 
		swap(x,y);//构造新的x——start 
		p=1;x[sa[0]]=0;
		for(i=1;i<indd;i++)
		{
			x[sa[i]]=(y[sa[i-1]]==y[sa[i]] && y[sa[i-1]+k]==y[sa[i]+k])?p-1:p++;
		}//——end 
		if(p>=indd)break ;// sa不会再发生变化,结束 
		ma=p;//下次排序的最大值 
	}
	for(i=0;i<indd;i++)//构造Rank 
	{
		Rank[sa[i]]=i;
	}
	int k=0;
	for(i=0;i<indd;i++)//构造Height 
	{
		if(k)k--;//设h[i]=Height[Rank[i]],则有h[i]>=h[i-1]-1,详细证明见后缀数组讲义 
		int j=sa[Rank[i]-1];
		while(S[i+k]==S[j+k])k++;
		Height[Rank[i]]=k;
	}
}

int find(int a)//寻找当前位置属于哪位喵星人 
{
	for(int i=a;i<indd;i++)
	{
		if(S[i]==maxn+n+1)return 0;//若为点名返回0 
		if(S[i]>maxn)return S[i]-maxn;
	}
}

int read()//读入数据在百万级以上,用逐字节读入优化 
{
	int a=0;
	char c=getchar();
	while(c<'0' || c>'9')c=getchar();
	do
	{
		a*=10;
		a+=c-'0';
		c=getchar();
	}while(c>='0' && c<='9');
	return a;
}

int main()
{
	freopen("name.in","r",stdin);
	freopen("name.out","w",stdout);
	n=read();m=read();
	for(int i=1;i<=n;i++)//将姓名串与点名串连接在一起 
	{
		int a=read();
		for(int j=1;j<=a;j++)
		{
			S[indd++]=read();
		}
		S[indd++]=maxn+i;//姓名串用maxn(10001)+编号i 隔断 
		a=read();
		for(int j=1;j<=a;j++)
		{
			S[indd++]=read();
		}
		S[indd++]=maxn+i;//姓名串用maxn(10001)+编号i 隔断
	}
	for(int i=1;i<=m;i++)
	{
		int a=read();
		len[i]=a;//记录点名串长度 
		ca[i]=indd;//记录点名串位置 
		for(int j=1;j<=a;j++)
		{
			S[indd++]=read();
		}
		S[indd++]=maxn+n+1;//点名串用 maxn+n+1隔断 
	}
	ma=maxn+n+2;
	build_Sa();
	for(int i=1;i<=m;i++)
	{
		memset(hash,0,sizeof(hash));
		summ=0;
		hash[0]=true;
		for(int j=Rank[ca[i]];j>=0;j--)//因为点名串的结尾(maxn+n+1)比姓名串大,所以有相同前缀的点名串和姓名串,点名串的字典序一定较大 
		{
			if(Height[j]<len[i])break ;//若当前不匹配,则后面的也一定不会匹配(见Height定义) 
			int a=find(sa[j-1]);//寻找匹配属于哪位喵星人 
			if(!hash[a])// 若该喵星人未被此点名串点过,更新答案 
			{
				hash[a]=true;
				numm[a]++;
				summ++;
			}
		}
		printf("%d\n",summ);
	}
	for(int i=1;i<n;i++)printf("%d ",numm[i]);
	printf("%d\n",numm[n]);
	return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值