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用户

通过次数 14.5K 提交次数 22.2K 通过率 65.3%

Related Posts

1026. 节点与其祖先之间的最大差值

## 题目描述:给定二叉树的根节点 root,找出存在于 不同 节点 A 和 B 之间的最大值 V,其中 V = |A.val - B.val|,且 A 是 B 的祖先。(如果 A 的任何子节点之一为 B,或者 A 的任何子节点是 B 的祖先,那么我们认为 A 是 B 的祖先)示例 1: ![](/img/leetcode/1026节点与其祖先之间的最大差值/tmp-tre

read more

1038. 从二叉搜索树到更大和树

## 题目描述:给定一个二叉搜索树 root (BST),请将它的每个节点的值替换成树中大于或者等于该节点值的所有节点值之和。提醒一下, 二叉搜索树 满足下列约束条件:- 节点的左子树仅包含键 小于 节点键的节点。 - 节点的右子树仅包含键 大于 节点键的节点。 - 左右子树也必须是二叉搜索树。*示例 1:***![](/img/leetcode/

read more

105. 从前序与中序遍历序列构造二叉树

## 题目描述:给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。示例 1: ``` 输入: preorder = [3,9,20,15,7], inorder = [

read more

106. 从中序与后序遍历序列构造二叉树

## 题目描述:给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。示例 1: ``` 输入:inorder = [9,3,15,20,7], postorder

read more

109. 有序链表转换二叉搜索树

## 题目描述:给定一个单链表的头节点 head ,其中的元素 按升序排序 ,将其转换为高度平衡的二叉搜索树。本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差不超过 1。示例 1: ``` 输入: head = [-10,-3,0,5,9] 输出: [0,-3,9

read more

114. 二叉树展开为链表

## 题目描述:给你二叉树的根结点 root ,请你将它展开为一个单链表:- 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。 - 展开后的单链表应该与二叉树 先序遍历 顺序相同。示例 1: ``` 输入:root

read more