3144. 分割字符频率相等的最少子字符串
题目描述:
给你一个字符串 s
,你需要将它分割成一个或者更多的 平衡 子字符串。比方说, s == "ababcc"
那么 ("abab", "c", "c")
, ("ab", "abc", "c")
和 ("ababcc")
都是合法分割,但是 ("a", "bab", "cc")
, ("aba", "bc", "c")
和 ("ab", "abcc")
不是,不平衡的子字符串用粗体表示。
请你返回 s
最少 能分割成多少个平衡子字符串。
注意: 一个 平衡 字符串指的是字符串中所有字符出现的次数都相同。
示例 1:
输入: s = "fabccddg"
输出: 3
解释:
我们可以将 `s` 分割成 3 个子字符串: `("fab, "ccdd", "g")` 或者 `("fabc", "cd", "dg")` 。
示例 2:
输入: s = "abababaccddb"
输出: 2
解释:
我们可以将 `s` 分割成 2 个子字符串: `("abab", "abaccddb")` 。
提示:
1 <= s.length <= 1000
s
只包含小写英文字母。
解题分析及思路:
方法:动态规划
题解CopyRight:灵茶山艾府 🔗
思路:
首先说明,分割方案是一定存在的,因为单个字母是平衡的,我们一定可以把 划分成 个平衡子串。
一、寻找子问题
示例 1 的 ,枚举最后一段的长度:
- 最后一段分割出一个长为 的子串,即 ,这是平衡的,问题变成剩余字符串 最少能分割出多少个平衡子串。
- 最后一段分割出一个长为 的子串,即 ,这是平衡的,问题变成剩余字符串 最少能分割出多少个平衡子串。
- ……
在这个过程中,我们只需要知道剩余字符串的长度,因为剩余字符串一定是 的一个前缀。
这些问题都是和原问题相似的、规模更小的子问题,可以用递归解决。
注 1:从右往左思考,主要是为了方便把递归翻译成递推。从左往右思考也是可以的。
注 2:动态规划有「选或不选」和「枚举选哪个」两种基本思考方式。在做题时,可根据题目要求,选择适合题目的一种来思考。本题用到的是「枚举选哪个」。
二、状态定义与状态转移方程
根据上面的讨论,我们只需要在递归过程中跟踪以下信息:
- :剩余字符串是 到 。
因此,定义状态为 ,表示当剩余字符串是 到 时,最少能分割出多少个平衡子串。
枚举最后一段从 到 ,如果这个子串是平衡的,那么接下来要解决的问题是:当剩余字符串是 到 时,最少能分割出多少个平衡子串,即 。
枚举所有小于等于 的 ,取 的最小值,即
其中 到 是平衡子串。
如何快速判断子串是平衡的呢?
我们可以在倒序枚举 的同时,用一个哈希表(或者数组)统计每个字符的出现次数。如果子串中每个字母的出现次数都相等,那么子串是平衡的。
优化:设子串中有 种字母,字母出现次数的最大值为 。子串是平衡的,当且仅当子串长度 等于 。
递归边界:。
递归入口:,也就是答案。
三、递归搜索 + 保存递归返回值 = 记忆化搜索
考虑到整个递归过程中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:
- 如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个 数组中。
- 如果一个状态不是第一次遇到( 中保存的结果不等于 的初始值),那么可以直接返回 中保存的结果。
注意: 数组的初始值一定不能等于要记忆化的值!例如初始值设置为 ,并且要记忆化的 也等于 ,那就没法判断 到底表示第一次遇到这个状态,还是表示之前遇到过了,从而导致记忆化失效。一般把初始值设置为 。
本题当 时, 一定是正数(因为任意字符串都存在合法分割方案),所以 数组初始化成 也可以。
四、1:1 翻译成递推
我们可以去掉递归中的「递」,只保留「归」的部分,即自底向上计算。
具体来说, 的定义和 的定义是一样的,都表示当剩余字符串是 到 时,最少能分割出多少个平衡子串。这里 是为了把 这个状态也翻译过来,这样我们可以把 作为初始值。
相应的递推式(状态转移方程)也和 一样:
其中 到 是平衡子串。
初始值 ,翻译自递归边界 。
答案为 ,翻译自递归入口 。
func minimumSubstringsInPartition(s string) int {
n := len(s)
f := make([]int, n+1)
for i := range s {
f[i+1] = math.MaxInt
cnt := [26]int{}
k, maxCnt := 0, 0
for j := i; j >= 0; j-- {
b := s[j] - 'a'
if cnt[b] == 0 {
k++
}
cnt[b]++
maxCnt = max(maxCnt, cnt[b])
if i-j+1 == k*maxCnt {
f[i+1] = min(f[i+1], f[j]+1)
}
}
}
return f[n]
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
复杂度:
- 时间复杂度:O(N2)
- 空间复杂度:O(N + C)
执行结果:
- 执行耗时:1 ms,击败了40.84 的Go用户
- 内存消耗:2.4 MB,击败了28.50 的Go用户