9.字符串(3) | 重复的子字符串

        今天这道题(459. 重复的子字符串)是KMP算法的典型应用,在没看题解之前的想法是分2步,先找到最小单位的字符串,再判断其在字符串种出现的次数是否正确。第2步比较容易,但第1步属实想不到解法。看了题解后,发现题解中的2种解法都不是直接来求解的,都是将原问题转化为了其他问题,也确实是在不看题解的情况下想不到的。


        解法一将原本的字符串s复制一份并拼接成新的s2,如果s是符合要求的字符串,那么由两个s拼接而成的s2的中间部分(不包含起始和末尾字符)必然包含s;否则就不包含。因为满足要求的字符串至少可以被分为相同的2段,其前后顺序调换后还会是原本的s。那么在其之后拼接s,原本的s的第2段和其之后s的第1段所合成的字符串与s相等。如s为abab时,拼接而成的ab ab ab ab种就包含ab ab。而当s包含相等子字符串的段数n大于2时,原本s的后(n - 1)段与新拼接s的第1段也会形成s。如s为ab ab ab时,拼接而成的ab ab ab ab ab ab就包含ab ab ab。而当字符串不符合要求时,其不包含最小相等子字符串,进行上面的拼接操作后在数组中间部分也不会得到原数组。所以具体做法为将2个s拼接得到s2,再将s2作为主串,s作为模式串进行KMP匹配,而因为匹配范围时中间部分(否则会s2的前一半,即原s会匹配成功),所以匹配主串s的匹配范围应该是从1(而非0)开始,到s2.size() - 2(而非s2,size() - 1)结束。

class Solution {
public:
    void getNext(vector<int>& next, string pattern) {
        int pre = -1, post = 0;
        next[post] = pre;
        while (post < next.size() - 1) {
            if (pre == -1 || pattern[pre] == pattern[post]) {
                next[++post] = ++pre;
            }
            else {
                pre = next[pre];
            }
        }
    }
    bool repeatedSubstringPattern(string s) {
        vector<int> next(s.size(), 0);
        getNext(next, s);
        string s2 = s + s;
        int i = 1, j = 0;
        while (i < s2.size() - 1) {
            if (j == -1 || s2[i] == s[j]) {
                ++i, ++j;
                if (j == s.size()) {
                    return true;
                }
            }
            else {
                j = next[j];
            }
        }
        return false;
    }
};

        代码中需要注意的是KMP的if判断条件(第7和第21行)中第一个条件是pre == -1(或j == -1),而不是next[pre] == -1(或next[j] == -1)。

        如果熟悉了KMP,可以用string.find()函数简化为下面的代码:

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

 其中需要熟悉string.erase()的使用,要以指针作为第一个参数;以及string.find()在未找到时返回string::npos。


        解法二利用KMP中next数组的物理意义,得到最小相等子串及其长度。next数组与字符串的最大相等前后缀长度密切相关。而前后缀相等的情况举例用字符串s为"abababab"来说,其最小相等前后缀的前缀为s[0]~s[5],后缀为s[2]~s[7],s[0]~s[5]与s[2]~s[7]各位对应相等。前缀s[0]~s[5]的前2位s[0]~s[1]与后缀前2位s[2]~s[3]相等,而第3~4位作为前缀的第3~4位,又与作为后缀第3~4位的s[4]~s[5]相等······以此类推直到字符串结束,都是前缀的非公共(与后缀不重叠)部分s[0]~s[1]的拷贝,所以前缀的非公共(与后缀不重叠)部分即为最小相等子串

        以上方法可以得到最小相等子串,但对于任意字符串s都适用,具体哪些情况字符串s才是符合条件的字符串?这个问题可以转化为“具体哪些情况字符串s不是符合条件的字符串”,有以下两种情况:

  1. s的最小相等子串为空,即其长度为0时。这种情况说明s的末尾字符无法匹配,故不符合要求;
  2. s的最小相等子串长度不到s的一半,即s的最大相等前后缀没有公共部分,且存在间隔。这种情况说明最大相等前后缀的间隔部分无法匹配,如字符串"a b c  a b";
  3. s的长度不能被其最小相等子串长度整除的。这种情况说明最后一段最小相等子串不完整,如字符串"a b a b a"。

而其中的第2项在实际判断时可以适用与第3条相同的判断方法,因为最大相等前后缀存在间隔,也说明计算得到的“最小相等子串”(实际不是)长度大于s的一半,所以无法整除。

        我使用的next数组的第i位,表示s的0至i - 1位所组成字符串的最大相等前后缀长度,所以需计算得到next[s.size()],而非像普通的KMP一样计算到next[s.size() - 1]为止。

class Solution {
public:
    void getNext(vector<int>& next, string pattern) {
        int pre = -1, post = 0;
        next[post] = pre;
        while (post < next.size() - 1) {
            if (pre == -1 || pattern[pre] == pattern[post]) {
                next[++post] = ++pre;
            }
            else {
                pre = next[pre];
            }
        }
    }
    bool repeatedSubstringPattern(string s) {
        vector<int> next(s.size() + 1, 0);
        getNext(next, s);
        if (next[(int)next.size() - 1] == 0) {
            return false;
        }
        int x = s.size() - next[(int)next.size() - 1];
        if (s.size() % x == 0) {
            return true;
        }
        else {
            return false;
        }
    }
};

        代码实现中要注意将字符串的size()或length()作为下标时,要转换为int避免错误。

        二刷:没想到解法,感觉解法一更直观简便,另外复习了string.erase()和string.substr()的用法。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值