题目描述
统计字符串中回文子串的个数(内容相同但起止位置不同算多个)。
思路解析
最直接的笨办法:枚举起点和终点(n² 个子串),每个子串再花 O(n) 判断是不是回文,合起来 O(n³)。换个思路:回文一定有个中心,从中心往两边扩,能省一个量级。
转折:为什么成立——回文关于中心镜像对称,知道中心就能向外验证。对每个中心,l、r 一起往外走,只要 s[l]==s[r] 就计数 +1(发现一个新回文)并继续;一旦不等或越界就停。中心有两种:单字符(奇回文)和两字符之间(偶回文),都要数。
奇中心 · 下标 1 的 a:从中间 a(下标 1)出发,l=r=1。奇中心第一步必成立:单字符 "a" 就是回文,count = 1。继续往两边扩。
奇中心 1 · 向外扩 a==a:l 退到 0、r 进到 2,s[0]==s[2],扩成功!发现回文 "aaa",count = 2。这个奇中心已贡献 2 个,再往外扩。
奇中心 1 · l 越界,停:转折(负例):l 再退一步就到 −1 越界,扩展停止。以下标 1 为奇中心一共数到 2 个回文。换下一个中心。
奇中心 · 下标 0 的 a:换成下标 0 的 a 当奇中心,l=r=0,单字符 "a" 又是一个回文,count = 3。试着往外扩。
奇中心 0 · l 越界,停:左边已经到头,l 退一步就越界,停。最左那个 a 当中心只能扩出它自己。下标 2 的 a 同理也只贡献 1 个(对称)。
偶中心 · 下标 0、1 之间:偶中心登场:以下标 0、1 之间为中心,l=0、r=1,s[0]==s[1],扩成功,发现 "aa",count = 4。偶回文只能从这种中心找到。
偶中心 0-1 · l 越界,停:l 再退就越界,停。这个偶中心数到 1 个 "aa"。还剩最后一个偶中心:下标 1、2 之间。
偶中心 · 下标 1、2 之间:最后一个偶中心(下标 1、2 之间):l=1、r=2,s[1]==s[2],又一个 "aa",count = 5。再扩 l 到 0、r 到 3 时 r 越界,停。
所有中心累计 · 共 6 个:把每个奇中心、偶中心扩出的回文全加起来:3 个 "a" + 2 个 "aa" + 1 个 "aaa" = 6。这就是答案。
最长回文子串(LC5)是「记录最长」,回文子串(647)是「累加计数」——同一个中心扩展骨架,只换「扩成功时做什么」就解决不同问题。这就是掌握套路的威力。
参考代码(Python)
1n = len(s)
2count = 0
3def expand(l, r): # 从中心向两边扩并计数
4 nonlocal count
5 while l >= 0 and r < n and s[l] == s[r]: # 两端相等才继续
6 count += 1 # 每扩成功一次 = 一个回文
7 l -= 1; r += 1
8for i in range(n):
9 expand(i, i) # 奇中心: 单字符
10 expand(i, i + 1) # 偶中心: 两字符之间
11return count复杂度分析
- 时间复杂度:O(n²) —— n 个位置 × 每个中心最多扩 n/2 步
- 空间复杂度:O(1) —— 只用 l、r 和一个计数器
套路模板
记住骨架:奇偶中心都试、相等就扩、扩成功就收集。把「收集」换成计数、记最长、记区间,就分别解 647、5 等回文题。
1def expand(l, r):
2 while l >= 0 and r < n and s[l] == s[r]: # 两边相等就扩
3 count += 1 # 收集: 计数(或记最长/区间)
4 l -= 1; r += 1
5for i in range(n):
6 expand(i, i) # 奇中心
7 expand(i, i + 1) # 偶中心易错点
- 错误写法:只数奇中心 expand(i,i) → 正确写法:奇 (i,i) 和偶 (i,i+1) 两种中心都要扩("aa" 这类偶回文中心落在两字符之间,只数奇中心会漏掉它们,结果偏少)
- 错误写法:扩成功后忘了 count += 1,或在 while 外才 +1 → 正确写法:每次 s[l]==s[r] 成立就立刻 count += 1(计数要绑在「扩成功」这一刻:每多对齐一层就是一个新回文,放 while 外会少算或算错)
以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握。