题目链接
题目描述
给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。
示例 1:
输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。
示例 2:
输入: nums = [1,2,3,4], k = 3
输出: false
提示:
1 <= k <= len(nums) <= 16
0 < nums[i] < 10000
每个元素的频率在 [1,4] 范围内
求解思路
- DFS+剪枝:
- 首先明确题目要求,将数组元素划分为k个子集,要求每个子集的和都相等。这个问题等价于把若干个权重不同的球放入k个桶中,让每个桶的值都相同。
- 首先我们求出所有元素值的和
sum
,如果sum
不是k
的倍数,那么不管怎么划分都不可能保证每个子集(桶)中的值相等所以直接返回false
。 target
表示每个桶要达到的目标值,我们对数组进行排序,让其降序排列。目的是先给数值大的元素划分,也就是先把大球往桶里放,可以增加剪枝的命中率,减少回溯。index
表示当前需要判断的是第几个球,bucket[i]
表示第i
个桶的存放情况。- 搜索过程中进行了两次剪枝:
- 如果当前桶和上一个桶内的元素和相等,则跳过。因为如果元素和相等,那么
nums[index]
选择上一个桶和选择当前桶可以得到的结果是一致的。有解就都有解,无解就都无解。 - 如果放入当前元素,桶会超过目标值,则跳过。
- 如果当前桶和上一个桶内的元素和相等,则跳过。因为如果元素和相等,那么
- 方法最后:如果桶遍历完了但是元素没有遍历完,说明不满足要求。
实现代码
class Solution {
public boolean canPartitionKSubsets(int[] nums, int k) {
int sum = 0;
for (int i = 0; i < nums.length; i ++) {
sum += nums[i];
}
// 如果总和不能刚好分成k份
if (sum % k != 0) {
return false;
}
// 每个子集的值
int target = sum / k;
// 排序,数组降序排列,目的是给数值大的元素划分,可以增加剪枝的命中率,减少回溯
Arrays.sort(nums);
for (int i = 0; i < nums.length/2; i++) {
int temp = nums[i];
nums[i] = nums[nums.length - 1 - i];
nums[nums.length - 1 - i] = temp;
}
// 存放子集的桶
int[] bucket = new int[k];
return backtrack(nums, 0, bucket, k, target);
}
// index表示当前遍历到第几个数字,bucket[i]表示第i个桶
private boolean backtrack(int[] nums, int index, int[] bucket, int k, int target) {
// 如果所有数字元素都能遍历完,说明没有问题
if (index == nums.length) {
return true;
}
for (int i = 0; i < k; i ++) {
// 如果当前桶和上一个桶内的元素和相等,则跳过
// 如果元素和相等,那么nums[index]选择上一个桶和选择当前桶可以得到的结果是一致的
if (i > 0 && bucket[i] == bucket[i - 1]) {
continue;
// 如果放入当前元素桶会超过目标值,则跳过
} else if (bucket[i] + nums[index] > target) {
continue;
} else {
// 桶中放入当前值
bucket[i] += nums[index];
// 递归进行下次搜索
if (backtrack(nums, index + 1, bucket, k, target)) {
return true;
}
// 回溯
bucket[i] -= nums[index];
}
}
// 如果桶遍历完了但是元素没有遍历完,说明不满足要求
return false;
}
}