题目
给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。
示例 1:
输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总
提示:
1 <= k <= len(nums) <= 16
0 < nums[i] < 10000
解题思路
我一直秉持的一个观点的是“其实递归本身就是完成一种重复功能的形式化描述”,最好在函数命名中能体现出来提醒自己。那么,对于这个题,其实有两个重要的点,需要我们去理解,理解透这两个点,这道题的解法我们就完全理解了,还是,先上代码。
代码
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;
//求出子集的和
sum=sum/k;
//排序,升序
Arrays.sort(nums);
//如果数组的最大值大于子集的和,则返回false
if(nums[nums.length-1]>sum) return false;
//建立一个人长度为k的桶
int[]arr=new int[k];
//桶的每一个值都是子集的和,即空桶的容量为子集的和
Arrays.fill(arr,sum);
//从数组最后一个数开始进行递归
return forEveryNum(nums,nums.length-1,arr,k);
}
boolean forEveryNum(int[]nums,int cur,int[]arr,int k){
//cur走到-1时,说明所有的数全部都放进桶里了。这时一定是true
if(cur<0){
return true;
}
//对每个数来说,遍历k个桶,选择一个桶放入
for(int i=0;i<k;i++){
//如果正好能放下当前的数或者放下当前的数后,桶里的容量还有剩余
if(arr[i]==nums[cur]||(cur>0&&arr[i]-nums[cur]>=nums[0])){
//放当前的数到桶里
arr[i]-=nums[cur];
//递归放下一个数
if(forEveryNum(nums,cur-1, arr,k)) return true; //只需要找到一个即可。
//这个数不该放在桶i中,从桶中拿回当前的数
arr[i]+=nums[cur];
}
}
return false;
}
}
两个重要的点
- for循环的那两句代码
for(int i=0;i<k;i++){
//如果正好能放下当前的数或者放下当前的数后,桶里的容量还有剩余
if(arr[i]==nums[cur]||(cur>0&&arr[i]-nums[cur]>=nums[0])){
- if+递归函数 return true这一句代码
if(forEveryNum(nums,cur-1, arr,k)) return true; //只需要找到一个即可。
不过这两个重要的点的核心逻辑其实就只有一个,我们要理解这个递归函数的功能
。首先,我将递归函数命名为forEveryNum
,就是想透露一个信息,这个递归函数是对nums里的每个数字进行处理的一个重复性功能的描述。
//对每个数来说,遍历k个桶,选择一个桶放入
for(int i=0;i<k;i++){
//如果正好能放下当前的数或者放下当前的数后,桶里的容量还有剩余
if(arr[i]==nums[cur]||(cur>0&&arr[i]-nums[cur]>=nums[0])){
其实上面这个for循环的意思就是说,对于每个数来讲,其都有k个选择(就是k个桶),每个数都需要尝试放在第i(i>=0&&i<K)个桶里,,如果大家对高中所学的排列组合还很熟练的话,其实这就是高中排列组合经常考的体型,那么这总共会有多少种组合呢?会是kn种,在这个kn种有一种组合满足要求即可。理解了这个,我们再来看看,这个for循环写的是不是这个意思?对每个cur指针指向的nums数组里的数来讲,都要进行这个for循环,即对nums数组里的每个数来讲,k个桶要尝试一遍,只要当前尝试的第i个桶正好能放下当前的数或者放下当前的数后桶里的容量还有剩余,那么这个数就可以放在这个桶里,那么我们就可以进行对下一个数选择可以放入的桶了。
if(forEveryNum(nums,cur-1, arr,k)) return true; //只需要找到一个即可。
因为我们在进行递归函数之前,就进行了预处理,这个预处理是按升序排序,且能进入到递归函数时,就已经保证了数组对最后一个数是小于空桶的容量的,所以,对于最后一个数我们我们选择第一个桶总没有错,所以,我们可以固定第一个数放在第一个桶来来进行剩下的处理,那么总的组合数其实就变成了k^(n-1)
种,但是对于递归函数来讲,我们要涵盖k^(n)
种情况,因为递归函数是重复功能的形式化描述,所以我们没有必要固定第一个数放在第一个桶里,所以,从这个角度也可以理解,为什么这句代码if(forEveryNum(nums,cur-1, arr,k)) return true
是这么写的,if(forEveryNum(nums,cur-1, arr,k))
保证了如果我们找到了,即if(cur<0){ return true; }
。那么if的条件判断为true,说明我们总的函数就可以退出了,返回true即可。
不知道我有没有把我的思路说明白,如果还有些地方不理解,请移步IDE多debug几次。