关于周期字符串问题的心得总结——KMP、双倍字符串

以下的一些心得体会参考或部分引用了如下资料:

       “春水煎茶”个人博客: https://writings.sh/post/algorithm-repeated-string-pattern

        “代码随想录”文章及视频资料:代码随想录 (programmercarl.com)

        leetcode题解:ACM 选手图解 LeetCode 重复的子字符串 | 编程文青李狗蛋

以下是我比较推荐的一些KMP算法入门题:

        leetcode28题:力扣 LeetCode

        leetcode459题:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

        洛谷P3375:P3375 【模板】KMP - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

        洛谷P4391:P4391 [BOI2009] Radio Transmission 无线传输 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

在洛谷题单上还有其他相关KMP例题。

对上面资料进行一定总结归纳可知,解决周期字符串问题的主要方法有两种:

1.移动匹配/双倍字符串

2.KMP方法

下面就直入正题地讨论这两种方法.

 一.移动匹配/双倍字符串

比起移动匹配双倍字符串这个名字或许更加形象。

假设有一个字符串s,我们需要判断它是否有多个重复子串构成,只需要设字符串ss=s+s,也就是用两个s串首尾拼接成一个大字符串ss,如果它由多个重复字串构成,那么ss一定也可以由多个相同地重复字串构成(当然了,ss一定是由s这个重复子串构成的)。

上面提到了,ss一定由s这个重复字串构成,为了防止这样的情况发生,我们去掉ss的第一个字符和最后一个字符,如果ss中还能由m个s`构成一个除了首尾s之外的额外的s,不就是说明这个s从头到尾都是重复的子串s`组成的吗。

同样的,想象一下,s串由m个子串s`构成,两个s串由2m个子串s`构成,那么ss中间部分一定会有m个连续子串s`构成一个s,所以如果s由重复子串构成,就一定可以在ss里面找到m个连续的s`组成一个s,也就是ss里面一定至少包含一个不位于首尾的s。

这样就证明了其充分性和必要性。

具体的推导可以参考“春水煎茶”大佬的博客,当然也可以看卡哥的代码随想录视频。

所以我们可以得到一个结论:如果字符串在其掐头去尾的双倍字符串中,它就是周期串 。

接下来就可以尝试解决力扣上的459号题了。

下面给出我个人实现的代码:

class Solution {
public:
    bool repeatedSubstringPattern(string s) {
        string t=s+s;
        t.erase(t.begin());
        t.erase(t.end()-1);
        if(t.find(s)!=string::npos)return true;
        return false;
    }
};

时间16ms,空间11.9mb,还行。

二.KMP方法——最大公共前后缀的推论。

这才是这份总结的重点。

我们以leetcode459题为基础,进一步解决洛谷的P4391。弄懂了这两题,也就差不多对前缀表和next数组有了比较系统深入的了解了。

以下总结都是基于已经了解了最基本的kmp算法的前提。

首先给出leetcode459题的可运行代码:

class Solution {
public:
    void getnext(const string& s,int* next){
        int j=-1;
        next[0]=-1;
        for(int i=1;i<s.size();i++){
            while(j>=0&&s[i]!=s[j+1])j=next[j];
            if(s[i]==s[j+1])j++;
            next[i]=j;
        }
    }

    bool repeatedSubstringPattern(string s) {
        if(s.size()==1)return false;
        int next[s.size()];
        getnext(s,next);
        int border=0;
        for(auto c:next){
            if(c+1>border)border=c+1;
        }    
        if(next[s.size()-1]!=-1&&s.size() % (s.size() - (next[s.size() - 1] + 1))==0)return true;
        else return false;
    }
};

1.奇怪的结论

首先定义一个函数来生成一个模式串的next数组,这是比较显然的,最晦涩难懂的莫过于最后的那个奇怪的公式:

s.size() % (s.size() - (next[s.size() - 1] + 1)) ==0

事实上,这是一个结论:

命题As 是周期串” \Leftrightarrow命题Blen(s) 是 len(q)-len(c) 的倍数”

这个结论的严谨证明可以参考春水煎茶大佬的博客,里面由图像和文字的描述。这里我简单说明一下我自己的一些理解。

2.next数组是怎么回事

首先我们要理解next数组到底是怎么一回事:

这里先分享一篇我觉得不错讲KMP的博客The Knuth-Morris-Pratt Algorithm in my own words - jBoxer (jakeboxer.com)

首先next数组的出现是一个从问题出发的解决方案。在KMP算法中,为了解决当模式串高度重复时,暴力算法逐个匹配效率的下的问题,我们需要模式串指针的正确回溯,也就是需要模式串指针回溯到他之前重复过一次的位置。也就是在已经遍历到的位置之前,找到一个一模一样的串,从那个串的最后重新匹配。

按上面给出的代码中getnext函数,可以知道得到next数组的伪代码形式写成:

while(当前位置和前一个位置的后缀的结尾接不上去的时候 且 j>= 0):

去前一个位置的后缀相对应一模一样的前缀里面找,那个一模一样的前缀也能划分成一个前缀和后缀,在这个前缀的前缀里面找。

如果前缀的前缀的下一个字符和当前位置的字符一样,说明(前缀的前缀+下一个字符)和(后缀的后缀+当前位置的字符)一模一样,找到所谓的j了,也就是当前位置的最大公共字符串。

如果找不到就去前缀的前缀的前缀里面找一直细分下去,直到前缀不能划分成有前后缀的字符串了(即j=-1)

我们来看一段KMP算法的模板:

        这是生成next数组

        int j=-1;
        next[0]=j;
        for(int i=1;i<s.size();i++){
            while(j>=0&&s[j+1]!=s[i])j=next[j];
            if(s[j+1]==s[i])j++;
            next[i]=j;
        }

        这是使用next数组匹配

        int j=-1;
        for(int i=0;i<haystack.size();i++){
            while(j>=0&&haystack[i]!=needle[j+1])j=next[j];
            if(haystack[i]==needle[j+1])j++;
            if(j==needle.size()-1)return i-needle.size()+1;
        }
        return -1;

两者是不是高度相似?事实上就是如此,KMP算法就是先遍历一遍模式串,通过寻找相同的前后缀在next数组里面标记重复的位置来生成一个next数组;再遍历一遍文本串,通过next数组来查找相同的子串。

所以next数组就是一组对重复前缀位置(也就是回溯位置)的记录,只是考虑到后续使用而进行了一定的变形,比如-1,右移之类的操作。

也即:重复模式串一定有内容不全为-1的next数组。

3.用next数组的思想来理解周期字符串

那么不难发现,当一个字符串是周期字符串的时候,它一定会有内容不全为-1的next数组。但是我们此时并不需要用它来匹配,此时我们去掉它的第一个重复子串S.1得到后一段,再去掉它的最后一个重复子串S.n,得到开头一段,就会长成下图的两个q的样子:

(图片来源)https://writings.sh/assets/images/posts/algorithm-repeated-string-pattern/repeated-string-pattern-kmp-4.jpeg​​​​​​​y​​​​​​​z




两个前后缀一定是错开,并且中间对应相同的,那么他们各自分别剩出来的部分就是我们取出来的重复子串了。

并且由于我们取出来的子串它本身是没有更小的重复子串(公共前后缀)的,也就是说这个子串的next数组里面的所有元素都是-1,所以他没有上面所说的前缀的前缀里面找到的公共前后缀。因此它一定同时也是最小重复子串。这样就证明了那个长相诡异的式子。

严谨的充分必要性推导可以参考:​​​​​​​周期字符串问题(两种方法) | 春水煎茶 - 王超的个人博客 (writings.sh)

所以所谓的周期字符串的最小重复子串,也就是把周期串作为模式串寻找它的next数组里全是-1的部分,全是-1则说明子串本身不重复,那么自然这个周期串就是由它组成的了。

  • 29
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
字符串存储在rodata区域是指程序中的字符串常量被存储在只读数据段(rodata)中。当我们在程序中使用字符串常量时,C语言编译器会将其放在只读数据段中,以保证这些字符串的内容不会被修改。 只读数据段(rodata)是程序的一部分,它位于内存的静态数据段。这个区域专门用来存储只读的常量数据,例如字符串常量、全局常量等。这些数据在程序的整个生命周期内都保持不变,因此被放置在只读数据段。 使用只读数据段存储字符串常量有以下几点好处: 1. 节省内存空间:使用只读数据段存储字符串常量可以避免重复存储相同的字符串,节省了内存空间。所有使用相同字符串常量的地方都可以共享同一块只读数据段中的内存。 2. 提高安全性:只读数据段被标记为只读,这意味着程序无法修改其中的内容。这提高了程序的安全性,防止了不小心或恶意修改字符串常量导致的错误。 3. 加速访问:将字符串常量存储在只读数据段中,可以提高对字符串常量的访问速度。因为只读数据段通常存储在可执行文件的静态数据区中,而这个区域的访问速度相对较快。 当我们在程序中使用字符串常量时,编译器会自动将其分配到只读数据段,并在程序运行时可以直接引用它们。需要注意的是,字符串常量保存在只读数据段中,因此任何试图修改这些字符串常量的操作都是非法的,会导致程序运行出错。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tomokochandesu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值