zoukankan      html  css  js  c++  java
  • KMP模式匹配

    概念

    ( ext{KMP}) 算法,又称模式匹配算法,能够在 (O(n+m)) 的时间复杂度内求解如下的问题:

    有两个字符串 (p[n])(s[m]) ,称字符串 (p) 为模式串、字符串 (s) 为文本串。要求判断模式串 (p) 是否为文本串 (s) 的子串,并求出模式串 (p) 在文本串 (s) 中出现的位置。

    算法流程

    1. 对模式串 (p) 进行自我匹配

    对于模式串 (p)(i) 位,求出最大的位数 (f[i]) ,使得模式串 (p) 中以第 (i) 位为结尾且长度为 (f[i]) 的子串和模式串 (p)(f[i]) 位组成的字符串重合。这里的 (f) 数组就是模式串 (p) 的失配数组。

    1. 扫描一遍文本串 (s) ,利用 (f) 数组寻找模式串 (p)

    假设现在文本串 (s) 已经匹配到第 (i) 位,模式串 (p) 已经匹配到第 (j) 位,此时会出现以下两种情况:

    (s[i]=p[j]) ,即当前字符匹配成功,就令 (i++)(j++) ,继续匹配下一对字符;

    (s[i]≠p[j]) ,即当前字符匹配失败(也称“失配”),由于模式串 (p)(f[j]) 位组成的字符串与文本串 (s)(i) 位前 (f[j]) 位组成的字符串依然重合(失配数组 (f) 的作用),我们就令 (j=f[j]) ,也就相当于把模式串 (p) 向右移动了 (j-f[j]) 位,重新判断当前的 (s[i]) 是否等于 (p[j])

    当然,如果已经达到 (j=n) 的时候,就说明当前模式串 (p) 已经在文本串 (s) 中完全匹配,我们便可以记录下答案。

    如此下去,便能够求出模式串 (p) 在文本串 (s) 中所有的位置,由此达到我们的目的。

    代码

    1. 求模式串 (p) 的失配数组 (f)
    for(int i=2,j=0;i<=n;i++)
    {
    	while(j>0&&p[i]!=p[j+1])
    	{
    		j=f[j];
    	}
    	if(p[i]==p[j+1])
    	{
    		j++;
    	}
    	f[i]=j;
    }
    
    1. 文本串 (s) 与模式串 (p) 的匹配过程:
    for(int i=1,j=0;i<=m;i++)
    {
    	while(j>0&&s[i]!=p[j+1])
    	{
    		j=f[j];
    	}
    	if(s[i]==p[j+1])
    	{
    		j++;
    	}
    	if(j==n)
    	{
    		printf("%d
    ",i-n+1);
    		j=f[j];
    	}
    }
    

    典型例题

    1. Luogu P4391 [BOI2009] Radio Transmission 无线传输

    ( ext{Solution})

    这是一道求失配数组 (f) 的题,比较基础。

    #include<iostream>
    #include<cstdio>
    #define Re register
    using namespace std;
    
    const int SIZE=2000001;
    
    char st[SIZE];
    int L,f[SIZE];
    
    int main()
    {
    	scanf("%d",&L);
    	for(Re int i=1;i<=L;i++)
    	{
    		cin>>st[i];
    	}
    	for(Re int i=2,j=0;i<=L;i++)
    	{
    		while(j>0&&st[i]!=st[j+1])
    		{
    			j=f[j];
    		}
    		if(st[i]==st[j+1])
    		{
    			j++;
    		}
    		f[i]=j;
    	}
    	printf("%d",L-f[L]);
    	return 0;
    }
    
    1. Luogu P3435 [POI2006] OKR-Periods of Words

    ( ext{Solution})

    此题用到失配数组 (f) 的性质:对于每个 (i) 而言,长度为 (f[i]) 的前缀与长度为 (f[i]) 的后缀是相等的。

    由题意可知,如果 (i) 有一个公共的前后缀,其长度为 (l) ,那么这个前缀 (i) 就有一个周期为 (i-l)

    于是我们用上失配数组 (f) 求解即可。

    但是,本题求的是最大的周期,因此我们应该求出最小的 (l) 满足题意。

    当然,这里可以用“记忆化”的思想来简化时间复杂度。

    #include<iostream>
    #include<cstdio>
    #define Re register
    #define LL long long
    using namespace std;
    
    const int SIZE=2000001;
    
    char st[SIZE];
    int L,f[SIZE];
    LL Ans;
    
    int main()
    {
    	scanf("%d",&L);
    	scanf("%s",st+1);
    	for(Re int i=2,j=0;i<=L;i++)
    	{
    		while(j>0&&st[i]!=st[j+1]) j=f[j];
    		if(st[i]==st[j+1]) j++;
    		f[i]=j;
    	}
    	for(Re int i=1;i<=L;i++)
    	{
    		int Now=i;
    		while(f[Now]>0) Now=f[Now];
    		if(f[i]>0) f[i]=Now;
    		Ans+=(i-Now);
    	} 
    	printf("%lld",Ans);
    	return 0;
    }
    
    1. Luogu P1470 [USACO2.3] 最长前缀 Longest Prefix

    ( ext{Solution})

    此题在 ( ext{KMP}) 算法的基础上加了差分的思想。

    对于每一个 (1≤i≤card(O)) ,在每次用 ( ext{KMP}) 算法找到 (s)(O[i]) 所在的位置 ([L,R]) 时,就修改一下差分数组。

    最后对差分数组求前缀和,大于 (0) 即为满足题意的长度,输出最大的长度即可。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #define Re register
    using namespace std;
    
    char opt[301][21],st[300001];
    int n,m;
    int Len[301];
    int f[301];
    int dif[300001];
    
    int main()
    {
    	while(1)
    	{
    		scanf("%s",opt[++m]+1);
    		if(opt[m][1]=='.') break;
    		Len[m]=strlen(opt[m]+1);
    	}
    	m--;
    	while(scanf("%s",st+n+1)!=EOF)
    	{
    		n=strlen(st+1);
    	}
    	for(Re int k=1;k<=m;k++)
    	{
    		memset(f,0,sizeof f);
    		for(Re int i=2,j=0;i<=Len[k];i++)
    		{
    			while(j>0&&opt[k][i]!=opt[k][j+1])
    			{
    				j=f[j];
    			}
    			if(opt[k][i]==opt[k][j+1])
    			{
    				j++;
    			}
    			f[i]=j;
    		}
    		for(Re int i=1,j=0;i<=n;i++)
    		{
    			while(j>0&&st[i]!=opt[k][j+1])
    			{
    				j=f[j];
    			}
    			if(st[i]==opt[k][j+1])
    			{
    				j++;
    			}
    			if(j==Len[k])
    			{
    				dif[i-Len[k]+1]++;
    				dif[i+1]--;
    				j=f[j];
    			}
    		}
    	}
    	for(Re int i=1;i<=n;i++)
    	{
    		dif[i]+=dif[i-1];
    		if(dif[i]<=0)
    		{
    			printf("%d",i-1);
    			return 0;
    		}
    	}
    	printf("%d",n);
    	return 0;
    }
    
    1. P4824 [USACO15FEB] Censoring S

    ( ext{Solution})

    此题还用到了栈 ( ext{stack}) 的思想。

    ( ext{KMP}) 算法过程中,把遍历 (S) 时遍历到的 (i) 入栈(本题栈中存的最好是下标)。

    当匹配上 (T) 时,就把匹配成功的部分出栈,然后 (j) 从栈中所能匹配到的最大的位置开始,即 (j=f[stack.len]) ,继续做下去。

    最后把栈中的元素输出即可。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #define Re register
    using namespace std;
    
    const int SIZE=2000001;
    
    char s[SIZE],t[SIZE];
    int n,m,Len;
    int f[SIZE];
    int Last[SIZE];
    int Stack[SIZE];
    
    int main()
    {
    	scanf("%s",s+1);
    	scanf("%s",t+1);
    	n=strlen(s+1);
    	m=strlen(t+1);
    	for(Re int i=2,j=0;i<=m;i++)
    	{
    		while(j>0&&t[i]!=t[j+1])
    		{
    			j=f[j];
    		}
    		if(t[i]==t[j+1])
    		{
    			j++;
    		}
    		f[i]=j;
    	}
    	for(Re int i=1,j=0;i<=n;i++)
    	{
    		while(j>0&&s[i]!=t[j+1])
    		{
    			j=f[j];
    		}
    		if(s[i]==t[j+1])
    		{
    			j++;
    		}
    		Last[i]=j;
    		Stack[++Len]=i;
    		if(j==m)
    		{
    			Len-=m;
    			j=Last[Stack[Len]];
    		}
    	}
    	for(Re int i=1;i<=Len;i++)
    	{
    		printf("%c",s[Stack[i]]);
    	}
    	return 0;
    }
    

    总结

    ( ext{KMP}) 算法的思想在于把模式串 (p) 的失配数组 (f)(O(N)) 的时间复杂度预处理,并在与文本串 (s) 匹配的过程中起到关键性的作用:匹配失败就跳到下一个有可能的位置再匹配。相比较于朴素算法,优化了很多不必要的匹配过程。而这也是 ( ext{KMP}) 算法中最为重要的思想所在。

  • 相关阅读:
    输入一个1-9的数i,再输入一个数字n,表示 i 出现的次数,输入的2个数字 i 和 n 组合成如下表达式:如i=2,n=4,2+22+222+2222=?,计算结果是多少?
    现有数列1/2;2/3;3/5;5/8······第十次出现的是什么?
    猜数游戏:范围时1-100,若错误就提示大了还是小了,猜对则结束,允许猜10次,游戏结束后对玩家评价:1次猜对;5次内猜对;10次内猜对;没有猜对
    登录模拟,用户名和密码输入错误后给出相关错误提示,并告知还有多少次错误机会,如果5次验证失败将冻结账户
    30人围坐轮流表演节目,按顺序数1-3,每次数到3的人就表演节目,表演过的人不再参加报数,那么在仅剩一个人没有表演的时候,共报数多少人次?
    docker 自定义镜像
    php 镜像richarvey/nginx-php-fpm的ngnix配置
    php tp5常用小知识
    php Tp5下mysql的增删改查
    php 面试常问问题
  • 原文地址:https://www.cnblogs.com/kebingyi/p/14015825.html
Copyright © 2011-2022 走看看