698. 划分为k个相等的子集
一、题目描述
给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。
提示:
- 1 <= k <= len(nums) <= 16
- 0 < nums[i] < 10000
- 每个元素的频率在 [1,4] 范围内
二、解题思路
查找所有可能情况,可以采用回溯法。
回溯为了加快速度,还要加上快速剪枝和记忆搜索。
快速剪枝:
- 对数组进行排序,可以方便判断元素的前后大小
- 快速剪枝,只要当前
sum + nums[i] > target
,那么后面就不必遍历了
记忆搜索(状态去重):
数组有 n 位数,所有总共有 2 ^ n
中不同的状态,但是题目限制了 1 < n < 16
。
- 可以采用状态压缩的方式,用整数的低 16 位保存 n 位状态
- 压缩状态到整数表示以后,就能用数组
new boolean[1 << n]
进行状态去重
总的处理过程如下:
- 验证数组总和,是否可以均分成 k 组
sum(nums) % k
- 验证均值
sum(nums) / k
的有效性,即判断它是否不小于数组所有值
- 状态压缩,回溯遍历所有情况
- 回溯过程中,快速剪枝 + 记忆去重
三、复杂度分析
3.1 时间 O(n * 2^n)
总共 2^n
种状态,每种状态进行 n 轮判断,验证可能性。
3.2 空间 O(2^n)
记忆搜索需要 2^n
空间保存所有状态。
四、参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| int[] nums;
boolean[] cache;
int target = 0;
private boolean canPartitionKSubsets(int[] nums, int k) { int sum = Arrays.stream(nums).sum(); if (sum % k != 0) { return false; }
int avg = sum / k;
Arrays.sort(nums);
int n = nums.length; if (avg < nums[n - 1]) { return false; }
int state = (1 << n) - 1; this.nums = nums; this.target = avg; this.cache = new boolean[state + 1]; Arrays.fill(this.cache, true);
return dfs(state, 0); }
private boolean dfs(int state, int sum) { if (state == 0) { return true; } if (!cache[state]) { return false; }
int n = nums.length; for (int i = 0; i < n; i++) { boolean used = ((state >> i) & 1) == 0; if (used) { continue; } if (nums[i] + sum > target) { break; }
int nextState = state ^ (1 << i); int nextSum = (sum + nums[i]) % target;
if (dfs(nextState, nextSum)) { return true; } }
cache[state] = false; return false; }
|