[LeetCode] 698. Partition to K Equal Sum Subsets
grandyang opened this issue · comments
Given an integer array nums
and an integer k
, return true
if it is possible to divide this array into k
non-empty subsets whose sums are all equal.
Example 1:
Input: nums = [4,3,2,3,5,2,1], k = 4
Output: true
Explanation: It's possible to divide it into 4 subsets (5), (1, 4), (2,3), (2,3) with equal sums.
Example 2:
Input: nums = [1,2,3,4], k = 3
Output: false
Constraints:
1 <= k <= nums.length <= 16
1 <= nums[i] <= 104
- The frequency of each element is in the range
[1, 4]
.
这道题给了一个数组 nums 和一个数字k,问我们该数字能不能分成k个非空子集合,使得每个子集合的和相同。给了k的范围是 [1,16],而且数组中的数字都是正数。这跟之前那道 Partition Equal Subset Sum 很类似,但是那道题只让分成两个子集合,所以问题可以转换为是否存在和为整个数组和的一半的子集合,可以用 dp 来做。但是这道题让求k个和相同的,感觉无法用 dp 来做,因为就算找出了一个,其余的也需要验证。这道题可以用递归来做,首先还是求出数组的所有数字之和 sum,首先判断 sum 是否能整除k,不能整除的话直接返回 false。然后需要一个 visited 数组来记录哪些数字已经被选中了,然后调用递归函数,目标是组k个子集合,且每个子集合之和为 target = sum/k。还需要变量 start,表示从数组的某个位置开始查找,curSum 为当前子集合之和,在递归函数中,如果 k=1,说明此时只需要组一个子集合,那么当前的就是了,直接返回 true。如果 curSum 等于 target 了,则再次调用递归,此时传入 k-1,start 和 curSum 都重置为0,因为当前又找到了一个和为 target 的子集合,要开始继续找下一个。否则的话就从 start 开始遍历数组,如果当前数字已经访问过了则直接跳过,否则标记为已访问。然后调用递归函数,k保持不变,因为还在累加当前的子集合,start 传入 i+1,curSum 传入 curSum+nums[i],因为要累加当前的数字,如果递归函数返回 true 了,则直接返回 true。否则就将当前数字重置为未访问的状态继续遍历,参见代码如下:
解法一:
class Solution {
public:
bool canPartitionKSubsets(vector<int>& nums, int k) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % k != 0) return false;
vector<bool> visited(nums.size());
return helper(nums, k, sum / k, 0, 0, visited);
}
bool helper(vector<int>& nums, int k, int target, int start, int curSum, vector<bool>& visited) {
if (k == 1) return true;
if (curSum == target) return helper(nums, k - 1, target, 0, 0, visited);
for (int i = start; i < nums.size(); ++i) {
if (visited[i]) continue;
visited[i] = true;
if (helper(nums, k, target, i + 1, curSum + nums[i], visited)) return true;
visited[i] = false;
}
return false;
}
};
我们也可以对上面的解法进行一些优化,比如先给数组按从大到小的顺序排个序,然后在递归函数中,可以直接判断,如果 curSum 大于 target 了,直接返回 false,因为题目中限定了都是正数,并且也给数组排序了,后面的数字只能更大,这个剪枝操作大大的提高了运行速度,感谢热心网友 hellow_world00 提供,参见代码如下:
解法二:
class Solution {
public:
bool canPartitionKSubsets(vector<int>& nums, int k) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % k != 0) return false;
sort(nums.begin(), nums.end(), greater<int>());
vector<bool> visited(nums.size(), false);
return helper(nums, k, sum / k, 0, 0, visited);
}
bool helper(vector<int>& nums, int k, int target, int start, int curSum, vector<bool>& visited) {
if (k == 1) return true;
if (curSum > target) return false;
if (curSum == target) return helper(nums, k - 1, target, 0, 0, visited);
for (int i = start; i < nums.size(); ++i) {
if (visited[i]) continue;
visited[i] = true;
if (helper(nums, k, target, i + 1, curSum + nums[i], visited)) return true;
visited[i] = false;
}
return false;
}
};
下面这种方法也挺巧妙的,思路是建立长度为k的数组v,只有当v里面所有的数字都是 target 的时候,才能返回 true。我们还需要给数组排个序,由于题目中限制了全是正数,所以数字累加只会增大不会减小,一旦累加超过了 target,这个子集合是无法再变小的,所以就不能加入这个数。实际上相当于贪婪算法,由于题目中数组数字为正的限制,有解的话就可以用贪婪算法得到。用一个变量 idx 表示当前遍历的数字,排序后,从末尾大的数字开始累加,遍历数组v,当前位置加上 nums[idx],如果超过了 target,跳过继续到下一个位置,否则就调用递归,此时的 idx 为 idx-1,表示之前那个数字已经成功加入数组v了,尝试着加下一个数字。如果递归返回 false 了,就将 nums[idx] 从数组v中对应的位置减去,还原状态,然后继续下一个位置。如果某个递归中 idx 等于 -1 了,表明所有的数字已经遍历完了,此时检查数组v中k个数字是否都为 target,是的话返回 true,否则返回 false,参见代码如下:
解法三:
class Solution {
public:
bool canPartitionKSubsets(vector<int>& nums, int k) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % k != 0) return false;
vector<int> v(k);
sort(nums.begin(), nums.end());
return helper(nums, sum / k, v, (int)nums.size() - 1);
}
bool helper(vector<int>& nums, int target, vector<int>& v, int idx) {
if (idx == -1) {
for (int t : v) {
if (t != target) return false;
}
return true;
}
int num = nums[idx];
for (int i = 0; i < v.size(); ++i) {
if (v[i] + num > target) continue;
v[i] += num;
if (helper(nums, target, v, idx - 1)) return true;
v[i] -= num;
}
return false;
}
};
Github 同步地址:
类似题目:
参考资料:
https://leetcode.com/problems/partition-to-k-equal-sum-subsets/
有一种利用bit位的dp做法,感觉很不错, 只是现在这些case都很小, 体现不了它的优点, 但是其中的dp**非常棒, 建议看一下, 补充一下这个帖子
有一种利用bit位的dp做法,感觉很不错, 只是现在这些case都很小, 体现不了它的优点, 但是其中的dp**非常棒, 建议看一下, 补充一下这个帖子
求贴代码~
位图的代码实现了一下 感觉好像leetcode提高了test数量还是啥的 现在的几个版本代码直接跑都会超时
class Solution {
public:
unordered_map<int, bool> memo;
bool canPartitionKSubsets(vector<int>& nums, int k) {
if(k > nums.size()) return false;
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % k != 0) return false;
int used = 0; // use bitmap
return helper(nums, k, sum / k, 0, 0, used);
}
bool helper(vector<int>& nums, int k, int target, int bucket, int start, int used) {
if(k == 0) // all buckets are filled
return true;
if(bucket == target) { // current bucket is filled
bool res = helper(nums, k - 1, target, 0, 0, used);
memo[used] = res;
return res;
}
if(memo.count(used))
return memo[used];
for(int i = start; i < nums.size(); ++i) {
if(((used >> i) & 1) == 1) { // check whether ith bit is 1
continue;
}
if(nums[i] + bucket > target) {
continue;
}
used |= 1 << i; // set ith bit to 1
bucket += nums[i];
if(helper(nums, k, target, bucket, i + 1, used))
return true;
bucket -= nums[i];
used ^= 1 << i; // set back ith bit to 0
}
return false;
}
};