某老板:你的工资是5到6千,不是5千到6千~
刚刷到这个,真给我看懵了。
员工入职前听老板说工资“五到六千”,正常人谁听了不理解成五千到六千?结果发工资一看,三千。去问老板,人家还挺淡定,说我核实了,三千没错,当初跟你讲的是“五到六千”,不是“五千到六千”。
这话一出来,打工人血压都上来了。不是,这差别在哪?搁这儿玩文字密室逃脱呢?工资能这么解释,那以后“周末双休”是不是也能说成“双休是双倍休息幻想”?“包吃住”是不是变成“包含吃住这个词”?
最离谱的是,老板还一副自己很严谨的样子。好像不是他坑,是你语文学得太认真了。
这种操作真的挺牛马的,合同不写清楚,口头画饼,发钱开始抠字眼。HR看了都得沉默两秒:原来还能这么赖啊。
字符串 abab 一眼能看出来是重复的,aba 就不行。
但代码不能靠眼睛。这个题卡人的地方不在“会不会枚举”,而在你怎么判断一个字符串是不是由某个更短的片段拼出来的。
比如:
"abab" = "ab" + "ab"
"abcabc" = "abc" + "abc"
"aaaa" = "a" + "a" + "a" + "a"
"abac" 不行
我第一反应一般不枚举所有子串长度。能过,但有点笨。尤其字符串长一点时,你会发现自己在不停切片、拼接、比较,写出来像在硬搬砖。
这个题有个很顺手的判断:看 KMP 里的 next 数组,也就是前缀表。
前缀表记录什么?
记录当前位置之前,字符串的“最长相同前后缀”长度。
拿 ababab 看:
a b a b a b
0 0 1 2 3 4
最后一个值是 4,说明整个字符串长度 6 里面,前后有 4 个字符能对上。
那剩下的长度就是:
6 - 4 = 2
这个 2 就是可能重复的最小周期长度。只要总长度能被它整除,就说明可以重复拼出来。
看代码:
classSolution:
defrepeatedSubstringPattern(self, s: str) -> bool:
n = len(s)
if n < 2:
returnFalse
lps = [0] * n
left = 0
for right in range(1, n):
while left > 0and s[right] != s[left]:
left = lps[left - 1]
if s[right] == s[left]:
left += 1
lps[right] = left
same_part = lps[-1]
if same_part == 0:
returnFalse
block_len = n - same_part
return n % block_len == 0
这里我比较在意这句:
left = lps[left - 1]
很多人第一次写 KMP,错就错在这里。匹配不上时,不是把 left 粗暴归零,而是退回到上一个还能接着匹配的位置。
否则你前面算出来的信息全浪费了。
跑几个例子:
cases = ["abab", "aba", "abcabcabc", "aaaa", "abac"]
for x in cases:
print(x, Solution().repeatedSubstringPattern(x))
输出大概是:
abab True
aba False
abcabcabc True
aaaa True
abac False
还有一种很短的写法:
defcheck(s: str) -> bool:
return s in (s + s)[1:-1]
这写法面试里经常能看到,也确实巧。
比如 abab 拼两次变成:
abababab
去掉头尾:
bababa
里面还能找到原字符串 abab,说明它有周期。
但我一般不会只写这个。太像背答案了。面试官多问一句“为什么”,你还得绕回周期解释。
KMP 这版虽然代码长一点,但它把判断过程摊开了:先找最长相同前后缀,再算周期长度,最后看能不能整除。
这个题真正要防的不是语法,是误判。
ababa 的前后缀也能对上,但它不能由重复子串拼出来,因为长度 5 没法被周期 2 整除。
所以最后这个判断不能少:
return n % block_len == 0
少了它,代码看着像过了,实际一测边界就漏。算法题里这种“看起来差不多”的地方,通常就是坑。