前缀函数与KMP算法

给定一个长度为的字符串,其前缀函数被定义为一个长度为的数组。其中的定义是:

简单来说就是,子串最长的相等的真前缀与真后缀的长度。

用数学语言描述如下:

特别地,规定。

举例来说,对于字符串abcabcd,

,因为a没有真前缀和真后缀,根据规定为0

,因为ab无相等的真前缀和真后缀

,因为abc无相等的真前缀和真后缀

,因为abca只有一对相等的真前缀和真后缀:a,长度为1

,因为abcab相等的真前缀和真后缀只有ab,长度为2

,因为abcabc相等的真前缀和真后缀只有abc,长度为3

,因为abcabcd无相等的真前缀和真后缀

同理可以计算字符串aabaaab的前缀函数为。

一个直接按照定义计算前缀函数的算法流程:

具体实现如下:

第一个重要的观察是相邻的前缀函数值至多增加。

参照下图所示,只需如此考虑:当取一个尽可能大的时,必然要求新增的也与之对应的字符匹配,即,此时。

所以当移动到下一个位置时,前缀函数的值要么增加一,要么维持不变,要么减少。

此时的改进的算法为:

1234567891011vectorprefix_function(strings){intn=(int)s.length();vectorpi(n);for(inti=1;i=0;j--)//improved:j=i=>j=pi[i-1]+1if(s.substr(0,j)==s.substr(i-j+1,j)){pi[i]=j;break;}returnpi;}123456789defprefix_function(s):n=len(s)pi=[0]*nforiinrange(1,n):forjinrange(pi[i-1]+1,-1,-1):ifs[0:j]==s[i-j+1:i+1]:pi[i]=jbreakreturnpi12345678910111213staticint[]prefix_function(Strings){intn=s.length();int[]pi=newint[n];for(inti=1;i=0;j--){if(s.substring(0,j).equals(s.substring(i-j+1,i+1))){pi[i]=j;break;}}}returnpi;}在这个初步改进的算法中,在计算每个时,最好的情况是第一次字符串比较就完成了匹配,也就是说基础的字符串比较次数是n-1次。

而由于存在j=pi[i-1]+1(pi[0]=0)对于最大字符串比较次数的限制,可以看出每次只有在最好情况才会为字符串比较次数的上限积累1,而每次超过一次的字符串比较消耗的是之后次数的增长空间。

由此我们可以得出字符串比较次数最多的一种情况:至少1次字符串比较次数的消耗和最多n-2次比较次数的积累,此时字符串比较次数为n-1+n-2=2n-3。

可见经过此次优化,计算前缀函数只需要进行次字符串比较,总复杂度降为了。

在第一个优化中,我们讨论了计算时的最好情况:,此时。现在让我们沿着这个思路走得更远一点:讨论当时如何跳转。

失配时,我们希望找到对于子串,仅次于的第二长度,使得在位置的前缀性质仍得以保持,也即:

如果我们找到了这样的长度,那么仅需要再次比较和。如果它们相等,那么就有。否则,我们需要找到子串仅次于的第二长度,使得前缀性质得以保持,如此反复,直到。如果,则。

观察上图可以发现,因为,所以对于的第二长度,有这样的性质:

也就是说等价于子串的前缀函数值,即。同理,次于的第二长度等价于的前缀函数值,

显然我们可以得到一个关于的状态转移方程:0)"src=data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7>

所以最终我们可以构建一个不需要进行任何字符串比较,并且只进行次操作的算法。

而且该算法的实现出人意料的短且直观:

1234567891011vectorprefix_function(strings){intn=(int)s.length();vectorpi(n);for(inti=1;i0&&s[i]!=s[j])j=pi[j-1];if(s[i]==s[j])j++;pi[i]=j;}returnpi;}1234567891011defprefix_function(s):n=len(s)pi=[0]*nforiinrange(1,n):j=pi[i-1]whilej>0ands[i]!=s[j]:j=pi[j-1]ifs[i]==s[j]:j+=1pi[i]=jreturnpi123456789101112131415staticint[]prefix_function(Strings){intn=s.length();int[]pi=newint[n];for(inti=1;i0&&s.charAt(i)!=s.charAt(j)){j=pi[j-1];}if(s.charAt(i)==s.charAt(j)){j++;}pi[i]=j;}returnpi;}这是一个在线算法,即其当数据到达时处理它——举例来说,你可以一个字符一个字符的读取字符串,立即处理它们以计算出每个字符的前缀函数值。该算法仍然需要存储字符串本身以及先前计算过的前缀函数值,但如果我们已经预先知道该字符串前缀函数的最大可能取值,那么我们仅需要存储该字符串的前个字符以及对应的前缀函数值。

该任务是前缀函数的一个典型应用。

给定一个文本和一个字符串,我们尝试找到并展示在中的所有出现(occurrence)。

为了简便起见,我们用表示字符串的长度,用表示文本的长度。

我们构造一个字符串,其中为一个既不出现在中也不出现在中的分隔符。接下来计算该字符串的前缀函数。现在考虑该前缀函数除去最开始个值(即属于字符串和分隔符的函数值)后其余函数值的意义。根据定义,为右端点在且同时为一个前缀的最长真子串的长度,具体到我们的这种情况下,其值为与的前缀相同且右端点位于的最长子串的长度。由于分隔符的存在,该长度不可能超过。而如果等式成立,则意味着完整出现在该位置(即其右端点位于位置)。注意该位置的下标是对字符串而言的。

因此如果在某一位置有成立,则字符串在字符串的处出现。

正如在前缀函数的计算中已经提到的那样,如果我们知道前缀函数的值永远不超过一特定值,那么我们不需要存储整个字符串以及整个前缀函数,而只需要二者开头的一部分。在我们这种情况下这意味着只需要存储字符串以及相应的前缀函数值即可。我们可以一次读入字符串的一个字符并计算当前位置的前缀函数值。

对字符串和,若长度为的前缀和长度为的后缀相等,就称长度为的前缀是的border。

由有长度为的border可以推导出是的周期。

在该节我们将同时讨论两个问题。给定一个长度为的字符串,在问题的第一个变种中我们希望统计每个前缀在同一个字符串的出现次数,在问题的第二个变种中我们希望统计每个前缀在另一个给定字符串中的出现次数。

首先让我们来解决第一个问题。考虑位置的前缀函数值。根据定义,其意味着字符串一个长度为的前缀在位置出现并以为右端点,同时不存在一个更长的前缀满足前述定义。与此同时,更短的前缀可能以该位置为右端点。容易看出,我们遇到了在计算前缀函数时已经回答过的问题:给定一个长度为的前缀,同时其也是一个右端点位于的后缀,下一个更小的前缀长度是多少?该长度的前缀需同时也是一个右端点为的后缀。因此以位置为右端点,有长度为的前缀,有长度为的前缀,有长度为的前缀,等等,直到长度变为。故而我们可以通过下述方式计算答案。

给定一个长度为的字符串,我们希望计算其本质不同子串的数目。

我们将迭代的解决该问题。换句话说,在知道了当前的本质不同子串的数目的情况下,我们要找出一种在末尾添加一个字符后重新计算该数目的方法。

令为当前的本质不同子串数量。我们添加一个新的字符至。显然,会有一些新的子串以字符结尾。我们希望对这些以该字符结尾且我们之前未曾遇到的子串计数。

构造字符串并将其反转得到字符串。现在我们的任务变为计算有多少的前缀未在的其余任何地方出现。如果我们计算了的前缀函数最大值,那么最长的出现在中的前缀其长度为。自然的,所有更短的前缀也出现了。

因此,当添加了一个新字符后新出现的子串数目为。

值得注意的是,我们也可以重新计算在头部添加一个字符,或者从尾或者头移除一个字符时的本质不同子串数目。

给定一个长度为的字符串,我们希望找到其最短的「压缩」表示,也即我们希望寻找一个最短的字符串,使得可以被的一份或多份拷贝的拼接表示。

显然,我们只需要找到的长度即可。知道了该长度,该问题的答案即为长度为该值的的前缀。

让我们计算的前缀函数。通过使用该函数的最后一个值,我们定义值。我们将证明,如果整除,那么就是答案,否则不存在一个有效的压缩,故答案为。

假定可被整除。那么字符串可被划分为长度为的若干块。根据前缀函数的定义,该字符串长度为的前缀等于其后缀。但是这意味着最后一个块同倒数第二个块相等,并且倒数第二个块同倒数第三个块相等,等等。作为其结果,所有块都是相等的,因此我们可以将字符串压缩至长度。

诚然,我们仍需证明该值为最优解。实际上,如果有一个比更小的压缩表示,那么前缀函数的最后一个值必定比要大。因此就是答案。

根据扩展欧几里得算法我们可以得到一组和使得。通过与等式适当叠加我们可以得到一组0"src=data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7>和使得。这意味着通过不断应用前述方程组中的方程我们可以得到新的方程组。

由于整除,这意味着是的一个周期。又因为n-p"src=data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7>,故有,所以是一个比更小的的周期。因此字符串有一个长度为的压缩表示,同的最小性矛盾。

综上所述,不存在一个长度小于的压缩表示,因此答案为。

让我们重新回到通过一个分隔符将两个字符串拼接的新字符串。对于字符串和我们计算的前缀函数。显然,因为是一个分隔符,前缀函数值永远不会超过。因此我们只需要存储字符串和其对应的前缀函数值,之后就可以动态计算对于之后所有字符的前缀函数值:

实际上在这种情况下,知道的下一个字符以及之前位置的前缀函数值便足以计算下一个位置的前缀函数值,而不需要用到任何其它的字符和对应的前缀函数值。

换句话说,我们可以构造一个自动机(一个有限状态机):其状态为当前的前缀函数值,而从一个状态到另一个状态的转移则由下一个字符确定。

因此,即使没有字符串,我们同样可以应用构造转移表的算法构造一个转移表:

该自动机在什么时候有用呢?首先,记得大部分时候我们为了一个目的使用字符串的前缀函数:寻找字符串在字符串中的所有出现。

因此使用该自动机的最直接的好处是加速计算字符串的前缀函数。

通过构建的自动机,我们不再需要存储字符串以及其对应的前缀函数值。所有转移已经在表中计算过了。

但除此以外,还有第二个不那么直接的应用。我们可以在字符串是某些通过一些规则构造的巨型字符串时,使用该自动机加速计算。Gray字符串,或者一个由一些短的输入串的递归组合所构造的字符串都是这种例子。

出于完整性考虑,我们来解决这样一个问题:给定一个数,以及一个长度的字符串,我们需要计算在第个Gray字符串中的出现次数。回想起Gray字符串以下述方式定义:

由于其天文数字般的长度,在这种情况下即使构造字符串都是不可能的:第个Gray字符串有个字符。然而我们可以在仅仅知道开头若干前缀函数值的情况下,有效计算该字符串末尾的前缀函数值。

除了自动机之外,我们同时需要计算值:在从状态开始处理后的自动机的状态,以及值:当从状态开始处理后,在中的出现次数。实际上为在执行操作时前缀函数取值为的次数。易得问题的答案为。

我们该如何计算这些值呢?首先根据定义,初始条件为以及。之后所有值可以通过先前的值以及使用自动机计算得到。为了对某个计算相应值,回想起字符串由,字母表中第个字符,以及三者拼接而成。因此自动机会途径下列状态:

的值同样可被简单计算。

其中当其中表达式取值为真时值为,否则为。综上,我们已经可以解决关于Gray字符串的问题,以及一大类与之类似的问题。举例来说,应用同样的方法可以解决下列问题:给定一个字符串以及一些模式,其中每个模式以下列方式给出:该模式由普通字符组成,当中可能以的形式递归插入先前的字符串,也即在该位置我们必须插入字符串次。以下是这些模式的一个例子:

递归代入会使字符串长度爆炸式增长,他们的长度甚至可以达到的数量级。而我们必须找到字符串在每个字符串中的出现次数。

该问题同样可通过构造前缀函数的自动机解决。同之前一样,我们利用先前计算过的结果对每个模式计算其转移然后相应统计答案即可。

THE END
1.什么是算法?(翻译文章)算法的概念来自于哪个数学家“算法”一词源自波斯学者Abdullah Jafar Muhammad ibn Musa Al-Khwarizmi的名字,他是九世纪的数学家和天文学家。他的工作为代数和数学算法过程的发展奠定了基础。他经常被称为“代数之父”。Al-Khwarizmi 对算法定义的贡献是深远的: 算法是一种定义明确的计算程序,由一组有限的步骤组成,接受一个或多个输入并产生https://blog.csdn.net/qq_20245171/article/details/143428003
2.科技名词算法algorithm科技博览科普博览资讯核心提示:算法algorithm定义:解决给定问题的确定的计算机指令序列,用以系统地描述解决问题的步骤。学科:计算机科学技术_理论计算机科学_算法设计与分析相关名词:指令 程序 软件开发图片来源:视觉中国【延伸阅读】算法是解题方案准确而完整的描述,是一系列解决问题的清晰指令,代表着用系统的方法描述解决问题的策略机制。也就http://www.agricoop.net/news/show.php?itemid=21242
3.算法基础入门概述著名计算机科学家沃思(NiklausWirth)提出一个公式:算法 + 数据结构 = 程序,其中算法是程序的灵魂。在数学和计算机科学/算学之中,算法/演算法/算则法(algorithm)为一个计算的具体步骤,常用于计算、数据处理和自动推理。精确而言,算法是一个表示为有限长列表的有效方法。算法应包含清晰定义的指令用于计算函数。http://baijiahao.baidu.com/s?id=1658978532936320587&wfr=spider&for=pc
4.百科常见的加密算法有哪些?一文了解区块链中常见的加密算法。 0x00 密码学 互联网世界,密码无处不在。中心化的系统存在账户,有账户就有账户名和密码,密码可以说是标识账号归属的最重要手段之一。 我们来看维基百科怎么阐释 密码学。 密码学(英语:Cryptography)可分为古典密码学和现代密码学。在西欧语文中,密码学一词源于希腊语 kryptós“隐https://maimai.cn/article/detail?fid=380988923&efid=UWcLYeEnR7bgyIXld5eUrQ
5.算法随意问技术百科算法标签下的所有问题http://tool.suiyiwen.com/tag/%E7%AE%97%E6%B3%95
6.什么是极光算法,极光算法的应用领域与发展历程–云服务器CVM网1.极光算法百科 极光算法是一种基于表面密度函数的流体*算法,主要用于计算液体的运动与形态变化。早期由斯坦福大学的JosS*教授在2003年提出,后来又得到了很多人的改进和发展。 极光算法的核心思想是将液体表面完整而真实地呈现出来,以及利用表面上的力来计算液体运动的影响。它与传统的粒子法和网格法不同,可以有效地防https://cvmecs.com/23779.html
7.算法算法 百科解释目录 1 概述 2 历史发展 3 算法分类 4 算法特征 5 算法的描述 目录 1 概述 2 历史发展 3 算法分类 4 算法特征 5 算法的描述 算法- 概述 求解问题类的、机械的、统一的方法,它由有限多个步骤组成,对于问题类中的每个给定的具体问题,机械地执行这些步骤就可以得到问题的https://www.mscbsc.com/cidian/baikeapb
8.算法“算法”是简易百科的Tags,Tags信息表是通过对词条“算法”进行分类和标记,以便用户更好地了解和搜索相关内容。通过使用Tags,用户可以更方便地查找和比较不同词条之间的相似性和差异性,从而更好地理解和掌握相关知识。同时,简易百科的tags还可以帮助用户发现新的兴趣点https://www.isolves.com/e/tags/?tagname=%E7%AE%97%E6%B3%95
9.百科什么是思维算法(AoT)?百科| 什么是思维算法(AoT)? 摘要 思维算法 (AoT) 是人工智能 (AI) 领域的一种突破性方法,彻底改变了 AI 模型的思考和推理方式 。 币界网报道: 作者:Aimen Noor,CoinTelegraph;编译:五铢, 一、思维算法(AoT)的解释 AoT 通过模仿人类思维过程来增强 AI 推理能力,提高解决问题的适应性和效率。https://m.528btc.com/news/116212848.html
10.算法算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制[1]。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。如果一个算法有缺陷,或不适合于某个问题,执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、空间或效率来https://baike.sogou.com/v105662.htm
11.算法本专题为您整理有关于算法的行业信息及相关加密资产动态。 Float Protocol 降低短期波动性的稳定币。 DefiDollar 尝试成为稳定币的指数,使用 DeFi 基本元素保持在美元附近,并补贴抵押率。 Stand Cash 具有储备资产机制的算法稳定币。 Fei Protocol 去中心化、可扩展且公平的稳定币。 https://www.btcbaike.com/zt/37awt.html
12.粒子群算法(ParticleswarmoptimizationPSO)百度百科版本 粒子群算法,也称粒子群优化算法或鸟群觅食算法(Particle Swarm Optimization),缩写为 PSO, 是由J. Kennedy和R. C. Eberhart等开发的一种新的进化算法(Evolutionary Algorithm – EA)。 PSO 算法属于进化算法的一种,和模拟退火算法相似,它也是从随机解出发,通过迭代寻找最优解,它也是通过适应度来评价https://cloud.tencent.com/developer/article/1555832
13.算法在数学和计算机科学之中,算法(Algorithm)为一个计算的具体步骤,常用于计算、数据处理和自动推理。精确而言,算法是一个表示为有限长列表的有效方法。算法应包含清晰定义的指令用于计算函数。 来自维基百科: 算法 综合 百科 VIP 热门 动态 论文 精华暂无数据参考https://zhuanzhi.ai/topic/2001515932557420/baike
14.遗传算法遗传算法遗传算法(Genetic Algorithm)是一种模拟生物进化过程的优化算法。它是通过模拟自然选择、遗传交叉和变异等生物遗传学中的基本原理来搜索最优解的方法。遗传算法在解决复杂问题、优化函数、机器学习等领域具有广泛应用。 遗传算法的基本思想是通过对候选解进行编码,以构建一个称为"染色体"的表达式。这些染色体通过交https://vebaike.com/doc-view-1808.html
15.一文通俗解释什么是哈希算法!什么是哈希算法?币种百科在了解比特币投资和区块链技术中,哈希算法可以说经常出现,币圈戏言说唱有嘻哈,算法有哈希。关于“算法”一词,目前国内用户使用的比较模糊,有时指共识机制,有时指具体的Hash算法,作为区块链算法,哈希算法一直让普通大众感到晦涩难懂,那么,什么是哈希算法?接下来币圈子小编就来给大家通俗的讲解一下哈希算法是什么?希望https://m.jb51.net/blockchain/929884.html
16.图像算法工程师岗位职责要求图像算法工程师是做什么的职位百科|图像算法工程师职位招聘信息 26994| 11 图像算法工程师是指跟踪前沿研究成果,持续优化现有图像识别算法,提升图像识别性能的高级人才。 岗位要求: 中级图像算法工程师 学历要求: 本科 适合专业: 数学与应用数学,软件工程 专业技能要求: C/C++ Matlab https://mbaike.51job.com/zhiwei/73031
17.Capon算法学术百科提供全面的“Capon算法”相关文献(论文)下载,论文摘要免费查询,Capon算法论文全文下载提供PDF格式文件。Capon算法中文、英文词汇释义(解释),“Capon算法”各类研究资料、调研报告等。https://wiki.cnki.com.cn/HotWord/1409906.htm
18.网络流—最大流51CTO博客二、Dinic算法(百科讲解) Dinic算法是网络流最大流的优化算法之一,每一步对原图进行分层,然后用DFS求增广路。Dinic算法最多被分为n个阶段,每个阶段包括建层次网络和寻找增广路两部分。 Dinic算法的思想是分阶段地在层次网络中增广。它与最短增广路算法不同之处是:最短增广路每个阶段执行完一次BFS增广后,要重新启https://blog.51cto.com/u_15888102/5878460
19.物流怎么收费?按公斤/吨/方/趟算法大全「行业百科」按公斤/吨/方/趟算法大全「行业百科」 物流怎么收费?当发货量比较大且货物体积较大的时候,走物流是比较合适的运输方式,不同的物品的收费都是不一样的,轻货是按照立方收货,有的是按照公斤,装载量比较大就是按照吨,接下来就和上海物流公司小编一起了解一下物流怎么收费。https://www.gml.cn/Mobile/MArticles/wlzmsfagjdftsfdqxybk_page1.html
20.一文看懂人工智能里的算法(4个特征+3个算法选择Tips)算法没有高级和低级之分,快速便宜的解决问题才是目的,一味追求复杂的算法(例如:深度学习),相当于“用大炮打蚊子” 有时候有多种算法可以解决同一个问题,用最低的成本和最短的时间解决问题才是目的。根据不同环境选择合适的算法很重要。 百度百科+维基百科 https://easyai.tech/ai-definition/algorithm/
21.进化计算词条广场 人物百科 企业百科 词条卡合作 百度收录 登录/注册 赞(3) | 阅读(152) 进化计算编辑 本词条由“匿名用户” 建档。在计算机科学中,进化计算是受生物进化启发的用于全局优化的一系列算法,以及研究这些算法的人工智能和软计算的子领域。在技术方面,它们是具有元启发式或随机优化特征的基于群体的https://vibaike.com/124823/?ivk_sa=1024320u