题目描述
思路解析
一句话答案:LeetCode 2405 字符串的最优划分:贪心地从左扫,维护当前段用过的字符集合,遇到重复就被迫切一刀、清空集合另起一段,因为每段延到最长段数才最少,时间 O(n)。
把字符串切成几段,每段内部不能有重复字符
给一个只含小写字母的字符串 s,要把它切成若干连续子串,每个子串内部的字符必须互不相同,问最少切成几段。题面给的 s 是 abacaba,答案 4,切成 ab、ac、ab、a 四段;s 是 ssssss 时六个 s 只能各自成段,答案 6;s 是 abcdef 整串无重复,一段就够,答案 1。
每段延到不能延为止就切,这样够省吗
要让段数少,很自然会想每段能塞多长就塞多长:从头往当前段里装字符,不撞重复就一直装,直到再装一个就会重复,才在这里切一刀。可有个地方让人犯嘀咕——万一在某处提前收手、故意少装一个,会不会让后面几段拼得更长、总段数反而更少?被迫才切,真的不吃亏吗?
为什么被迫才切,切出来的段就最少
答案是不吃亏。这里用的是贪心——当前段的字符集合里只要还容得下这个字母,就把它并进这一段、绝不主动另起。为什么它对:一段之所以被切,是因为下一个字符在这段里已经出现过,此时这段无论如何都装不下它,切是被逼的,不是主动浪费。反过来,如果提前把某段切短,那些本可以留在这段的字符只能挤到后面的段里去,后面的段要么正好一样长、要么被迫更早撞重复,段数只会持平或变多。所以每段都撑到被迫切断,累起来的段数就是最少的。
落到代码:一个集合加一个段数
实现只要两样东西:一个集合 seen 记当前段用过哪些字符,一个计数 ans 记段数,ans 起手就是 1,因为整串至少占一段。从左到右读每个字符 ch:若 ch 已在 seen 里,当前段装不下它,就 ans 加 1、清空 seen,让旧段封口、新段从零起;随后无论走没走切断这一步,都把 ch 放进 seen,它是当前段新收的字符。读完全串,ans 就是最少段数。
abacaba 逐字读,四段怎么切出来
seen 空、ans=1 起步。读 a,不在 seen,放进去 seen={a};读 b,不在,seen={a,b};读到下标 2 的 a,它已经在 seen 里,当前段 ab 装不下第二个 a,ans 变 2、清空 seen,再把这个 a 收进新段 seen={a}。往后 c 并入成 {a,c};下标 4 的 a 又撞重复,ans 变 3、清空后 seen={a};b 并入成 {a,b};下标 6 最后一个 a 再撞,ans 变 4、清空后 seen={a}。串读完,ans=4,对应 ab、ac、ab、a 四段。
哪几步一写就错,复杂度又如何
ans 若从 0 起步,全程只在撞重复时才加 1,最后会比真实段数少一段,因为开头那段没人给它计数。切断时若只加 1 却忘了清空 seen,旧段用过的字符会继续拦着新段,本来能并的字符被当成重复,段数白白多切。还有清空之后忘了把当前这个 ch 放回 seen,下一个字符跟它比时就查不到,重复判断跟着错位。复杂度这边很干净:每个字符只读一次,时间 O(n);seen 最多装下 26 个小写字母,空间是常数 O(1)。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住「字符不重复就并入当前段,一旦重复就切一刀、清空集合另起一段」,下面每帧都在套它。
开局:还没读任何字符,当前段是空的,段数先记 1(最少也得有一段)。
扫到第 0 个字符 'a'。它不在当前段字符集里,加进来仍不重复,可以并入当前段。
把 'a' 放进集合(高亮行),当前段延长为 "a"。段数不变,继续往后扫。
扫到第 1 个字符 'b'。它不在当前段字符集里,加进来仍不重复,可以并入当前段。
把 'b' 放进集合(高亮行),当前段延长为 "ab"。段数不变,继续往后扫。
扫到第 2 个字符 'a',可它已经在当前段集合里了(高亮行)。当前段再装它就会重复,必须在这里切断。
切一刀:段数加到 2,集合清空,新的一段从 'a' 重新开始(蓝色是已切走的旧段,紫框是新段)。
扫到第 3 个字符 'c'。它不在当前段字符集里,加进来仍不重复,可以并入当前段。
把 'c' 放进集合(高亮行),当前段延长为 "ac"。段数不变,继续往后扫。
扫到第 4 个字符 'a',可它已经在当前段集合里了(高亮行)。当前段再装它就会重复,必须在这里切断。
切一刀:段数加到 3,集合清空,新的一段从 'a' 重新开始(蓝色是已切走的旧段,紫框是新段)。
扫到第 5 个字符 'b'。它不在当前段字符集里,加进来仍不重复,可以并入当前段。
把 'b' 放进集合(高亮行),当前段延长为 "ab"。段数不变,继续往后扫。
扫到第 6 个字符 'a',可它已经在当前段集合里了(高亮行)。当前段再装它就会重复,必须在这里切断。
切一刀:段数加到 4,集合清空,新的一段从 'a' 重新开始(蓝色是已切走的旧段,紫框是新段)。
扫到第 7 个字符 'c'。它不在当前段字符集里,加进来仍不重复,可以并入当前段。
把 'c' 放进集合(高亮行),当前段延长为 "ac"。段数不变,继续往后扫。
扫到第 8 个字符 'e'。它不在当前段字符集里,加进来仍不重复,可以并入当前段。
把 'e' 放进集合(高亮行),当前段延长为 "ace"。段数不变,继续往后扫。
整串扫完,一路切了 3 刀,得到 4 段:ab | ac | ab | ace,每段内部都没有重复字符。最少划分段数 = 4。
边界先想清:单字符为 1、全相同为 n、全不重复为 1。
面试重点:贪心正确性 + 可用位掩码把集合换成常数操作。
参考代码
class Solution: def partitionString(self, s: str) -> int: seen = set() ans = 1 for ch in s: if ch in seen: ans += 1 seen.clear() seen.add(ch) return ans复杂度
- 时间:O(n),每个字符只读一次
- 空间:O(1),集合最多装 26 个小写字母
易错点
面试追问把动画讲成自己的话
追问能不能不用集合,改用别的结构?
追问这题为什么贪心一定对?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
将字符串中的元音字母排序
LeetCode 2785 · 中等 · 沿着 贪心套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题