个人笔记,仅学习交流用
题目:
给定一个整数数组 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] 范围内
解法:
首先知道输入有两个,一个是数组nums,一个是k,要将数组nums内的数分成k个总和相等的子集,则每个子集的总和应该为nums.Sum()/k,这个数字应该为整数,因此如果nums.Sum()%k不等于0,则必然不可能分成k个相等子集,求出ave=nums.Sum()/k。
之后我们要取nums中的任意位数,使其之和为ave,因为len(nums)<=16,因此可以使用一个整数S来表达当前nums数字的使用情况,用二进制来表示,令1为未使用,0为使用,这种方法叫做状态压缩动态规划,简称状压DP,例如示例1:
nums=[4,3,2,3,5,2,1],k=4,则ave=5,初始状态下其S=1111111(二进制,7个1,代表7个数字都未被使用,最右边代表第一位,这里通过位运算符(1<<n)-1来实现),为了让每种状态只被遍历一次,我们创建bool[] dp,令dp=false时跳过,每当遍历到一个新的状态后,令dp=false,dp的大小为1<<n,dp默认全为true。
第一步,【遍历】S中不为0的位,通过((s>>i)&1)!=0来判断对应位数是否为1,如取第一位,S=1111110(S=S^(1<<i),代表取走第i位),则可以得到p=nums[0]+p。p代表当前这个子集已经存储的数的总和,初始为0,p始终<=ave,因此如果p>ave,则跳过这一位。如果p=ave,则重置p=0,因此令p=(num[i]+p)%ave。
取完一位之后,我们便把问题从:
S=1111111,p=0
变成:
S=1111110,p=4
我们通过递归再取出S=1111110中的一位数,再次计算p,以此反复,直到最后求出S=0000000时返回true,如果不存在S=0000000情况,则再遍历完所有之后返回false
以下是代码:
public class Solution {
int[] nums;
int ave, n;
bool[] dp;
public bool CanPartitionKSubsets(int[] nums, int k) {
this.nums = nums;
int all = nums.Sum();
if (all % k != 0) {
return false;
}
ave = all / k;
Array.Sort(nums);
n = nums.Length;
if(nums[n-1]>ave) return false;
dp=new bool[1<<n];
Array.Fill(dp,true);
return DFS((1<<n)-1,0);
}
public bool DFS(int s, int p) {
if(s==0)return true;
if(!dp[s])return false;
dp[s]=false;
for(int i=0;i<n;i++){
if(nums[i]+p>ave)break;
if(((s>>i)&1)!=0){
if(DFS(s^(1<<i),(p+nums[i])%ave))return true;
}
}
return false;
}
}