题目描述
[1681] 最小不兼容性
给你一个整数数组 nums 和一个整数 k 。你需要将这个数组划分到 k 个相同大小的子集中,使得同一个子集里面没有两个相同的元素。
一个子集的 不兼容性 是该子集里面最大值和最小值的差。
请你返回将数组分成 k 个子集后,各子集 不兼容性 的 和 的 最小值 ,如果无法分成分成 k 个子集,返回 -1 。
子集的定义是数组中一些数字的集合,对数字顺序没有要求。
示例 1:
输入:nums = [1,2,1,4], k = 2
输出:4
解释:最优的分配是 [1,2] 和 [1,4] 。
不兼容性和为 (2-1) + (4-1) = 4 。
注意到 [1,1] 和 [2,4] 可以得到更小的和,但是第一个集合有 2 个相同的元素,所以不可行。
示例 2:
输入:nums = [6,3,8,1,3,1,2,2], k = 4
输出:6
解释:最优的子集分配为 [1,2],[2,3],[6,8] 和 [1,3] 。
不兼容性和为 (2-1) + (3-2) + (8-6) + (3-1) = 6 。
示例 3:
输入:nums = [5,3,3,6,3,3], k = 3
输出:-1
解释:没办法将这些数字分配到 3 个子集且满足每个子集里没有相同数字。
提示:
1 <= k <= nums.length <= 16
nums.length 能被 k 整除。
1 <= nums[i] <= nums.length
- 搜索
- 剪枝
- 集合
- bitset
题目剖析&信息挖掘
此题为搜索题,模拟选择集合然后求解,需要用到剪枝和集合。
解题思路
方法一 巧用数字表示集合&搜索剪枝
思考
- 刚一看到题目的规模只有16(很小),就想到基本上是搜索穷举法,
- 以k=4为例,相当于是把数字放到4个集合里,然后计算各集合的不兼容性,再加起来。
- 但是一算复杂度,相当于是C(16,4)*C(12, 4)*C(8,4)*C(4,4) = 1820*495*70*1 = 63063000, 数量太大了。
- 那么就想到加剪枝的方法,想到一个条件就是在搜索时把当前最佳答案记一下。每次选出一个集合时就检查当前所得到的不兼容性的和与当前最优解,如果已经大于等于最优解,就停止搜索。
分析
- 有了上面的基础我试着做一些数据验证。
- 以nums=[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, k=4
- 我先搜索出所有4个数字的集合,对他们的不兼容性作统计。
不兼容性大小 | 个数 |
---|---|
1 | 0 |
2 | 0 |
3 | 13 |
4 | 36 |
5 | 66 |
6 | 100 |
7 | 137 |
8 | 168 |
9 | 196 |
10 | 216 |
11 | 225 |
12 | 220 |
13 | 198 |
14 | 156 |
15 | 91 |
- 我们知道这一些数据的最佳分组是 (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12), (13, 14, 15, 16),答案是12
- 我们粗算一下,那么对于第1组集合来说不兼容性>=4的集合基本上选择一次就不再去选了,那可选的一共是 13+36 = 49
- 对于第2组集合可选择的余地就更小了。
- 所以通过剪枝的方法可以把穷举次数下降到百万级别。
- 这里还有一个问题要解决,就是如何去穷举。
- 直观的做法是先选取出第一个集合,接着再枚举出第2个,依次类推。
- 这种做法会存在重复计算,导致算法复杂度无形增加。
- 举例说明一下,(1, 2, 3, 4),(5, 6, 7, 8),(9, 10, 11, 12), (13, 14, 15, 16)
- (1, 2, 3, 9),(5, 6, 7, 8),(4, 10, 11, 12), (13, 14, 15, 16)
- 上面的例子中第2个集合是重复枚举的,如果是通过逐位确定的方式肯定会存在重复计算的过程。
- 那么我们可以先把所有的集合都构造出来,然后依次去枚举整个集合。
- 如何构造集合呢,我发现这个数字特别小16个。我们的int有32位,可以利用bitset的思想。用一个整数去表示集合。
- 比如 10000 = {4}, 111110010 = {1,4,5,6,7,8}。
- 那么不兼容性就是等就最高位1与最低位1的间隔。
思路
- 先根据题目的数组构造出所有长度为len(nums)/k的集合。
- 利用dfs算法,对每一层选择一个集合。
- 判断剪枝条件,当前解是否超出最优解,还有一个条件是选择的数字是否超出实际数字的个数。
- 选择到第k个时要更新最优解,并返回。
const n = 10000
type Incompat struct {
setValue map[int]int // 每个集合的分数
validSet []int //存储可以选择的集合
minSetValue int // 存储最优级解
}
// 计算集合分数
func (s *Incompat) getSetValue(n int) int {
}
// 生成可选择的集合
func (s *Incompat) MakeSet(dep int, nums map[int]bool, selNum, set int) {
}
// 选择和恢复原来的数字,要对数字进行计数
func (s *Incompat) addNumb(numCnt map[int]int, n, d int) bool {
}
func (s *Incompat) DFS(first int, numCnt map[int]int, selectSetCnt, totalVal int) {
if totalVal >= s.minSetValue { // 判断是否超出最优解
return
}
if selectSetCnt == 0 {
if totalVal < s.minSetValue {
s.minSetValue = totalVal
}
return
}
for ; first < len(s.validSet); first++ {
if !s.addNumb(numCnt, s.validSet[first], -1) { // 判是数字个数是否还够,并减掉
continue
}
s.DFS(first, numCnt, selectSetCnt-1, totalVal+s.setValue[s.validSet[first]])
s.addNumb(numCnt, s.validSet[first], 1) //加回之前选择的数字
}
}
// 测试获得兼容性为 x 的集合
func (s *Incompat) ShowSetValue(x int) int {
cnt := 0
for _, n := range s.validSet {
//fmt.Printf("%d %b %d\n", n, n, s.setValue[n])
if s.setValue[n] == x {
cnt++
}
}
return cnt
}
func minimumIncompatibility(nums []int, k int) int {
s := &Incompat{}
s.setValue = make(map[int]int, n)
s.validSet = make([]int, 0, n)
numsMap := map[int]bool{}
numsCnt := map[int]int{}
for _, n := range nums {
numsMap[n] = true
numsCnt[n]++ // 统计数字个数
}
s.MakeSet(0, numsMap, len(nums)/k, 0) // 利用所有存在的数字,构造可选择的集合
s.minSetValue = math.MaxInt32
s.DFS(0, numsCnt, k, 0)
if s.minSetValue == math.MaxInt32 {
s.minSetValue = -1
}
/*
// 测试代码 查看各不兼容性的数值
for i:=1;i<16;i++ {
fmt.Println(i, s.ShowSetValue(i))
}
*/
return s.minSetValue
}
注意
- 最优解要初始化
知识点
- bitset
- DFS
- 搜索&剪枝
复杂度
- 时间复杂度:不好估,剪枝优化了
- 空间复杂度:O(1)
参考
代码实现
const n = 10000
type Incompat struct {
setValue map[int]int // 每个集合的分数
validSet []int //存储可以选择的集合
minSetValue int // 存储最优级解
}
// 计算集合分数
func (s *Incompat) getSetValue(n int) int {
high, low := uint(16), uint(1)
for ; ((1 << high) & n) == 0; high-- {
}
for ; ((1 << low) & n) == 0; low++ {
}
return int(high - low)
}
// 生成可选择的集合
func (s *Incompat) MakeSet(dep int, nums map[int]bool, selNum, set int) {
if selNum == 0 {
s.validSet = append(s.validSet, set)
s.setValue[set] = s.getSetValue(set)
}
if dep == 16 {
return
}
for i := dep + 1; i <= 16; i++ {
if !nums[i] {
continue
}
nums[i] = false
s.MakeSet(i, nums, selNum-1, set|(1<<uint(i)))
nums[i] = true
}
}
// 选择和恢复原来的数字,要对数字进行计数
func (s *Incompat) addNumb(numCnt map[int]int, n, d int) bool {
if d < 0 {
for i := uint(1); i <= 16; i++ {
if (n & (1 << i)) == 0 {
continue
}
if numCnt[int(i)] <= 0 { // 数字不够用了,返回不可选择
return false
}
}
}
for i := uint(1); i <= 16; i++ {
if (n & (1 << i)) == 0 {
continue
}
numCnt[int(i)] += d
}
return true
}
func (s *Incompat) DFS(first int, numCnt map[int]int, selectSetCnt, totalVal int) {
if totalVal >= s.minSetValue {
return
}
if selectSetCnt == 0 {
if totalVal < s.minSetValue {
s.minSetValue = totalVal
}
return
}
for ; first < len(s.validSet); first++ {
if !s.addNumb(numCnt, s.validSet[first], -1) { // 判是数字个数是否还够,并减掉
continue
}
s.DFS(first, numCnt, selectSetCnt-1, totalVal+s.setValue[s.validSet[first]])
s.addNumb(numCnt, s.validSet[first], 1) //加回之前选择的数字
}
}
// 测试获得兼容性为 x 的集合
func (s *Incompat) ShowSetValue(x int) int {
cnt := 0
for _, n := range s.validSet {
//fmt.Printf("%d %b %d\n", n, n, s.setValue[n])
if s.setValue[n] == x {
cnt++
}
}
return cnt
}
func minimumIncompatibility(nums []int, k int) int {
s := &Incompat{}
s.setValue = make(map[int]int, n)
s.validSet = make([]int, 0, n)
numsMap := map[int]bool{}
numsCnt := map[int]int{}
for _, n := range nums {
numsMap[n] = true
numsCnt[n]++ // 统计数字个数
}
s.MakeSet(0, numsMap, len(nums)/k, 0) // 利用所有存在的数字,构造可选择的集合
s.minSetValue = math.MaxInt32
s.DFS(0, numsCnt, k, 0)
if s.minSetValue == math.MaxInt32 {
s.minSetValue = -1
}
/*
// 测试代码 查看各不兼容性的数值
for i:=1;i<16;i++ {
fmt.Println(i, s.ShowSetValue(i))
}
*/
return s.minSetValue
}
/*
minimumIncompatibility([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, 4)
*/