[Cqoi2014]通配符匹配:哈希+动态规划
前置知识:字符串哈希
P3167 [CQOI2014] 通配符匹配
题目描述
几乎所有操作系统的命令行界面(CLI)中都支持文件名的通配符匹配以方便用户。最常见的通配符有两个,一个是星号(*
),可以匹配 0 个及以上的任意字符:另一个是问号(?
),可以匹配恰好一个任意字符。现在需要你编写一个程序,对于给定的文件名列表和一个包含通配符的字符串,判断哪些文件可以被匹配。
输入格式
第一行是一个由小写字母和上述通配符组成的字符串。第二行包含一个整数 \(n\),表示文件个数。接下来 \(n\) 行,每行为一个仅包含小写字母字符串,表示文件名列表。
输出格式
输出 \(n\) 行,每行为 YES
或 NO
,表示对应文件能否被通配符匹配。
输入输出样例 #1
输入 #1
*aca?ctc
6
acaacatctc
acatctc
aacacatctc
aggggcaacacctc
aggggcaacatctc
aggggcaacctct
输出 #1
YES
YES
YES
YES
YES
NO
说明/提示
对于 \(100 \%\) 的数据
- 字符串长度不超过 \(100000\)
- \(1 \le n \le 100\)
- 通配符个数不超过 \(10\)
思路+部分分代码+AC代码
首先这道题前面的字符能否匹配上直接影响到后面的状态,这不就是DP嘛!
还有个需要思考的问题,通配符是无法直接比较的,怎么办呢?我们可以分段处理,遇到通配符就断开,而遇到通配符时肯定能匹配上。
接下来考虑转移,我们按通配符分组了,通配符不超过十个,那么我们可以设f[i][j]=0/1,表示文本串的前j个字符能否匹配上前i段通配符串。为什么这么设置呢?回看我们最开始想到的,dp是为了用前面字符串能否匹配来判断后面的,本问题中实际上是用来减少时间复杂度的,看代码会更清晰一些。
第一版(90pts):
#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long//hash用的
using namespace std;
inline int read(){int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch-'0');ch=getchar();}return x*f;}inline void write(int x){if(x<0)x*=-1,putchar('-');if(x>9)write(x/10);putchar(x%10+'0');return;}inline int max(int x,int y){return (x<y)?y:x;}inline int min(int x,int y){return (x<y)?x:y;}
//一些优化(不重要,可略过)
const int N=1e5+10,P=131;
ull p[N],h1[N],h2[N];
int n,f[12][N],id[N],num;//id数组用来记录通配符的位置
char s[N],t[N];
ull get(int l,int r,ull h[])//获得字串hash
{
return h[r]-h[l-1]*p[r-l+1];
}
signed main()
{
p[0]=1;//不要忘记初始化
for(int i=1;i<=N;i++)p[i]=p[i-1]*P;
cin>>s+1;//s+1代表字符下标从1开始
n=read();
int len=strlen(s+1);
s[++len]='?';//如果最后一个不是?在下面循
//环求h1的时候最后一段不会算上,手动模拟一下就懂啦
for(int i=1;i<=len;i++)
if(s[i]=='?'||s[i]=='*') id[++num]=i;
//保证最后的id[num]是我们补的最后一个?
for(int i=1;i<=num;i++)
for(int j=id[i-1]+1;j<=id[i]-1;j++)//从上一个通配符以后
h1[i]=h1[i]*P+s[j];//到下一个通配符之前是一段
//h1[i]代表第i段的哈希值
while(n--)
{
memset(f,0,sizeof(f));
f[0][0]=1;//初始化,dp必不可少的一趴
cin>>t+1;
int siz=strlen(t+1);
t[++siz]='a';//因为上文多加了一个?这里我们相应地补上任意一个字母即可
for(int i=1;i<=siz;i++)
h2[i]=h2[i-1]*P+t[i];
for(int i=0;i<num;i++)
for(int j=0;j<=siz;j++)
{
if(f[i][j]==0)continue;//这就是dp的主要用途
//因为我们要用现在的i来推i+1的状态,如果这个j连i都匹配不上,那i+1必然也不行
if(s[id[i+1]]=='?')//下一个通配符是'?'
{
if(h1[i+1]!=get(j+1,j+id[i+1]-id[i]-1,h2))continue;//匹配不上
f[i+1][j+id[i+1]-id[i]]=1;//匹配上了,不用-1是因为?占一个下标,且是一定可以被匹配上的
}
else
{
if(h1[i+1]!=get(j+1,j+id[i+1]-id[i]-1,h2))continue;
for(int z=j+id[i+1]-id[i]-1;z<=siz;z++)f[i+1][z]=1;//不同点就是*可以是任意多个字符,i+1段后面的都可以被匹配
}
}
if(f[num][siz])printf("YES");
else printf("NO");
putchar('\n');
}
return 0;
}
哈哈,惊讶地发现T了一点,距离AC只有yi步之遥.显然这个时间复杂度还不够优秀,继续想想哪里可以优化吧( '-' ).请先理解上面代码.
#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int maxn=1e5+10;
const ull P=131;
ull p[maxn],h1[maxn],h2[maxn];
int q,f[12][maxn],id[maxn],num,m,n;
char s[maxn],t[maxn];
ull get(int l,int r,ull h[])
{
if(r<l)return 0;
return h[r]-h[l-1]*p[r-l+1];
}
int main()
{
p[0]=1;
for(int i=1;i<=100000;i++)p[i]=p[i-1]*P;
scanf("%s%d",s+1,&q);
n=strlen(s+1);s[++n]='?';
for(int i=1;i<=n;i++) if(s[i]=='*'||s[i]=='?') id[++num]=i;
for(int i=1;i<=num;i++)
for(int j=id[i-1]+1;j<=id[i]-1;j++)
h1[i]=h1[i]*P+s[j];
while(q--)
{
scanf("%s",t+1);m=strlen(t+1);t[++m]='k';
memset(f,0,sizeof(f));
f[0][0]=1;
for(int i=1;i<=m;i++)h2[i]=h2[i-1]*P+t[i];
for(int i=0;i<num;i++)
for(int j=0;j<=m;j++)
{
if(f[i][j]==0)continue;
if(f[i][j]==2)f[i][j+1]=2;//2是*这种情况对后面的一个延时标记,省去了for循环的复杂度
if(h1[i+1]!=get(j+1,j+id[i+1]-id[i]-1,h2))continue;//因为不管通配符是?还是*都要先判断,直接写在前面
if(s[id[i+1]]=='?')f[i+1][j+id[i+1]-id[i]]=1;
else f[i+1][j+id[i+1]-id[i]-1]=2;
}
if(f[num][m])printf("YES");
else printf("NO");
putchar('\n');
}
return 0;
}
应该很详细了吧,如果有错误或者不理解的地方欢迎评论区指出哦( ^ _ ^ )!
参考博客(https://blog.csdn.net/jziwjxjd/article/details/113795426?spm=1001.2014.3001.5506)
码字不易,点个赞再走呗(卖萌)