单字符重复子串的最大长度 图解题解
这道题到底在问什么
- 输入
- text = "ababa"
- 输出
- 3
- 输入
- text = "aaabaaa"(本节演示)
- 输出
- 6
最优解:为什么这么做
一句话答案:LeetCode 1156 单字符重复子串的最大长度按相同字符分组:每段数左段 l、跨一个数右段 r,候选取 min(l+r+1, 该字母总数),一遍扫完。时间 O(n)、空间 O(1)。
换一次字符,要让哪一段变最长
给一个只含小写字母的字符串 text,允许你挑两个位置上的字符互换一次,也允许一次都不换,而换进来的字符必须是串里本来就有的。换完之后,在结果里找一段「全是同一个字符」的子串,问这一段最长能有多长。题面 text="ababa" 时答案是 3,text="aaabaaa" 时答案是 6。
两两枚举交换位置,暴力法要跑三层
串长 n,最直白的做法是枚举「交换哪两个位置」——两两配对就有 n² 种换法,每换一次还得重新扫一遍串、量出最长的同字符段,又是一层 n,三层叠起来是 O(n³),串一长就跑不动。真正的浪费在于:相同字符本来就一段一段挨着排好,枚举交换却把这层现成的结构整个抹平。
相同字符早就连成段,何必再框一个窗口
既然相同字符天生连成一段段,与其维护一个来回伸缩的窗口,不如直接按段扫。先用一个计数桶记下每个字母在整串里出现多少次(a 有几个、b 有几个),后面判断「够不够换」要用它。然后从左到右把串切成一段段相同字符,对每一段单独问一句:用那唯一的一次交换,它最长能撑到多少。
一段字符怎么算它能撑到多长
站在某一段的起点,先数它自己有多长,记成左段 l。左段会停在第一个不同的字符上,那正是它右边那个「拦路」的字符。这一次交换恰好用来把拦路字符换走,于是跳过它、继续数右边还连着几个相同字符,记成右段 r。左段、换进中间空位的那一个、右段接成一整片,候选长度就是 l + r + 1。那个加一,是从串里别处搬一个相同字符填进中间。但前提是别处真有多余的同字符可搬:如果这个字母的总数还不到 l + r + 1,就拼不出那么长,得让总数把候选压回来,即 min(l + r + 1, 该字母总数)。每段算完,起点直接跳到这一段末尾的下一位,不回头重算。
aaabaaa 逐段数下来,6 就这么冒出来
用 text="aaabaaa" 走一遍,先数清 a 有 6 个、b 有 1 个。第一段从第 0 位起是 a,数到第 3 位撞上 b,左段 l=3;跨过这个 b,右边第 4、5、6 位又接着三个 a,右段 r=3;候选 l+r+1=7,可 a 拢共只有 6 个,min(7, 6)=6,答案先记成 6。第二段是那个 b,左段 l=1,跳过右边的 a 后再没别的 b,右段 r=0,候选 min(1+0+1, 1)=1,够不上 6。第三段又是 a(第 4 到第 6 位),左段 l=3,已经顶到串尾、右边没得跨,右段 r=0,候选 min(3+0+1, 6)=4,仍不敌 6。三段都看完,最长就是 6。
候选 7 却只能拿 6,卡在字母不够
每段起点只被外层走一次、右段也只多看一小段相同字符,整串一遍扫完,时间 O(n);计数桶最多装 26 个字母,与串多长无关,空间 O(1)。串里全是同一个字符时压根不用换,答案就是串长;全不相同的字符各自成段、别处没有第二个同字符可搬,候选 1+0+1=2 会被总数 1 压回 1。最容易漏的是候选算出 7、a 却只有 6 那种,第 7 个字符凭空变不出来,必须让总数给候选封顶。
▶ 动画逐步走查(共 27 步)——想跟着动画一帧帧对照就展开
- 3记牢三步: 数左段 l, 跨一个数右段 r, 候选 = min(l+r+1, 这个字母总数)。下面每一帧都在套它。
- 4上面这排是要演示的字符串 a a a b a a a, 一共 7 个字符。先把每个字母数清楚: a 有 6 个, b 有 1 个, 记在右边面板。接下来按「相同字符连成一段」分组, 一段一段看哪段能换出最长的同字符子串。当前最优答案先记 0。
- 5从第 0 位起开一个新组, 字母是 a。先数它本身这一段有多长, 第 0 位是 a, 左段长度 l 记到 1。
- 6继续往右, 第 1 位还是 a, 左段长度 l 数到 2。
- 7继续往右, 第 2 位还是 a, 左段长度 l 数到 3。
- 8第 3 位变成了 b, 不再是 a, 这一组的左段就停在这里, 左段长度 l = 3。
- 9这一组想更长, 就用唯一的那一次交换, 把中间这个 b(第 3 位, 标红)换出去。换走之后, 看它右边还连着几个 a, 能不能接上来。
- 10越过那个 b 往右看, 第 4 位也是 a, 右段长度 r 数到 1。
- 11越过那个 b 往右看, 第 5 位也是 a, 右段长度 r 数到 2。
- 12越过那个 b 往右看, 第 6 位也是 a, 右段长度 r 数到 3。
- 13右边也数到字符串末尾, 这一组的右段长度 r 定格在 3。
- 14把左段 l = 3 和右段 r = 3 接起来, 中间空出的那一位再换进一个 a, 候选长度 = 3 + 3 + 1 = 7。那个加一, 就是换进来补上中间的那个 a。
- 15可是全场只有 6 个 a, 要换进来的那个 a 必须真实存在。候选 7 超过了总数 6, 所以这一组实际最多排出 6 个 a。6 比原来的最优 0 大, 刷新最优答案 ans = 6。
- 16从第 3 位起开一个新组, 字母是 b。先数它本身这一段有多长, 第 3 位是 b, 左段长度 l 记到 1。
- 17第 4 位变成了 a, 不再是 b, 这一组的左段就停在这里, 左段长度 l = 1。
- 18这一组想更长, 就用唯一的那一次交换, 把中间这个 a(第 4 位, 标红)换出去。换走之后, 看它右边还连着几个 b, 能不能接上来。
- 19第 5 位是 a 了, 不是 b, 右段停在这里, 右段长度 r = 0。
- 20把左段 l = 1 和右段 r = 0 接起来, 中间空出的那一位再换进一个 b, 候选长度 = 1 + 0 + 1 = 2。那个加一, 就是换进来补上中间的那个 b。
- 21可是全场只有 1 个 b, 要换进来的那个 b 必须真实存在。候选 2 超过了总数 1, 所以这一组实际最多排出 1 个 b。1 没超过当前最优 6, ans 保持 6。
- 22从第 4 位起开一个新组, 字母是 a。先数它本身这一段有多长, 第 4 位是 a, 左段长度 l 记到 1。
- 23继续往右, 第 5 位还是 a, 左段长度 l 数到 2。
- 24继续往右, 第 6 位还是 a, 左段长度 l 数到 3。
- 25数到第 6 位就到字符串末尾了, 这一组 a 的左段长度 l 定格在 3。
- 26这一组 a 一直顶到字符串末尾, 右边没有位置可以向右跨, 所以右段 r = 0。但它左边紧挨着一个 b(第 3 位, 已标红): 只要整串里还有多余的 a, 就能用那一次交换把这个 b 换成 a, 把这一组往左接长一个。
- 27这一组顶在字符串末尾, 右边没法向右跨, 右段 r = 0。能加一, 是因为它左边紧挨着一个 b(第 3 位, 标红): 只要整串还有多余的 a, 就能把这个 b 换成 a, 把这组拼到 l + r + 1 = 3 + 0 + 1 = 4 个 a。
- 28a 的总数有 6 个, 够换, 所以这一组实打实能排出 4 个 a。4 没超过当前最优 6, ans 保持 6。
- 29所有组都看完了。最好的是第 0 组那段 a: 左边 3 个、右边 3 个, 中间隔着一个 b。一次交换就是把某个 a 和中间那个 b 对调: b 变成 a 接上两段, 但被对调走的那个 a 让某一端少一个, 所以全串总共只有 6 个 a, 最长就是 6。
⚠️ 容易写错的地方
✗ 错:以为能多换几个位置, 把好几个不同字符都跨过去
✓ 对:只能交换一次, 一段里最多跨过一个不同字符
题目限定一次交换, 跨两个不同字符需要两次, 不允许
✗ 错:直接拿 l + r + 1 当答案, 忘了字母不够的情况
✓ 对:要对 min(l + r + 1, 该字母总数) 封顶
换进中间的那个相同字符必须真实存在, 总数不够就拼不出那么长, 比如 "aaabaaa" 候选 7 但只有 6 个 a
✗ 错:处理完一段后 i 只加 1, 同一段被反复算
✓ 对:处理完一段, i 直接跳到这一段末尾的下一位 j
每段只需以它的起点算一次, i 跳到段末后面才不重复扫描, 保证线性
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def maxRepOpt1(self, text: str) -> int:
cnt = Counter(text)
n = len(text)
ans = i = 0
while i < n:
j = i
while j < n and text[j] == text[i]:
j += 1
l = j - i
k = j + 1
while k < n and text[k] == text[i]:
k += 1
r = k - j - 1
ans = max(ans, min(l + r + 1, cnt[text[i]]))
i = j
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int maxRepOpt1(string text) {
int cnt[26] = {0};
for (char& c : text) {
++cnt[c - 'a'];
}
int n = text.size();
int ans = 0, i = 0;
while (i < n) {
int j = i;
while (j < n && text[j] == text[i]) {
++j;
}
int l = j - i;
int k = j + 1;
while (k < n && text[k] == text[i]) {
++k;
}
int r = k - j - 1;
ans = max(ans, min(l + r + 1, cnt[text[i] - 'a']));
i = j;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int maxRepOpt1(String text) {
int[] cnt = new int[26];
int n = text.length();
for (int i = 0; i < n; ++i) {
++cnt[text.charAt(i) - 'a'];
}
int ans = 0, i = 0;
while (i < n) {
int j = i;
while (j < n && text.charAt(j) == text.charAt(i)) {
++j;
}
int l = j - i;
int k = j + 1;
while (k < n && text.charAt(k) == text.charAt(i)) {
++k;
}
int r = k - j - 1;
ans = Math.max(ans, Math.min(l + r + 1, cnt[text.charAt(i) - 'a']));
i = j;
}
return ans;
}
}复杂度
时间
O(n)
n 是字符串长度。i 每轮直接跳到本段末尾, 右段也只往后多看一小段, 每个位置被看的次数是常数, 整体线性
空间
O(1)
只用一个长度 26 的计数桶(Python 的 Counter 最多 26 个键), 和字符串多长无关, 算常数级
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 单字符重复子串的最大长度 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
候选长度为什么是 l + r + 1,不是 l + r?+
那一次交换不只是把中间拦路的字符换走,还能顺手从串里别处搬一个相同字符填进空出来的中间位置——左段、补进来的这一个、右段连成一整片,所以是 l 加 r 再加 1。前提是别处确实有多余的同字符可搬;要是这个字母的总数正好等于 l + r,那个加一就落空,会被 min 压回 l + r。
按相同字符分段扫,和变长滑动窗口是一回事吗?+
目标相通,写法更省心。相同字符本来就连成一段段,直接按段扫、每段只看「自己 + 跨一个 + 后面一段」就够了,不必维护一个随时伸缩的窗口。也有人用滑窗控制「窗口里除主字符外至多 1 个异类」来做,但分组写法读起来更直白,也少一层边界判断。
外层起点为什么要跳到段末之后,而不是逐位加一?+
每段只需从它的起点算一次候选,算完就该把整段掠过。要是处理完只把起点加一,同一段会被反复当成起点重算,扫描次数从线性退化成平方级。让起点直接落到这一段末尾的下一位,每个字符只被当作起点一次,才守得住 O(n) 这条线。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 单字符重复子串的最大长度 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。