力扣459. 重复的子字符串

题目来源(力扣):

https://leetcode.cn/problems/repeated-substring-pattern/description/

题目描述:

给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。

基本思路:

先说直接的方法:
如果串长度为len,则答案子串的长度一定为len的因数(但不包括len本身)
例如s长度为36
则可能的子串长度为:
1 2 18 3 12 4 9 6
以这些长度开头的子串tmp拼凑出c,如果c==s,则说明符合题意返回true
如果上述长度的子串都无法组成s则返回0

1、代码实现:

class Solution
{
public:
    bool repeatedSubstringPattern(string s)
    {
        int len = s.size();
        for (int k = 1; k < len; k++)  //看似是O(n),实际上只有当k为len的倍数时才执行算法,实际上小于2*sqrt(n)
        {
            if (len % k)
                continue;
            int a = k, b = len / k; // a代表子串长度,b代表拼凑次数
            string tmp = "", c = "";
            for (int i = 0; i < a; i++)  //将子串提取出来
                tmp += s[i];
            for (int j = 0; j < b; j++)  //构造临时串c
                c += tmp;
            // cout<<tmp<<" "<<c<<endl;
            if (c == s)
                return 1;
        }
        return 0;
    }
};

时间复杂度

时间复杂度O(n^2)

首先符合条件的子串数量少于2*sqrt(n),

将子串提取出来的时间复杂度和构造临时串c的时间复杂度相同,都为n*sqrt(n)

因此最坏情况下复杂度为O(2*sqrt(n)*n*sqrt(n)) =>O(n^2)

2、一点优化

方法1虽然能够得到答案,但是实际上在构造临时串c的过程中,可以发现已经无法构成串s了(某些字符不同),理论上应该无需继续构造c而尝试下一长度,
但是之前的写法必须将c构造完毕后才能和s进行比较,所以效率不算太高

例如s="abcabcabcabc"
当tmp="a"时,c="aaaaaaaaaaaa"
当c在构造到第二个字符,即c="aa"时,就可以发现2个串不同了(s[1]='b',c[1]='a'),因此可以边构造串c边进行s的比对,如果不同就提前退出,优化后如下:

class Solution
{
public:
    bool repeatedSubstringPattern(string s)
    {
        int len = s.size();
        int sq = sqrt(len);
        for (int k = 1; k < len; k++)
        {
            if (len % k)
                continue;
            int a = k, b = len / k; // a代表子串长度,b代表拼凑次数
            bool ok = 1;            // 判断s与c是否相同 0代表不同 1代表默认相同
            string tmp = "", c = "";
            for (int i = 0; i < a; i++)
                tmp += s[i];
            for (int j = 0; j < b; j++)
            { // 在构造过程中就进行比较,边构造串c边进行s的比对
                c += tmp;
                int left = a * j, right = a * j + a; // 当前c新加入的区间,也是需要进行比较的区间
                for (int i = left; i < right; i++)
                {
                    if (c[i] != s[i])
                    {
                        ok = 0;
                        break;
                    }
                }
                if (!ok)
                    break; // 如果中途就不同了,那么不需要再继续拼凑c了
            }
            // cout<<tmp<<" "<<c<<endl;
            if (ok)
                return 1; // 说明c在构造过程中(包括完成时)一直都为s的子串,符合题意
        }
        return 0;
    }
};

时间复杂度好于未优化之前的

3、另起炉灶

上述的方法直接基于题意,相对而言直观易懂
接下来的方法更加接近于数学思维或者说算法逆向思维
刚才的方法是找出可能符合条件的子串,判断能够构成s
而下面的方法是,根据s自身的特点,来判断是否有符合条件的子串

这个根据s自身的特点,以及等下会用到的最长公共前后缀其实就是KMP算法的核心思想,具体可见 https://www.cnblogs.com/hb-computer/articles/18499786

按照《代码随想录》的原话来说:“数组长度减去最长相等前后缀的长度相当于第一个重复子字符串的长度,也就是一个重复周期的长度,如果这个重复周期可以被整除,则说明整个数组可以就是这个周期的循环。”
说白了就是

asdfasdfasdf
000012345678
|___________|     整个串s
|___|             最左边的串a
    |________|    最长公共前后缀b
如果s的长度能被a整除,则说明串s是由若干个a组成的

而求解最长公共前后缀的方式在KMP算法中有详尽的阐述 https://www.cnblogs.com/hb-computer/articles/18499786
所以这道题目就转化为了
1、求next数组
2、判断是否整除

代码如下

3、代码

class Solution
{
public:
    int next[10003] = {0};
    void getnext(string s)  //构造next数组,存储最长公共前后缀长度
    {
        int j = 0; // j为前缀末尾
        for (int i = 1; i < s.size(); i++)
        { // i为后缀末尾
            while (j > 0 && s[i] != s[j])
                j = next[j - 1];
            if (s[i] == s[j])
            {
                j++;
                next[i] = j;
            }
        }
    }
    bool repeatedSubstringPattern(string s)
    {
        getnext(s);
        int len = s.size();
        int a = len - next[len - 1];
        if (a > 0 && next[len - 1] > 0 && s.size() % a == 0)  //判断是否整除
            return 1;
        return 0;
    }
};

时间复杂度O(n)

补充(重要)

接下来将简单地证明为什么方法3是正确且可行的

如果这个串是可以由子串重复构成的,则
|_______________|     整个串s
|___|___|___|___|     可以分割为若干相等子串,从左往右依次设为a1、a2、a3...

不妨碍假设串s长度为n,串a长度为m

1.则在构造next数组的过程中,
|___|___|
0 0 .. | 1 2 .. m |
这说明a1[0]=a2[0],a1[1]=a2[1],…a1[m-1]=a2[m-1]
或者说s[0~m-1]=s[m~2m-1]
简记为 a1=a2

2.同理,继续构造next数组的过程中
|___|___|___|
0 0 .. 0 | 1 2 .. m | m+1 m+2 .. 2m|
这说明s[0~m-1]=s[0~2m-1]
简记为 a1+a2 = a2+a3

由1.2.可知,a1=a2=a3 
即,如果s存在符合提交的子串,则s满足:s的最长公共前后缀,是由最前面的a1若干复制后构成的
即最长公共前后缀的长度可以被a1的长度整除,就符合题意

而s由a1和最长公共前后缀一起构成,
所以可以进一步理解为:s的长度如果可以被a1的长度整除,就符合题意
而a1的长度为 **s.size() - next[s.size()-1]**,a1长度不为0,最长公共前后缀也不为0
因此在代码中呈现为:
        if (a > 0 && next[len - 1] > 0 && s.size() % a == 0)  //a1长度不为0,最长公共前后缀长度也不为0,s的长度如果可以被a1的长度整除
            return 1;

posted @ 2024-10-26 09:53  HB_Computer  阅读(77)  评论(0)    收藏  举报