字符串7——重复的子字符串

  • 例题
    • 题目链接
    • 题目说明
  • 解题
    • 方法一:枚举
      • 思路与算法
      • 代码
      • 复杂度分析
    • 方法二:字符串匹配
      • 思路与算法
      • 代码
      • 复杂度分析
    • 方法三:KMP 算法
      • 思路与算法
      • 代码
      • 复杂度分析
      • 正确性证明
      • 思考题答案
    • 方法四:优化的 KMP 算法
      • 思路与算法
      • 代码
      • 复杂度分析

例题

题目链接

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

题目说明

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

示例 1:

输入: s = "abab"
输出: true
解释: 可由子串 "ab" 重复两次构成。

示例 2:

输入: s = "aba"
输出: false

示例 3:

输入: s = "abcabcabcabc"
输出: true
解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)

提示:

  • 1≤s.length≤1041 \leq s.length \leq1041≤s.length≤104
  • s由小写英文字母组成s 由小写英文字母组成s由小写英文字母组成

解题

方法一:枚举

思路与算法

如果一个长度为 nnn 的字符串 sss 可以由它的一个长度为 n′n'n′ 的子串 s′s's′ 重复多次构成,那么:

  • nnn 一定是 n′n'n′ 的倍数;

  • s′s's′ 一定是 sss 的前缀;

  • 对于任意的 i∈[n′,n)i \in [n', n)i∈[n′,n),有 s[i]=s[i−n′]s[i] = s[i-n']s[i]=s[i−n′]。

也就是说,sss 中长度为 n′n'n′ 的前缀就是 s′s's′,并且在这之后的每一个位置上的字符 s[i]s[i]s[i],都需要与它之前的第 n′n'n′个字符 s[i−n′]s[i-n']s[i−n′] 相同。

因此,我们可以从小到大枚举 n′n'n′ ,并对字符串 sss 进行遍历,进行上述的判断。注意到一个小优化是,因为子串至少需要重复一次,所以 n′n'n′不会大于 nnn 的一半,我们只需要在 [1,n2][1, \frac{n}{2}][1,2n​] 的范围内枚举 n′n'n′ 即可。

代码

C++

class Solution {public:bool repeatedSubstringPattern(string s) {int n = s.size();for (int i = 1; i * 2 <= n; ++i) {if (n % i == 0) {bool match = true;for (int j = i; j < n; ++j) {if (s[j] != s[j - i]) {match = false;break;}}if (match) {return true;}}}return false;}
};

复杂度分析

  • 时间复杂度:O(n2)O(n^2)O(n2),其中 nnn 是字符串 sss 的长度。枚举 iii 的时间复杂度为 O(n)O(n)O(n),遍历 sss 的时间复杂度为 O(n)O(n)O(n),相乘即为总时间复杂度。

  • 空间复杂度:O(1)O(1)O(1)。

方法二:字符串匹配

思路与算法

我们可以把字符串 sss 写成s′s′⋯s′s′s's' \cdots s's's′s′⋯s′s′

的形式,总计 nn′\frac{n}{n'}n′n​ 个 s′s's′ 。但我们如何在不枚举 n′n'n′ 的情况下,判断 sss 是否能写成上述的形式呢?

如果我们移除字符串 sss 的前 n′n'n′ 个字符(即一个完整的 s′s's′),再将这些字符保持顺序添加到剩余字符串的末尾,那么得到的字符串仍然是 sss。由于 1≤n′<n1 \leq n' < n1≤n′<n,那么如果将两个 sss 连在一起,并移除第一个和最后一个字符,那么得到的字符串一定包含 sss,即 sss 是它的一个子串。

因此我们可以考虑这种方法:我们将两个 sss 连在一起,并移除第一个和最后一个字符。如果 sss 是该字符串的子串,那么 sss 就满足题目要求。

注意到我们证明的是如果 sss 满足题目要求,那么 sss 有这样的性质,而我们使用的方法却是如果 sss 有这样的性质,那么 sss 满足题目要求。因此,只证明了充分性是远远不够的,我们还需要证明必要性。

题解区的很多题解都忽略了这一点,但它是非常重要的。

证明需要使用一些同余运算的小技巧,可以见方法三之后的「正确性证明」部分。这里先假设我们已经完成了证明,这样就可以使用非常简短的代码完成本题。在下面的代码中,我们可以从位置 111 开始查询,并希望查询结果不为位置 nnn,这与移除字符串的第一个和最后一个字符是等价的。

代码

C++

class Solution {public:bool repeatedSubstringPattern(string s) {return (s + s).find(s, 1) != s.size();}
};

python3

class Solution:def repeatedSubstringPattern(self, s: str) -> bool:return (s + s).find(s, 1) != len(s)

复杂度分析

由于我们使用了语言自带的字符串查找函数,因此这里不深入分析其时空复杂度。

方法三:KMP 算法

思路与算法

在方法二中,我们使用了语言自带的字符串查找函数。同样我们也可以自己实现这个函数,例如使用比较经典的 KMP 算法。

读者需要注意以下几点:

  • KMP 算法虽然有着良好的理论时间复杂度上限,但大部分语言自带的字符串查找函数并不是用 KMP 算法实现的。这是因为在实现 API 时,我们需要在平均时间复杂度和最坏时间复杂度二者之间权衡。普通的暴力匹配算法以及优化的 BM 算法拥有比 KMP 算法更为优秀的平均时间复杂度;

  • 学习 KMP 算法时,一定要理解其本质。如果放弃阅读晦涩难懂的材料(即使大部分讲解 KMP 算法的材料都包含大量的图,但图毕竟只能描述特殊而非一般情况)而是直接去阅读代码,是永远无法学会 KMP 算法的。读者甚至无法理解 KMP 算法关键代码中的任意一行。

由于本题就是在一个字符串中查询另一个字符串是否出现,可以直接套用 KMP 算法。因此这里对 KMP 算法本身不再赘述。读者可以自行查阅资料进行学习。这里留了三个思考题,读者可以在学习完毕后尝试回答这三个问题,检验自己的学习成果:

  • 设查询串的的长度为 nnn,模式串的长度为 mmm,我们需要判断模式串是否为查询串的子串。那么使用 KMP 算法处理该问题时的时间复杂度是多少?在分析时间复杂度时使用了哪一种分析方法?

  • 如果有多个查询串,平均长度为 nnn,数量为 kkk,那么总时间复杂度是多少?

  • 在 KMP 算法中,对于模式串,我们需要预处理出一个 fail\textit{fail}fail 数组(有时也称为 next\textit{next}next 数组、π\piπ数组等)。这个数组到底表示了什么?

代码

C++

class Solution {public:bool kmp(const string& query, const string& pattern) {int n = query.size();int m = pattern.size();vector<int> fail(m, -1);for (int i = 1; i < m; ++i) {int j = fail[i - 1];while (j != -1 && pattern[j + 1] != pattern[i]) {j = fail[j];}if (pattern[j + 1] == pattern[i]) {fail[i] = j + 1;}}int match = -1;for (int i = 1; i < n - 1; ++i) {while (match != -1 && pattern[match + 1] != query[i]) {match = fail[match];}if (pattern[match + 1] == query[i]) {++match;if (match == m - 1) {return true;}}}return false;}bool repeatedSubstringPattern(string s) {return kmp(s + s, s);}
};

复杂度分析

  • 时间复杂度:O(n)O(n)O(n),其中 nnn 是字符串 sss 的长度。

  • 空间复杂度:O(n)O(n)O(n)。

正确性证明

一方面,如果长度为 nnn 的字符串 sss 是字符串 t=s+st=s+st=s+s 的子串,并且 sss 在 ttt 中的起始位置不为 000 或 nnn,那么 sss 就满足题目的要求。证明过程如下:

  • 我们设 sss 在 ttt 中的起始位置为 iii,i∈(0,n)i \in (0, n)i∈(0,n)。也就是说,ttt 中从位置 iii 开始的 nnn 个连续的字符,恰好就是字符串 sss。那么我们有:

s[0:n−1]=t[i:n+i−1]s[0:n-1] = t[i:n+i-1]s[0:n−1]=t[i:n+i−1]

由于 ttt 是由两个 sss 拼接而成的,我们可以将 t[i:n+i−1]t[i:n+i-1]t[i:n+i−1] 分成位置 n−1n-1n−1 左侧和右侧两部分:

{s[0:n−i−1]=t[i:n−1]s[n−i:n−1]=t[n:n+i−1]=t[0:i−1]\left \{ \begin{aligned} s[0:n-i-1] &= t[i:n-1] \\ s[n-i:n-1] &= t[n:n+i-1] = t[0:i-1] \end{aligned} \right.{s[0:n−i−1]s[n−i:n−1]​=t[i:n−1]=t[n:n+i−1]=t[0:i−1]​

每一部分都可以对应回 sss:

{s[0:n−i−1]=s[i:n−1]s[n−i:n−1]=s[0:i−1]\left \{ \begin{aligned} s[0:n-i-1] &= s[i:n-1] \\ s[n-i:n-1] &= s[0:i-1] \end{aligned} \right.{s[0:n−i−1]s[n−i:n−1]​=s[i:n−1]=s[0:i−1]​

这说明,sss 是一个「可旋转」的字符串:将 ss 的前 ii 个字符保持顺序,移动到 sss 的末尾,得到的新字符串与 ss 相同。也就是说,在模 nnn 的意义下,

s[j]=s[j+i]s[j] = s[j+i]s[j]=s[j+i]

对于任意的 jjj 恒成立。

「在模 nnn 的意义下」可以理解为,所有的加法运算的结果都需要对 nnn 取模,使得结果保持在 [0,n)[0, n)[0,n) 中,这样加法就自带了「旋转」的效果。

如果我们不断地连写这个等式:

s[j]=s[j+i]=s[j+2i]=s[j+3i]=⋯s[j] = s[j+i] = s[j+2i] = s[j+3i] = \cdotss[j]=s[j+i]=s[j+2i]=s[j+3i]=⋯

那么所有满足 j0=j+k⋅ij_0 = j + k \cdot ij0​=j+k⋅i的位置 j0j_0j0​ 都有 s[j]=s[j0]s[j] = s[j_0]s[j]=s[j0​],jjj 和 j0j_0j0​

在模 iii 的意义下等价。由于我们已经在模 nnn 的意义下讨论这个问题,因此 jjj 和 j0j_0j0​

在模 gcd(n,i)\mathrm{gcd}(n, i)gcd(n,i) 的意义下等价,其中 gcd\mathrm{gcd}gcd 表示最大公约数。也就是说,字符串 ss 中的两个位置如果在模 gcd(n,i)\mathrm{gcd}(n, i)gcd(n,i) 的意义下等价,那么它们对应的字符必然是相同的。

由于 gcd(n,i)\mathrm{gcd}(n, i)gcd(n,i) 一定是 nnn 的约数,那么字符串 sss 一定可以由其长度为 gcd(n,i)\mathrm{gcd}(n, i)gcd(n,i)的前缀重复 ngcd(n,i)\frac{n}{\mathrm{gcd}(n, i)}gcd(n,i)n​次构成。

另一方面,如果 sss 满足题目的要求,那么 sss包含若干个「部分」,t=s+st=s+st=s+s包含两倍数量的「部分」,因此 sss 显然是 ttt 的子串,并且起始位置可以不为 000 或 nnn:我们只需要选择 ttt 中第一个「部分」的起始位置即可。

综上所述,我们证明了:长度为 nnn 的字符串 sss 是字符串 t=s+st=s+st=s+s 的子串,并且 sss 在 ttt 中的起始位置不为 000 或 nnn,当且仅当 sss 满足题目的要求。因此,

思考题答案

设查询串的的长度为 nnn,模式串的长度为 mmm,我们需要判断模式串是否为查询串的子串。那么使用 KMPKMPKMP 算法处理该问题时的时间复杂度是多少?在分析时间复杂度时使用了哪一种分析方法?

时间复杂度为 O(n+m)O(n+m)O(n+m),用到了均摊分析(摊还分析)的方法。

具体地,无论在预处理过程还是查询过程中,虽然匹配失败时,指针会不断地根据 fail\textit{fail}fail 数组向左回退,看似时间复杂度会很高。但考虑匹配成功时,指针会向右移动一个位置,这一部分对应的时间复杂度为 O(n+m)O(n+m)O(n+m)。又因为向左移动的次数不会超过向右移动的次数,因此总时间复杂度仍然为 O(n+m)O(n+m)O(n+m)。

如果有多个查询串,平均长度为 nnn,数量为 kkk,那么总时间复杂度是多少?

时间复杂度为 O(nk+m)O(nk+m)O(nk+m)。模式串只需要预处理一次。
在 KMP 算法中,对于模式串,我们需要预处理出一个 fail\textit{fail}fail 数组(有时也称为 next\textit{next}next 数组、π\piπ数组等)。这个数组到底表示了什么?

fail[i]\textit{fail}[i]fail[i] 等于满足下述要求的 xxx 的最大值:s[0:i]s[0:i]s[0:i] 具有长度为 x+1x+1x+1的完全相同的前缀和后缀。这也是 KMP 算法最重要的一部分。

方法四:优化的 KMP 算法

思路与算法

如果读者能够看懂「正确性证明」和「思考题答案」这两部分,那么一定已经发现了方法三中的 KMP 算法有可以优化的地方。即:

在「正确性证明」部分,如果我们设 iii 为最小的起始位置,那么一定有 gcd(n,i)=i\mathrm{gcd}(n, i) = igcd(n,i)=i,即 nnn 是 iii 的倍数。这说明字符串 sss 是由长度为 iii 的前缀重复 ni\frac{n}{i}in​次构成;

由于 fail[n−1]\textit{fail}[n-1]fail[n−1] 表示 sss 具有长度为 fail[n−1]+1\textit{fail}[n-1]+1fail[n−1]+1 的完全相同的(且最长的)前缀和后缀。那么对于满足题目要求的字符串,一定有 fail[n−1]=n−i−1\textit{fail}[n-1] = n-i-1fail[n−1]=n−i−1,即 i=n−fail[n−1]−1i = n - \textit{fail}[n-1] - 1i=n−fail[n−1]−1;

对于不满足题目要求的字符串,nnn 一定不是 n−fail[n−1]−1n - \textit{fail}[n-1] - 1n−fail[n−1]−1 的倍数。

上述所有的结论都可以很容易地使用反证法证出。

因此,我们在预处理出 fail\textit{fail}fail 数组后,只需要判断 nnn 是否为 n−fail[n−1]−1n - \textit{fail}[n-1] - 1n−fail[n−1]−1的倍数即可。

代码

C++

class Solution {public:bool kmp(const string& pattern) {int n = pattern.size();vector<int> fail(n, -1);for (int i = 1; i < n; ++i) {int j = fail[i - 1];while (j != -1 && pattern[j + 1] != pattern[i]) {j = fail[j];}if (pattern[j + 1] == pattern[i]) {fail[i] = j + 1;}}return fail[n - 1] != -1 && n % (n - fail[n - 1] - 1) == 0;}bool repeatedSubstringPattern(string s) {return kmp(s);}
};

复杂度分析

  • 时间复杂度:O(n)O(n)O(n),其中 nnn 是字符串 sss 的长度。

  • 空间复杂度:O(n)O(n)O(n)。

字符串7——重复的子字符串相关推荐

  1. js实现kmp算法_「leetcode」459.重复的子字符串:KMP算法还能干这个!

    不瞒你说,重复子串问题,KMP很拿手 题目459.重复的子字符串 给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成.给定的字符串只含有小写英文字母,并且长度不超过10000. 示例 1: ...

  2. 力扣459. 重复的子字符串(KMP,JavaScript)

    如果 next[len - 1] != 0,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度). 最长相等前后缀的长度为:next[len - 1] . 数组长度为:le ...

  3. 算法Day8|字符串专题二 剑指 Offer 58 - II. 左旋转字符串,28. 找出字符串中第一个匹配项的下标,459. 重复的子字符串

    剑指 Offer 58 - II. 左旋转字符串 解题思路: 反转区间为前n的子串 反转区间为n到末尾的子串 反转整个字符串 class Solution {public String reverse ...

  4. 【字符串】leet459.重复的子字符串(C/C++/Java/Python/Js)

    leetcode459.重复的子字符串 1 题目 2 思路 3 代码 3.1 C++版本 3.2 C版本 3.3 Java版本 3.4 Python版本 3.5 JavaScript版本 4 总结 K ...

  5. 字符串专题-LeetCode:剑指 Offer 58 - II. 左旋转字符串、LeetCode 459.重复的子字符串、 代码思路和注意点

    文章目录 一.剑指 Offer 58 - II. 左旋转字符串 二.LeetCode 459.重复的子字符串 一.剑指 Offer 58 - II. 左旋转字符串 思路: 预留出n个字符空间s.res ...

  6. 402-字符串(题目:剑指Offer58-II.左旋转字符串、 28. 实现 strStr()、459.重复的子字符串)

    题目:剑指Offer58-II.左旋转字符串 class Solution {public:string reverseLeftWords(string s, int n) {string s1(s. ...

  7. 随想录Day9--28. 实现 strStr() , 459.重复的子字符串

    今天的两道题关键在于学习KMP算法.KMP算法运用场景在于一串字符串里面查找是否含有某个子字符串,如"abcdef"里面就含有"cdf"这么个子字符串.先把题目 ...

  8. 【代码随想录二刷】day9 | 28. 实现 strStr() 459.重复的子字符串

    二刷主要记录理解不一样的题 一刷地址:day9 今日题目:中等 KMP:困难 => 第一时间想到了使用KMP,但是不太会,只有用常规方法完成 实现 strStr():拼接完两个字符串s之后,取其 ...

  9. 代码随想录Day09:28. 实现 strStr()、459.重复的子字符串、字符串总结 、双指针回顾

    目录 Day09:28. 实现 strStr().459.重复的子字符串.字符串总结 .双指针回顾 28. 实现 strStr() (一刷只看了思想) 459.重复的子字符串 (本题一刷跳过了) 字符 ...

最新文章

  1. ubuntu18.04 出现 Command ‘ifconfig‘ not found 问题的解决办法
  2. matlab模拟风场竖桥向时程,索梁结构应急桥抖振响应分析
  3. 关于java的对象数组
  4. word 插入代码_Word教程:最神奇的快捷键:Alt+X,一秒变出各种符号!
  5. dotNET Core 中怎样操作 AD?
  6. 前端学习(2966):上午回顾
  7. Series与DataFrame数据类型操作基础
  8. 二分法的样例 题解
  9. 安卓平台中的动态加载技术分析
  10. 深圳保障性住房【公租房、安居房、人才房】简单说明
  11. vmware 桌面 服务器版,vmware云桌面软件服务器(vmware云桌面搭建教程)
  12. IOS美图秀秀(滤镜和涂鸦)和 添加阴影功能
  13. MyBatis入门回顾
  14. 短信截取 android,谷歌Android增加语音操作功能 可语音发送短信
  15. 图书管理系统 jsp + servlet + mysql (2023)
  16. css中white-space的值pre-wrap
  17. HTML入门习题及答案
  18. Win8.1打开电脑时提示C:\WINDOWS\system32\config\systemprofile\Desktop不可用的解决方法
  19. spark原理之一张图搞定broadcast
  20. 一图看懂centos和ubuntu命令区别

热门文章

  1. 望都墙绘 美丽乡村文化墙墙体彩绘
  2. 数学专业英语 -- 组合分析和数值分析
  3. u盘插上去计算机没有,正常U盘插入电脑没反应?u盘插上没反应的绝对有效解决方法-太平洋电脑网...
  4. 温哥华菜鸟生活攻略(1)
  5. 【九度OJ】查找第K小数
  6. 小米2022校招前端实习一面总结
  7. java编译器:必须对其进行捕获或声明以便抛出
  8. 基于HTML+JavaScript+CSS计算机实验室预约管理系统网页设计 文档+html源码
  9. STM32实战(1):搭建模板工程
  10. codeforces 727E. Games on a CD