【题解】 [HNOI2004] L 语言

题目传送门

题意

题目描述
一段文章 \(T\) 、一个单词 \(W\) 由若干小写字母构成。一个字典 \(D\) 是若干个单词的集合。若文章 \(T\) 可以被分成若干部分,且每一个部分都是字典 \(D\) 中的单词,则称一段文章 \(T\) 在某个字典 \(D\) 下是可以被理解的。

例如字典 \(D\) 中包括单词 \(\texttt{is},\texttt{name},\texttt{what},\texttt{your}\),则文章 \(\texttt{whatisyourname}\) 是在字典 \(D\) 下可以被理解的,因为它可以分成 \(4\) 个单词:\(\texttt{what},\texttt{is},\texttt{your},\texttt{name}\),且每个单词都属于字典 \(D\),而文章 \(\texttt{whatisyouname}\) 在字典 \(D\) 下不能被理解,但可以在字典 \(D'=D\cup\{\texttt{you}\}\) 下被理解。这段文章的一个前缀 \(\texttt{whatis}\),也可以在字典 \(D\) 下被理解,而且是在字典 \(D\) 下能够被理解的最长的前缀。

给定一个字典 \(D\),你的程序需要判断若干段文章在字典 \(D\) 下是否能够被理解。并给出其在字典 \(D\) 下能够被理解的最长前缀的位置。

输入格式

第一行两个整数 \(n\)\(m\),表示字典 \(D\) 中有 \(n\) 个单词,且有 \(m\) 段文章需要被处理。

接下来 \(n\) 行,每行一个字符串 \(s\),表示字典 \(D\) 中的一个单词。

接下来 \(m\) 行,每行一个字符串 \(t\),表示一篇文章。

输出格式

对于输入的每一篇文章,你需要输出一行一个整数,表示这段文章在字典 \(D\) 可以被理解的最长前缀的位置。

数据规模与约定

  • 对于 \(80\%\) 的数据,保证 \(m \leq 20\)\(|t| \leq 10^6\)
  • 对于 \(100\%\) 的数据,保证 \(1 \leq n \leq 20\)\(1 \leq m \leq 50\)\(1 \leq |s| \leq 20\)\(1 \leq |t| \leq 2 \times 10^6\)\(s\)\(t\) 中均只含小写英文字母。

提示

  • 请注意数据读入对程序效率造成的影响。
  • 请注意【数据规模与约定】中标注的串长是单串长度,并不是字符串长度和。

思路

由于你谷的数据有加强,所以主要以你谷上的得分为对照。

文本串的前缀\(s[0,i]\)能被理解,当且仅当某个位置\(j \in [0,j)\)满足\(s[0,j]\)能够被理解,并且\(s[j+1,i]\)能够被理解。

一个朴素的想法:
und数组记录文本串\(s[0,und[tmp]]\)恰好能被理解时的位置。文本串从头到尾遍历,每次都枚举und里表示的能被理解的前缀,然后在字典里面验证\(s[und[j]+1,i]\)是否能被理解。最后更新und数组。
f数组记录截至当前的i,能够被理解的最长前缀。由于能被理解的最长前缀长度序列不下降,因此递推时只需要取当前和前一位置f数组的较大值。

朴素的算法必无法从这题手里骗到满分。事实上,\(O(大约n^2)\)的时间复杂度实在算不得优秀。

55pts on Luogu-Code1
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=2e6+5;
int n,m,lm;
string w;
int trie[500][27],cnt;
bool ex[500];
ll f[N],und[N],tmp;

inline ll lkup(ll l,ll r){
	if(l>r) return 0;
	int p=0;
	ll ret=0;
	for(int i=l;i<=r;i++){
		int c=w[i]-'a';
		if(!trie[p][c]) return ret;
		p=trie[p][c];
		if(ex[p]) ret=i-l+1;
	}
	if(ex[p]) ret=r-l+1;
	return ret;
}

inline ll find(string s){
	memset(f,0,sizeof(f));
	tmp=0; 
	int l=s.length();
	for(int i=0;i<l;i++){
		f[i]=lkup(0,i);
		for(int j=tmp-1;j>=0;j--){
			f[i]=max(f[i],und[j]+1+lkup(und[j]+1,(ll)i));//[j+1,i]
			if(f[i]==i+1) break;
		}
		f[i]=max(f[i],f[i-1]);
		if(f[i]==i+1) und[tmp++]=i;
	}
	return f[l-1];
}

inline void ins(string str){
	int l=str.length();
	lm=max(lm,l);
	int p=0;
	for(int i=0;i<l;i++){
		int c=str[i]-'a';
		if(!trie[p][c]) trie[p][c]=++cnt;
		p=trie[p][c];
	}
	ex[p]=1;
}

inline string read(){
	string s;
	char ch=getchar();
	while(ch<'a' || ch>'z') ch=getchar();
	while(ch>='a' && ch<='z'){
		s+=ch;
		ch=getchar();
	}
	return s;
}

int main(){
	scanf("%d%d",&n,&m);
	while(n--){
		w=read();
		ins(w);
	}
	while(m--){
		w=read();
		printf("%lld\n",find(w));
	}
	return 0;
}

Many days later……
据说正解是AC自动机,那就先打一个AC自动机板子吧(错误思想)。
怎么保证连着匹配前缀呢?打到最后灵光一闪,直接在trie上比较就可以了啊!哪里用得着什么AC自动机!

这次打出来和Code1有几点不同:

90 pts on Luogu-Code2
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int S=2e6+5;
const int N=400;
int n,m,ml;
int tr[N*100][30],cnt;//,fail[N*100](留着做纪念)
char s[N],t[S];
bool ex[N*100],f[S];

inline int query(char r[],int len,int st){
	int p=0,ret=0;
	len=min(st+ml,len);
	for(int i=st;i<len;i++){
		int c=r[i]-'a';
		if(tr[p][c]){
			if(ex[tr[p][c]]){
				f[i]=true;
				ret=i+1;
			}
			p=tr[p][c];
		} else break;
	}
	return ret;//maximum prefix length that can reach
}

inline int find(char r[]){
	int l=strlen(r);
	int ret=0;
	ret=query(r,l,0);
	for(int i=0;i<l;i++){
		if(f[i]){
			ret=max(ret,query(r,l,i+1));
		}
	} 
	return ret;
}

inline void ins(char r[]){
	int l=strlen(r);
	ml=max(ml,l);
	int p=0;
	for(int i=0;i<l;i++){
		int c=r[i]-'a';
		if(!tr[p][c]) tr[p][c]=++cnt;
		p=tr[p][c];
	}
	ex[p]=1;
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf(" %s",s);
		ins(s);
	}
	while(m--){
		memset(f,0,sizeof(f));
		scanf(" %s",t);
		printf("%d\n",find(t));
	}
	return 0;
}
/*
递推,如果不想dfs的话
把多模式串匹配问题化归成一个模式串匹配问题。
不用fail,不像自动机。 
*/

至此,LOJ上就可以AC了,但是你谷的加强数据还是过不了╮(╯▽╰)╭

90 pts on Luogu-Code3
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int S=2e6+5;
const int N=400;
int n,m,ml;
int tr[N*100][30],cnt;
char s[N],t[S];
bool ex[N*100],f[S];
ll zt;

inline int query(char r[],int len,int st) {
	int p=0,ret=0;
	len=min(st+ml,len);
	for(int i=st; i<len; i++) {
		int c=r[i]-'a';
		if(tr[p][c]) {
			if(ex[tr[p][c]]) {
				zt|=(1<<(i-st+1));//f[i]=true;
				ret=i+1;
			}
			p=tr[p][c];
		} else break;
	}
	return ret;
}

inline int find(char r[]) {
	int l=strlen(r);
	int ret=0;
	ret=query(r,l,0);
	int i=0;
	while(!(zt&1) && zt) zt>>=1,i++;
	while(i<l && zt) {
		ret=max(ret,query(r,l,i));
		zt>>=1; i++;
		while(!(zt&1) && zt) zt>>=1,i++;
	}
	return ret;
}

inline void ins(char r[]) {
	int l=strlen(r);
	ml=max(ml,l);
	int p=0;
	for(int i=0; i<l; i++) {
		int c=r[i]-'a';
		if(!tr[p][c]) tr[p][c]=++cnt;
		p=tr[p][c];
	}
	ex[p]=1;
}

int main() {
	scanf("%d%d",&n,&m);
	for(int i=1; i<=n; i++) {
		scanf(" %s",s);
		ins(s);
	}
	while(m--) {
		memset(f,0,sizeof(f));
		scanf(" %s",t);
		printf("%d\n",find(t));
	}
	return 0;
}
/*
递推,如果不想dfs的话
把多模式串匹配问题化归成一个模式串匹配问题。
不用fail,不像自动机。
*/

“人类的使命,在于自强不息地追求完美”,文学巨匠托尔斯泰曾言。虽然至此,已经可以ACLOJ上的题目,但是没有过你谷上的加强样例,笔者怎么会止歇?

One day later……

笔者再次遇到了瓶颈。

“不破不立”,只有敢于挑战思维定式,才能创造新的成果,突破瓶颈。我在瞎写

代码实现

这是与Code 3的不同之处:

数组:

  1. len数组为从根节点到字母i到节点到路径上,所有单词末尾的位置到标记(例如有i,it,t处的len便是1),不含当前位置(都往前记一位)
  2. 直接用ex数组储存某个单词的长度

主函数:

  1. 增加了建AC自动机的环节
  2. 把Code3的findquery简化成了直接query

query函数:

  1. 直接把文章\(T\)扔到字典图上从前往后跑
  2. f[i]为真代表从i-1i答案可能在i或i后面
  3. \(x\)为数组f的二进制记录(从高位到低位是\(1 \rightarrow i\))。

怎么想的:
AC自动机毕竟是字符串匹配利器,不用白不用。这道题和模板题的差别在于,要一个完整的单词才算数,因此想一种方法,让匹配字母的同时关注这个单词是否结束。把单词是否结尾也附在字典图上。所以想到位运算(可以压缩状态)。重点是Code3用一种直接匹配的方法,需要屡次确认是否匹配。而用位运算改进后效率大幅提升。

关于位运算句:
例如 \(x=(101001)_2\)
\(len[p]=(100000)_2\)
此时x&p>0,f[i]为真。
因为p最后一位是0,所以p不是单词的末尾,所以p所在单词的末尾一定在p后面,这意味着可以用len[p]中1标记结尾到单词和p所在的单词来替换当前组合,使答案更长。
如果len[p]的末位是1,那么替换恰好进行到位置i。因此答案\(\geq\)当前位置i。

AC Code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int S=2e6+5;
const int N=405;
int n,m;
int tr[N*100][30],cnt,ex[N*100],fail[N*100];
int f[S],len[N*100],trans[N*100];
char s[N],t[S];

inline int query(char r[]) {
	int p=0,x=0;
	int l=strlen(r+1);
	f[0]=1;
	for(int i=1; i<=l; i++) {
		p=tr[p][r[i]-'a'];
		x=((x<<1)|f[i-1])&((1<<20)-1);
        f[i]=(x&len[p])!=0;
	}
	for(int i=l;i>=1;i--){
		if(f[i]) return i;
	}
	return 0;
}

inline void build_AC(){
	queue <int> q;
	for(int i=0;i<26;i++){
		if(tr[0][i]) q.push(tr[0][i]);
	}
	while(!q.empty()){
		int p=q.front();
		q.pop();
		for(int i=0;i<26;i++){
			if(tr[p][i]){
				q.push(tr[p][i]);
				fail[tr[p][i]]=tr[fail[p]][i];
			}
			else tr[p][i]=tr[fail[p]][i];
		}
	}
	for(int i=1;i<=cnt;i++){
		int j=i;
		while(j){
			if(ex[j]) len[i]|=(1<<(ex[j]-1));
			j=fail[j];
		}
	}
}

inline void ins(char r[]) {
	int l=strlen(r);
	int p=0;
	for(int i=0; i<l; i++) {
		int c=r[i]-'a';
		if(!tr[p][c]) tr[p][c]=++cnt;
		p=tr[p][c];
	}
	ex[p]=l;
}

int main() {
	scanf("%d%d",&n,&m);
	for(int i=1; i<=n; i++) {
		scanf(" %s",s);
		ins(s);
	}
	build_AC();
	while(m--) {
		scanf(" %s",t+1);
		printf("%d\n",query(t));
	}
	return 0;
}
posted @ 2022-03-31 21:59  Searshkiu  阅读(72)  评论(0编辑  收藏  举报