【题解】 [HNOI2004] L 语言

题目传送门

题意

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

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

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

输入格式

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

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

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

输出格式

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

数据规模与约定

  • 对于 80% 的数据,保证 m20|t|106
  • 对于 100% 的数据,保证 1n201m501|s|201|t|2×106st 中均只含小写英文字母。

提示

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

思路

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

文本串的前缀s[0,i]能被理解,当且仅当某个位置j[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(n2)的时间复杂度实在算不得优秀。

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的二进制记录(从高位到低位是1i)。

怎么想的:
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。因此答案当前位置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 @   Searshkiu  阅读(143)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示