题解
思路:
- 首先确定能否分为k组,即
sum(nums) % k == 0
是否成立,如果不成立显然无法分组 - 得到每组集合的和
avg = sum(nums) / k
. - 判断能否找到一种分组方式
s1,s2,...,sk
,使得sum(s1)=sum(s2)=...=sum(sk)=avg
。
使用回溯来进行分组,在所有可能的情况中找到一个。单纯的回溯会超时:
class Solution {
public boolean canPartitionKSubsets(int[] nums, int k) {
int sum = 0, avg, maxNum = 0;
for(int num : nums){
sum += num;
maxNum = Math.max(maxNum, num);
}
if(sum % k != 0){
return false;
}
avg = sum / k; //获取平均值
//至少有一个数大于了均值,则一定无法分组
if(maxNum > avg){
return false;
}
//能否找到k组,每组和的值为avg
return dfs(0, nums, new int[k], avg, k);
}
private boolean dfs(int start, int[] nums, int[] set, int target, int k){
if(start == nums.length){
boolean ret = true;
for (int i = 0; i < k; i++) {
if(set[i] != target) {
ret = false;
break;
}
}
return ret;
}
boolean ret = false;
//当前数加到某个集合中
for (int j = 0; j < k; j++) {
if (set[j] + nums[start] <= target){
set[j] += nums[start];
ret = ret || dfs(start+1, nums, set, target, k);
set[j] -= nums[start]; //回溯
}
}
return ret;
}
}
需要利用一些剪枝操作来减少部分复杂度:
- 对数组从大到小进行排序,使得递归过程中的
set[j] + nums[start] > target
命中率(?)更高 - 需要注意的是,遍历到某个数
num
时,对应的k
个集合为s1,s2,...,sk
,如果有多个集合的和是相同的,那么当前值只需要选择一个集合放入即可,不需要全部放入一次进行多余的判断。(这个是剪枝的一个重点) - 递归退出条件的简化:
start == nums.length
时,不需要再做额外的判断,直接返回true
即可。因为每个数都是在满足set[j] + nums[start] <= target
时才会被放入,而不能均分的情况已经在一开始被判断掉了,因此如果所有数都被选择到,则当前k
个集合一定是我们需要的那个答案。 - 对于第一个整数,其放在任意一个集合中的情况得到的结果都是相同的,因此直接放在第一个集合中即可,不判断后续集合。
修改后的代码如下:
class Solution {
public boolean canPartitionKSubsets(int[] nums, int k) {
int sum = 0, avg, maxNum = 0;
for(int num : nums){
sum += num;
maxNum = Math.max(maxNum, num);
}
if(sum % k != 0){
return false;
}
avg = sum / k; //获取平均值
//至少有一个数大于了均值,则一定无法分组
if(maxNum > avg){
return false;
}
//逆序排列,提高命中率
int[] sortedNums = Arrays.stream(nums).boxed().sorted((a, b) -> b - a).mapToInt(value -> value).toArray();
//能否找到k组,每组和的值为avg
// System.out.println(new Date());
boolean dfs = dfs(0, sortedNums, new int[k], avg, k);
// System.out.println(new Date());
return dfs;
}
private boolean dfs(int start, int[] nums, int[] set, int target, int k){
if(start == nums.length){
return true;
}
boolean ret = false;
//当前数加到某个集合中
for (int j = 0; j < k; j++) {
//第一个球的情况
if (start == 0 && j > 0) break;
// 重复排列的选择,连续的多个集合内和相同时,当前值选择一个放置即可
// 如果当前集合和上一个集合内的元素和相等,则跳过
// 原因:如果元素和相等,那么 nums[] 选择上一个集合和选择当前集合得到的结果是一致的
if (j > 0 && set[j] == set[j - 1]) continue;
if (set[j] + nums[start] > target) continue;
set[j] += nums[start];
ret = dfs(start+1, nums, set, target, k);
if (ret){
return true;
}
set[j] -= nums[start]; //回溯
}
return ret;
}
}