Repeated Substring Pattern【重复的子字符串【KMP】】

本文介绍两种判断一个非空字符串是否能由其子串重复拼接而成的方法:一种使用KMP算法,通过构建部分匹配表来高效解决问题;另一种通过检查字符串长度是否为潜在子串长度的倍数,并验证重复子串的有效性。

PROBLEM:

Given a non-empty string check if it can be constructed by taking a substring of it and appending multiple copies of the substring together. You may assume the given string consists of lowercase English letters only and its length will not exceed 10000.

Example 1:

Input: "abab"

Output: True

Explanation: It's the substring "ab" twice.

Example 2:

Input: "aba"

Output: False

Example 3:

Input: "abcabcabcabc"

Output: True

Explanation: It's the substring "abc" four times. (And the substring "abcabc" twice.)

SOLVE:

bool repeatedSubstringPattern(string str) {
    int i = 1, j = 0, n = str.size();  
    vector<int> dp(n+1,0);  
    dp[0]=-1;
    while( i < str.size() ){  
        if( j==-1||str[i] == str[j] )
             dp[++i]=++j;  
        else
             j = dp[j];  
    }  
    return dp[n]&&dp[n]%(n-dp[n])==0;
}

分析(KMP算法):这种方法用到了KMP算法,其中dp[i]中的i从0开始,dp[i]的值为“当前字符str[i]与模式字符str[j]不相同时,j跳至的下标”,其实也表示“当前字符之前,前缀与后缀相同的最大长度”,如str=“abcdabcd”,则str[5]='b','b'之前的子字符串“abcda”前缀a与后缀a相同,相同的最长长度为1,所以dp[5]=1。str[1]之前只有一个字符,所以dp[1]=0,由于当j==0&&str[i] != str[j]时,j=dp[j]=-1,下一次循环dp[i+1]=j+1=0,再次循环进行str[0]和str[i+1]的比较...

public boolean repeatedSubstringPattern(String str) {
	int l = str.length();
	for(int i=l/2;i>=1;i--) {
		if(l%i==0) {
			int m = l/i;
			String subS = str.substring(0,i);
			StringBuilder sb = new StringBuilder();
			for(int j=0;j<m;j++) {
				sb.append(subS);
			}
			if(sb.toString().equals(str)) return true;
		}
	}
	return false;
}
分析:此方法主要利用“字符串重复,则输入字符串长度必然为模式字符串长度的整数倍”的性质,比较好理解。
<think>嗯,我现在得解决这个问题:给定一个非空的字符串s,检查它是否可以通过由一个子串重复多次构成。也就是说,要判断这个字符串是不是某个子串重复多次组成的。比如像"abab"的话,就是由"ab"重复两次组成的,所以返回true。而如果是"aba",则没有这样的子串,返回false。 那我得想想怎么用代码来判断这种情况。首先,我得找出可能的子串长度。假设原字符串的长度是n,那么可能的子串长度k必须是n的一个因数,也就是说k必须是n的约数,并且k不能等于n本身。比如n=4的话,可能的k是1、2。因为子串至少重复两次,所以k最多是n/2。 那我的思路可能是这样的:遍历所有可能的子串长度k,也就是从1到n/2的所有能整除n的数。对于每个k,检查原字符串是否由这个子串重复组成。例如,当k=2时,子串是前两个字符,然后后面的每两个字符是否都等于这个子串。 那如何实现呢?首先,我需要循环k的可能值。然后,对于每个k,检查原字符串是否能被分割成多个这样的子串。比如,假设总长度是n,那么子串的重复次数应该是n/k次。所以,对于每个i,从k到n-1的位置,比较s[i]是否等于s[i%k]。如果所有位置都满足,那么返回true。否则继续下一个k。 或者,有没有更高效的方法?比如,将字符串s与自身拼接,然后去掉首尾字符,然后判断原字符串是否是拼接后的子串。比如,假设s由子串t重复构成,那么s+s的话,中间的部分应该包含s本身。比如s=abab,s+s=abababab,去掉第一个和最后一个字符得到bababa,里面是否包含abab?可能需要再仔细想一下这个方法的正确性。 比如,假设s由t重复k次组成,那么s+s应该是t重复2k次。去掉第一个字符后的字符串长度是2n-2。那么如果s存在于这个新字符串中,那么说明s可以由子串重复构成。比如,当s=abab,那么s+s=abababab。去掉首尾字符得到babaaba,其实可能中间是ababab。或者可能我记错了这个方法的正确性条件。这个方法可能的时间复杂度是O(n),因为拼接后的字符串长度为2n,所以查找子串的话可以用KMP算法,这样总的时间复杂度是O(n)。 那这样的话,在代码中,可以通过构造s + s,然后去掉首尾字符得到新的字符串,再检查s是否是这个新字符串的子串。如果是的话,就返回true,否则false。那这个方法是否正确? 比如,当s=abcabc,那么s+s=abcabcabcabc。去掉首字符后的字符串是bcabcabcabc,去掉最后一个字符的话是bcabcabcab。这个时候,原字符串abcabc是否存在于其中?中间的某个位置可能会有。比如,原字符串出现在第1位开始的位置,即bcabcab... 这里可能需要具体例子验证。 或者,可能应该将s+s去掉第一个和最后一个字符,得到新的字符串,然后判断是否包含s。比如,原方法是这样的:假设s由t重复多次构成,那么s+s将包含至少两次重复的t,所以去掉首尾后的字符串里肯定存在s。比如,s=abab,t=ab,s+s=abababab。去掉首尾是bababa。那原s=abab是否存在于其中?可能不存在。这时候这个方法似乎有问题。 这说明我的之前的思路可能有错误。那这时候应该重新考虑这个方法是否正确。 或者可能我的例子举错了。比如s=abab,s+s是abababab。去掉第一个字符是bababab,去掉最后一个字符是abababa。那么中间的字符串是否包含abab?比如,在位置1开始的子串是baba,那显然不包含。那这说明原方法对于这个例子是否成立? 这时候可能我的思路有问题。或者可能这个方法的正确性需要更仔细的数学证明。 或者,有没有可能正确的条件是,将s+s之后,去掉第一个字符和最后一个字符,得到的字符串是否包含s。例如,对于s=abab,s+s是abababab。去掉第一个字符是bababab,去掉最后一个字符得到abababa。那中间的字符串是ababab吗?或者我可能搞错了具体步骤。 或者,可能正确的做法是:将两个s拼接成s+s,然后去掉第一个字符和最后一个字符,得到的新字符串中如果包含s,那么返回true。比如,假设s由t重复k次组成,k>=2。那么s的长度n=k*m,其中m是t的长度。那么s+s的长度是2n。当我们将s+s拼接后,中间的字符串会是t重复2k次。这时候,如果我们去掉第一个字符和最后一个字符,得到的新字符串是s+s[1:-1],也就是长度是2n-2。这时候,原s是否是这个子字符串的一部分? 比如,当s由t重复k次组成,那么s+s就是t重复2k次。那么,去掉第一个字符后的字符串是 t[1:] + t重复2k-1次 + t[0:-1]。或者可能这样分析比较困难。或者可以举例子: 例子1:s=abab。t=ab,k=2。s+s=abababab。去掉首尾字符得到bababa。现在检查原s是否存在于这个字符串中。原s是abab,bababa是否包含abab?看bababa的结构,是b a b a b a,所以其中子串abab是否存在?比如位置1开始是abab吗?从索引1开始是a b a b,也就是abab,是的。所以此时返回true。 那这样的话,原方法对于这个例子是正确的。那可能我之前分析的例子有误。那这个时候,原方法是否正确? 再来看另一个例子:s=aba。假设它不是由子串重复构成的。s+s=abaaba。去掉首尾字符得到baab。检查s=aba是否存在于baab中?显然不存在。所以此时返回false,正确。 那对于s=abcabc。s由abc重复两次组成。s+s是abcabcabcabc。去掉首尾字符得到 bcabcabcab。检查是否存在abcabc?是的,比如从索引0开始的bcabcabca... 可能不是,或者是否存在?或者原s是否出现在这个字符串中? 例如,去掉首字符后的字符串是 bcabcabcabc(假设原s是abcabc,长度6。s+s是12位,去掉首尾后的长度是10。所以新字符串是 bcabcabcab。那原s是abcabc,是否在其中?可能在第2位开始,即 bcabcabcab中的第2位是 c,所以可能不是。或者可能我计算错了? 或者,正确的例子可能需要更仔细的计算。例如,s=abcabc,s+s是abcabcabcabc。去掉第一个字符和最后一个字符后的字符串是 bcabcabcab。原s的长度是6,检查这个字符串是否包含abcabc。该字符串是 bcabcabcab,长度是10。其中是否有连续的6个字符等于abcabc?比如从索引0开始是bca... 不是。从索引1开始是 cab... 不是。索引2开始是 abcabc,是的。因为 bcabcabcab 中的索引2到7是 abcabc吗?原字符串是 bc a b c a b c a b。索引2是a,后面依次是b,c,a,b,c。所以从索引2到索引7是 abcabc?是的。所以此时返回true。这说明方法正确。 那这说明,当s由子串重复组成时,将s+s去掉首尾后的字符串确实包含s。反之,如果s不是由子串重复构成的话,则不会包含。那这个方法的正确性是否成立? 那现在需要证明这个方法的正确性。假设s可以由子串t重复k次组成,k>=2。那么s = t*t*k。则s+s = t*2k。当我们将s+s去掉第一个字符和最后一个字符后,得到字符串中间的部分。此时,s必然是其中的子串。因为去掉首尾后的字符串仍然包含连续的t*(2k-1)次?或者可能,中间的s会出现在其中? 反过来,如果s+s去掉首尾后的字符串包含s,则s可以由某个子串重复多次构成。这需要双向证明。 假设s由t重复k次构成,那么s+s = t重复2k次。去掉首尾后的字符串是 t[1:] + t重复(2k-2)次 + t[:-1]。这时,中间的t重复部分是否包含至少k次t?比如,假设k=2,那么s+s是 tttt。去掉首尾后的字符串是 t[1:]ttt[:-1]。假设t的长度是m,那么原s的长度是2m。去掉首尾后的字符串长度是4m-2。其中,中间的字符串是否包含原s(长度为2m)? 例如,t=ab,s=abab。s+s是abababab。去掉首尾后的字符串是bababab(长度是7)。原s的长度是4,是否出现在其中?是的,在位置1开始的abab。这说明结论成立。 因此,这个方法的正确性成立。所以,我们可以用这个方法来判断。 所以,解决方案可以是:将字符串s拼接两次,得到s+s,然后去掉首尾字符,然后检查原s是否是新的字符串的子串。如果是,返回true,否则false。 那这样的代码如何实现呢?比如,在C++中,如何高效实现? 在C++中,要判断一个字符串是否是另一个的子串,可以用string的find函数。如果s在s+s.substr(1, 2*s.size()-2)中存在的话,就返回true。 具体来说,构造一个新的字符串temp = s + s。然后,temp = temp.substr(1, temp.size()-2)。然后判断temp.find(s) != string::npos。 比如,对于s=abab,temp是s+s即abababab。substr(1, 6)得到bababa。然后检查其中是否有abab。答案是正确的,因为bababa中的第1个位置开始是abab? 或者,原temp.substr的参数是否正确?因为原字符串的长度是2n,去掉首尾后的长度是2n-2。所以,substr的参数应该是起始位置1,长度是2n-2。比如,temp.substr(1, 2n-2)。例如,当s长度是4,2n是8,substr(1,6)得到从索引1开始的6个字符,即bababa。其中是否包含abab?是的,从索引1开始的子字符串是abab吗?索引1是b,索引2是 a,索引3是 b,索引4是 a,索引5是 b,索引6是 a。所以,可能我之前的例子分析有误。此时,在bababa中是否存在abab? 例如,字符串是b a b a b a。长度是6。原s是abab,长度4。检查是否存在连续的4个字符等于abab。可能的子串: 索引0: baba → 不行。 索引1: abab → 是的。所以,是的,此时返回true。 所以,这种方法是正确的。 那在C++中,代码可以这样写: return (s + s).substr(1, 2*s.size() - 2).find(s) != string::npos; 这样就完成了判断。这个解法的时间复杂度是O(n),因为字符串拼接和子字符串操作的时间复杂度是线性的,而find函数的实现如果是KMP的话,时间复杂度是O(n)。所以总的时间复杂度是O(n)。 那这个方法是否可行呢?应该可以。那原来的题目中的代码需要实现这个逻辑。所以,函数repeatedSubstringPattern的实现可以这样写: class Solution { public: bool repeatedSubstringPattern(string s) { return (s + s).substr(1, 2*s.size() - 2).find(s) != string::npos; } }; 那这个实现是否正确? 测试几个例子: 例子1:s="abab"。s+s是abababab,substr(1,6)是bababa。其中包含abab吗?是的,返回true。 例子2:s="aba"。s+s是abaaba,substr(1,4)是baab。查找s即"aba"是否在其中?没有,返回false。 例子3:s="abcabcabc"。s+s是abcabcabcabcabcabc。substr(1, 18-2=16)的长度?原s长度是9,所以substr(1, 2*9-2=16)。所以得到的字符串是bcabcabcabcabcab。查找s是否存在?是的,比如在某个位置。例如,原s是abcabcabc,而拼接后的字符串中的某个部分可能包含这个。所以返回true。 那这说明方法是可行的。 另一种解法是枚举可能的子串长度k,然后检查每个k是否符合条件。例如,对于每个可能的k(即n的因数,且k <=n/2),然后将s分成n/k段,每段是否都等于第一个k长度的子串。 这种解法的时间复杂度是O(n*sqrt(n)),因为因数的数量最多是O(sqrt(n)),每个因数需要O(n)的时间检查。这在n较大时可能不如前一种方法高效,但对于大多数情况应该也可以通过。 例如,代码可以这样写: class Solution { public: bool repeatedSubstringPattern(string s) { int n = s.size(); for (int k = 1; k <= n/2; ++k) { if (n % k != 0) continue; bool valid = true; for (int i = k; i < n; ++i) { if (s[i] != s[i % k]) { valid = false; break; } } if (valid) return true; } return false; } }; 比如,对于k=2的情况,检查每个i是否等于i%k的位置的字符。例如,s=abab,当k=2时,i=2的位置是a,i%k=0,即s[0]=a,相等。i=3是b,i%k=1,s[1]=b。所以全部相等,返回true。 这个解法的时间复杂度如何呢?假设n是质数的话,那么因数只有1,此时循环次数是n/2次,但其实当k=1的时候,n%k=0只有当n是1的时候成立,所以可能需要调整循环条件。或者代码中的循环k的条件是k <=n/2,并且n%k==0。所以,对于每个k,只有当k是n的因数时才会进行后续的检查。例如,当n=6时,可能的k是1、2、3。但3是否超过n/2?n/2=3,所以k可以等于3。例如,当n=4时,k=2是允许的。 那这种解法是否正确? 举个例子,当n=6,s=abcabc。k=3的情况。n%k=0。检查每个i>=3的位置是否等于i%3的字符。i=3时,s[3]是a,i%3=0 → s[0]是a,相等。i=4是b,i%3=1 → s[1]=b。i=5是c → s[2]=c。所以返回true。 是的,正确。所以这个解法是正确的。 那么,两种解法各有优缺点。第一种方法的时间复杂度较低,但需要了解这个巧妙的数学方法。第二种方法更直观,但时间复杂度可能略高。 根据题目要求,两种方法都是可行的。但第一种方法更高效,代码也更简洁。 所以,最终的解决方案可以采用第一种方法。</think>要判断一个非空字符串 `s` 是否可以由某个子串重复多次构成,可以通过以下两种方法实现: ### 方法一:字符串拼接法(高效) **思路** 将字符串 `s` 拼接成 `s + s`,然后去掉首尾字符。若原字符串 `s` 是拼接后字符串的子串,则说明 `s` 可以由某个子串重复多次构成。 **正确性证明** 假设 `s` 由子串 `t` 重复 `k` 次组成(`k >= 2`),则 `s + s` 包含 `2k` 个 `t`。去掉首尾字符后,剩余的字符串中至少包含 `2k-2` 个 `t`,因此原字符串 `s`(长度为 `k*|t|`)必然存在其中。 **代码实现** ```cpp class Solution { public: bool repeatedSubstringPattern(string s) { return (s + s).substr(1, 2*s.size() - 2).find(s) != string::npos; } }; ``` ### 方法二:枚举因数法(直观) **思路** 遍历所有可能的子串长度 `k`(需为 `s` 长度的因数)。对于每个 `k`,检查字符串是否由前 `k` 个字符重复构成。 **步骤** 1. 枚举所有可能的子串长度 `k`(`k` 需满足 `n % k == 0` 且 `k <= n/2`)。 2. 对每个 `k`,验证所有字符是否满足 `s[i] == s[i % k]`。 **代码实现** ```cpp class Solution { public: bool repeatedSubstringPattern(string s) { int n = s.size(); for (int k = 1; k <= n / 2; ++k) { if (n % k != 0) continue; bool valid = true; for (int i = k; i < n; ++i) { if (s[i] != s[i % k]) { valid = false; break; } } if (valid) return true; } return false; } }; ``` ### 两种方法对比 - **时间复杂度** - 方法一:$O(n)$(字符串拼接和子串查找)。 - 方法二:$O(n \sqrt{n})$(因数分解和逐字符验证)。 - **空间复杂度** 两种方法均为 $O(n)$。 ### 示例分析 - **示例1**:`s = "abab"` - 方法一:拼接后字符串为 `"abababab"`,去掉首尾得到 `"bababa"`,其中包含 `"abab"`,返回 `true`。 - 方法二:子串长度 `k=2`,验证通过,返回 `true`。 - **示例2**:`s = "aba"` - 方法一:拼接后字符串为 `"abaaba"`,去掉首尾得到 `"baab"`,不包含 `"aba"`,返回 `false`。 - 方法二:无满足条件的 `k`,返回 `false`。 两种方法均能高效解决问题,具体实现可根据场景选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值