【题解】 [HNOI2004] L 语言
题目传送门
题意
题目描述
一段文章 、一个单词 由若干小写字母构成。一个字典 是若干个单词的集合。若文章 可以被分成若干部分,且每一个部分都是字典 中的单词,则称一段文章 在某个字典 下是可以被理解的。
例如字典 中包括单词 ,则文章 是在字典 下可以被理解的,因为它可以分成 个单词:,且每个单词都属于字典 ,而文章 在字典 下不能被理解,但可以在字典 下被理解。这段文章的一个前缀 ,也可以在字典 下被理解,而且是在字典 下能够被理解的最长的前缀。
给定一个字典 ,你的程序需要判断若干段文章在字典 下是否能够被理解。并给出其在字典 下能够被理解的最长前缀的位置。
输入格式
第一行两个整数 和 ,表示字典 中有 个单词,且有 段文章需要被处理。
接下来 行,每行一个字符串 ,表示字典 中的一个单词。
接下来 行,每行一个字符串 ,表示一篇文章。
输出格式
对于输入的每一篇文章,你需要输出一行一个整数,表示这段文章在字典 可以被理解的最长前缀的位置。
数据规模与约定
- 对于 的数据,保证 ,。
- 对于 的数据,保证 ,,,, 与 中均只含小写英文字母。
提示
- 请注意数据读入对程序效率造成的影响。
- 请注意【数据规模与约定】中标注的串长是单串长度,并不是字符串长度和。
思路
由于你谷的数据有加强,所以主要以你谷上的得分为对照。
壹
文本串的前缀能被理解,当且仅当某个位置满足能够被理解,并且能够被理解。
一个朴素的想法:
用und
数组记录文本串恰好能被理解时的位置。文本串从头到尾遍历,每次都枚举und
里表示的能被理解的前缀,然后在字典里面验证是否能被理解。最后更新und
数组。
用f
数组记录截至当前的i
,能够被理解的最长前缀。由于能被理解的最长前缀长度序列不下降,因此递推时只需要取当前和前一位置f
数组的较大值。
朴素的算法必无法从这题手里骗到满分。事实上,的时间复杂度实在算不得优秀。
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的不同之处:
数组:
len
数组为从根节点到字母i
到节点到路径上,所有单词末尾的位置到标记(例如有i,it,t处的len
便是1
),不含当前位置(都往前记一位)- 直接用
ex
数组储存某个单词的长度
主函数:
- 增加了建AC自动机的环节
- 把Code3的
find
套query
简化成了直接query
query
函数:
- 直接把文章扔到字典图上从前往后跑
f[i]
为真代表从i-1
到i
答案可能在i或i后面- 为数组
f
的二进制记录(从高位到低位是)。
怎么想的:
AC自动机毕竟是字符串匹配利器,不用白不用。这道题和模板题的差别在于,要一个完整的单词才算数,因此想一种方法,让匹配字母的同时关注这个单词是否结束。把单词是否结尾也附在字典图上。所以想到位运算(可以压缩状态)。重点是Code3用一种直接匹配的方法,需要屡次确认是否匹配。而用位运算改进后效率大幅提升。
关于位运算句:
例如
此时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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现