首页 > 其他分享 >[题解]P2292 [HNOI2004] L 语言

[题解]P2292 [HNOI2004] L 语言

时间:2024-08-12 17:07:08浏览次数:12  
标签:int 题解 tr fail HNOI2004 tlen P2292 节点 define

P2292 [HNOI2004] L 语言

注:

  • 下文中,\(s[l\sim r]\)表示截取字符串\(s\)的第\(l\)个字符到第\(r\)个字符。
  • 文字描述的字符串下标从\(1\)开始,但代码实现从\(0\)开始。

我们建出AC自动机后,有一个比较暴力的思路。

我么用\(f[i]\)表示待查找字符串\(t\)的长度为\(i\)前缀是否满足题意。

我们求\(f[i]\),就从匹配到\(t[i]\)时自动机上的指针\(p\)开始,沿着fail树往上跳到根节点。中途经过的状态正是\(t[1\sim i]\)的所有后缀,对于长度为\(j\)的后缀,如果它正好是一个模式串,而且\(f[i-j]=\text{true}\),那么\(f[i-j]\)的状态就可以转移到\(f[i]\)上。

其中\(len[i]\)表示以节点\(i\)结尾的字符串长度,如果没有字符串以\(i\)结尾,则值为\(0\)。

核心代码:

int query(string s){
	int p=0,n=s.size(),ans=0;
	for(int i=1;i<=n;i++){
		p=tr[p][s[i-1]-'a'];
		f[i]=0;
		for(int j=p;j;j=fail[j]){
			if(len[j]&&f[i-len[j]]){
				f[i]=1,ans=i;
				break;
			}
		}
	}
	return ans;
}
优化前的代码 - 95pts TLE
#include<bits/stdc++.h>
#define T 30//模式串个数
#define N 410//模式串总长(节点数)
#define M 2000010//单个主串长度
#define S 26//字符集大小
using namespace std;
int n,m,tr[N][S],fail[N],cnt;
int f[M],len[N];
string s[T];
queue<int> q;
void ins(string s){
	int p=0;
	for(char i:s){
		int c=i-'a';
		if(!tr[p][c])
			tr[p][c]=++cnt;
		p=tr[p][c];
	}
	len[p]=s.size();
}
void get_fail(){
	for(int i=0;i<26;i++)
		if(tr[0][i]) q.push(tr[0][i]);
	while(!q.empty()){
		int u=q.front();
		q.pop();
		for(int i=0;i<26;i++){
			if(tr[u][i])
				fail[tr[u][i]]=tr[fail[u]][i],q.push(tr[u][i]);
			else tr[u][i]=tr[fail[u]][i];
		}
	}
}
int query(string s){
	int p=0,n=s.size(),ans=0;
	for(int i=1;i<=n;i++){
		p=tr[p][s[i-1]-'a'];
		f[i]=0;
		for(int j=p;j;j=fail[j]){
			if(f[i-len[j]]&&len[j]){
				f[i]=1,ans=i;
				break;
			}
		}
	}
	return ans;
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>s[i];
		ins(s[i]);
	}
	get_fail();
	f[0]=1;
	while(m--){
		cin>>s[0];
		cout<<query(s[0])<<"\n";
	}
	return 0;
}

建立自动机的复杂度是\(O(n|s||\Sigma|)\),查询单个字符串的复杂度是\(O(|t||s|)\),总时间复杂度是\(O(n|s||\Sigma|+m|t||s|)\)。

显然这样会TLE,考虑如何优化查询。


我们发现\(|s|\le20\),所以除去根节点的话,fail树最多是\(20\)层。

因此我们可以想到状态压缩,只要把每个节点到fail树的根节点路径上的信息压缩成一个整数即可。在构建自动机时,为每个节点\(i\)维护一个\(tlen[i]\),其二进制表示下的第\(j\)位(\(j\in [1,20]\))来表示“状态\(i\)长度为\(j\)的后缀是否是一个完整的字符串”。我们可以在build_fail()中完成这一过程。

每次匹配字符串的过程中,我们再额外维护一个整数\(x\),对于当前枚举的\(t[i]\),\(x\)的二进制表示下的第\(j\)位表示\(f[i-j]\)(即\(t[1\sim i-j]\)是否合法,规定\(f[0]=1,f[负数]=0\)),每次query()都应重复该过程。

这样,我们要求\(f[i]\),仅需把\(tlen[i]\)和\(x\)按位与一下,如果结果非零则\(f[i]=1\),否则\(f[i]=0\)。

\(tlen\)和\(x\)都可以\(O(1)\)转移得到:

  • \(tlen[i]=tlen[fail[i]]+\big(2^{len[i]}\times end(i)\big)\),其中\(end(i)\)表示节点\(i\)是否是模式串结尾,是则为\(1\),不是则为\(0\)。代码实现有一些区别,但本质是一样的。
  • \(x=2\times x+f[i-1]\)。

这个优化的本质就是把不断向上跳比较的过程,转换成\(2\)个整数进行按位与操作。因此时间复杂度优化到了\(O(n|s||\Sigma|+m|t|)\),可以AC。

优化后的代码 - 100pts AC
#include<bits/stdc++.h>
#define T 30//模式串个数
#define N 410//模式串总长(节点数)
#define M 2000010//单个主串长度
#define S 26//字符集大小
using namespace std;
int n,m,tr[N][S],fail[N],cnt;
int f[M],tlen[N];
string s[T];
queue<int> q;
void ins(string s){
	int p=0;
	for(char i:s){
		int c=i-'a';
		if(!tr[p][c])
			tr[p][c]=++cnt;
		p=tr[p][c];
	}
	tlen[p]=1<<(s.size()-1);//不需要len数组了
}
void get_fail(){
	for(int i=0;i<26;i++)
		if(tr[0][i]) q.push(tr[0][i]);
	while(!q.empty()){
		int u=q.front();
		q.pop();
		for(int i=0;i<26;i++){
			if(tr[u][i])
				fail[tr[u][i]]=tr[fail[u]][i],q.push(tr[u][i]),
				tlen[tr[u][i]]|=tlen[fail[tr[u][i]]];//转移1
			else tr[u][i]=tr[fail[u]][i];
		}
	}
}
int query(string s){
	int p=0,x=0,n=s.size();
	for(int i=1;i<=n;i++){
		p=tr[p][s[i-1]-'a'];
		x=((x<<1)|f[i-1])&((1<<20)-1);//转移2
		f[i]=(x&tlen[p])!=0;
	}
	for(int i=n;i>=0;i--)
		if(f[i]) return i;
	return -1;//这一步理论走不到
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>s[i];
		ins(s[i]);
	}
	get_fail();
	f[0]=1;//初始化别忘记
	while(m--){
		cin>>s[0];
		cout<<query(s[0])<<"\n";
	}
	return 0;
}

标签:int,题解,tr,fail,HNOI2004,tlen,P2292,节点,define
From: https://www.cnblogs.com/Sinktank/p/18352694

相关文章

  • AtCoder ABC 366题解
    前言本文部分思路来自于网络,仅供参考。A-Election2题目大意给定两个市长候选人的票数,求结果是否已经确定。解题思路如果剩下的人全部投票给票少的人票少的人也不能赢,则结果就已经确定了。code#include<bits/stdc++.h>usingnamespacestd;intmain(){intn,t,......
  • ABC366D 题解
    第一眼是想写\(kd-tree\)的。然后发现这就是一道三维前缀和的板子题。三维前缀和要想学习三维前缀和,我们首先得了解前缀和的概念,并且学会一维、二维前缀和。什么是前缀和前缀和是容斥原理的典型应用。这种优化方式可以使求和操作的时间复杂度降低到\(O(1)\)(但是需要提前预......
  • 题解:AT_abc351_f [ABC351F] Double Sum
    关于某c人士强制偷袭代码导致AT号被封,\({\color{red}\mathrm{警钟敲碎}}\)。题意一个长\(n\)的数组\(a\),求所有顺序对中两元素之差的和。分析听说有大佬2min切掉。很明显,暴力过不去。考虑计算每个元素的贡献。设\(id\)为该元素之前所有比它小的元素个数,\(sum\)表示这些......
  • tensorboard_logger库无法导入的问题解决
    一、问题描述最近在学习深度学习时,从大神们那里copy的代码中有用到tensorboard_logger这个库的东西,所以很自然地就用condainstall或者pip去安装它,但是结果是:python开源库里面没有这东西。这就让我很苦恼,所以只能自己动手,丰衣足食了。 二、解决方法首先找到tensorboard_logge......
  • ABC366 G - XOR Neighbors 题解
    发现题目实质上就是让你构造一组\(a_{1,2,\dots,n}\),有一些限制,要求一些\(a\)异或起来是\(0\)。看到\(n\le60\),果断列异或方程组,用异或高斯消元。具体地,有\(n\)个方程组,\(a_{i,j}\)表示第\(i\)个方程中\(j\)的系数。对于每一个变量\(i\),要把\(j>i\)的方程中的第......
  • 【题解】P3356 火星探险问题
    \(\large\mathfrak{1st.\Preamble|}\)前言这都什么年代了网络流24题居然还能写题解!个人认为这篇题解讲的还是比较详细的。\(\large\mathfrak{2nd.\Solution|}\)题解看到题目的第一眼,我的反应是这样的:这不跟深海机器人问题差不多吗?Ctrl-CCtrl-V秒了。不过我还是讲讲怎......
  • ABC366D 题解
    Solution题意简述给你一个正整数\(N\)和\(N^3\)个非负整数,表示为\(A_{x,y,z}\)其中\(1\leqx,y,z\leqN\)。您将得到以下格式的\(Q\)个查询,必须按顺序处理。对于第\(i\)次查询\((1\leqi\leqQ)\),您将得到一个整数元组\((Lx_i,Rx_i,Ly_i,Ry_i,Lz_i,......
  • 逆序对数列(P2513) - 题解
    [HAOI2009]逆序对数列原题链接题目描述对于一个数列\(\{a_i\}\),如果有\(i<j\)且\(a_i>a_j\),那么我们称\(a_i\)与\(a_j\)为一对逆序对数。若对于任意一个由\(1\simn\)自然数组成的数列,可以很容易求出有多少个逆序对数。那么逆序对数为\(k\)的这样自然数数列到底......
  • ABC366F Maximum Composition 题解
    ABC366FMaximumComposition题解题目大意给定\(N\)个一次函数\(f_i(x)=a_ix+b_i\),从中选出\(K\)个函数\(f_{p_1},f_{p_2},\dots,f_{p_K}\),使得\(f_{p_1}(f_{p_2}(\dotsf_{p_K}))\)最大,求最大值。Solve考虑这样一种情况:我已经选定\(p_{k+1},p_{k+1},\dots,p_K\),现......
  • ABC366F 题解
    Solution题意简述现在有\(N\)个线性函数\(f_1,\dots,f_N\)。函数\(f_i(x)=A_ix+B_i\)。找到一个长度为\(K\)的序列\(p=(p_1,\dots,p_k)\),序列元素为\(K\)个大小在\([1,N]\)的不同整数。输出\(f_{p_1}(f_{p_2}(\dotsf_{p_k}(1)\dots))\)可能的最大值。思路贪心......