Skip to content

Commit f7895bb

Browse files
authored
feat: add solutions to lc problem: No.0698 (#3447)
No.0698.Partition to K Equal Sum Subsets
1 parent 3e47705 commit f7895bb

File tree

8 files changed

+334
-93
lines changed

8 files changed

+334
-93
lines changed

solution/0600-0699/0698.Partition to K Equal Sum Subsets/README.md

Lines changed: 110 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ tags:
5656

5757
### 方法一:DFS + 剪枝
5858

59-
根据题意,我们需要将数组 `nums` 划分为 $k$ 个子集,且每个子集的和相等。因此,先累加 `nums` 中所有元素的和,如果不能被 $k$ 整除,说明无法划分为 $k$ 个子集,提前返回 `false`
59+
根据题意,我们需要将数组 $\textit{nums}$ 划分为 $k$ 个子集,且每个子集的和相等。因此,先累加 $\textit{nums}$ 中所有元素的和,如果不能被 $k$ 整除,说明无法划分为 $k$ 个子集,提前返回 $\textit{false}$
6060

61-
如果能被 $k$ 整除,不妨将每个子集期望的和记为 $s$,然后创建一个长度为 $k$ 的数组 `cur`,表示当前每个子集的和。
61+
如果能被 $k$ 整除,不妨将每个子集期望的和记为 $s$,然后创建一个长度为 $k$ 的数组 $\textit{cur}$,表示当前每个子集的和。
6262

63-
对数组 `nums` 进行降序排序(减少搜索次数),然后从第一个元素开始,依次尝试将其加入到 `cur` 的每个子集中。这里如果将 `nums[i]` 加入某个子集 `cur[j]` 后,子集的和超过 $s$,说明无法放入,可以直接跳过;另外,如果 `cur[j]``cur[j - 1]` 相等,意味着我们在 `cur[j - 1]` 的时候已经完成了搜索,也可以跳过当前的搜索。
63+
对数组 $\textit{nums}$ 进行降序排序(减少搜索次数),然后从第一个元素开始,依次尝试将其加入到 $\textit{cur}$ 的每个子集中。这里如果将 $\textit{nums}[i]$ 加入某个子集 $\textit{cur}[j]$ 后,子集的和超过 $s$,说明无法放入,可以直接跳过;另外,如果 $\textit{cur}[j]$$\textit{cur}[j - 1]$ 相等,意味着我们在 $\textit{cur}[j - 1]$ 的时候已经完成了搜索,也可以跳过当前的搜索。
6464

65-
如果能将所有元素都加入到 `cur` 中,说明可以划分为 $k$ 个子集,返回 `true`
65+
如果能将所有元素都加入到 $\textit{cur}$ 中,说明可以划分为 $k$ 个子集,返回 $\textit{true}$
6666

6767
<!-- tabs:start -->
6868

@@ -145,8 +145,7 @@ public:
145145
s /= k;
146146
int n = nums.size();
147147
vector<int> cur(k);
148-
function<bool(int)> dfs;
149-
dfs = [&](int i) {
148+
function<bool(int)> dfs = [&](int i) {
150149
if (i == n) {
151150
return true;
152151
}
@@ -210,31 +209,32 @@ func canPartitionKSubsets(nums []int, k int) bool {
210209

211210
```ts
212211
function canPartitionKSubsets(nums: number[], k: number): boolean {
213-
let s = nums.reduce((a, b) => a + b);
214-
if (s % k !== 0) {
215-
return false;
216-
}
217-
s /= k;
218-
nums.sort((a, b) => a - b);
219-
const n = nums.length;
220-
const f: boolean[] = new Array(1 << n).fill(false);
221-
f[0] = true;
222-
const cur: number[] = new Array(n).fill(0);
223-
for (let i = 0; i < 1 << n; ++i) {
224-
if (!f[i]) {
225-
continue;
212+
const dfs = (i: number): boolean => {
213+
if (i === nums.length) {
214+
return true;
226215
}
227-
for (let j = 0; j < n; ++j) {
228-
if (cur[i] + nums[j] > s) {
229-
break;
216+
for (let j = 0; j < k; j++) {
217+
if (j > 0 && cur[j] === cur[j - 1]) {
218+
continue;
230219
}
231-
if (((i >> j) & 1) === 0) {
232-
f[i | (1 << j)] = true;
233-
cur[i | (1 << j)] = (cur[i] + nums[j]) % s;
220+
cur[j] += nums[i];
221+
if (cur[j] <= s && dfs(i + 1)) {
222+
return true;
234223
}
224+
cur[j] -= nums[i];
235225
}
226+
return false;
227+
};
228+
229+
let s = nums.reduce((a, b) => a + b, 0);
230+
const mod = s % k;
231+
if (mod !== 0) {
232+
return false;
236233
}
237-
return f[(1 << n) - 1];
234+
s = Math.floor(s / k);
235+
const cur = Array(k).fill(0);
236+
nums.sort((a, b) => b - a);
237+
return dfs(0);
238238
}
239239
```
240240

@@ -246,22 +246,22 @@ function canPartitionKSubsets(nums: number[], k: number): boolean {
246246

247247
### 方法二:状态压缩 + 记忆化搜索
248248

249-
与方法一相同,我们依然先判断数组 `nums` 是否有可能被划分为 $k$ 个子集。如果不能被 $k$ 整除,直接返回 `false`
249+
与方法一相同,我们依然先判断数组 $\textit{nums}$ 是否有可能被划分为 $k$ 个子集。如果不能被 $k$ 整除,直接返回 $\textit{false}$
250250

251-
我们记 $s$ 为每个子集期望的和,当前元素被划分的情况为 `state`。对于第 $i$ 个数,若 `((state >> i) & 1)` 等于 $0$,说明第 $i$ 个元素未被划分。
251+
我们记 $s$ 为每个子集期望的和,当前元素被划分的情况为 $\textit{state}$。对于第 $i$ 个数,若 $\textit{state}$ 的第 $i$ 位为 $0$,说明第 $i$ 个元素未被划分。
252252

253253
我们的目标是从全部元素中凑出 $k$ 个和为 $s$ 的子集。记当前子集的和为 $t$。在未划分第 $i$ 个元素时:
254254

255-
- 若 $t + nums[i] \gt s$,说明第 $i$ 个元素不能被添加到当前子集中,由于我们对 `nums` 数组进行升序排列,因此数组 `nums` 从位置 $i$ 开始的所有元素都不能被添加到当前子集,直接返回 `false`
256-
- 否则,将第 $i$ 个元素添加到当前子集中,状态变为 `state | (1 << i)`,然后继续对未划分的元素进行搜索。需要注意的是,若 $t + nums[i] = s$,说明恰好可以得到一个和为 $s$ 的子集,下一步将 $t$ 归零(可以通过 `(t + nums[i]) % s` 实现),并继续划分下一个子集。
255+
- 若 $t + \textit{nums}[i] \gt s$,说明第 $i$ 个元素不能被添加到当前子集中,由于我们对 $\textit{nums}$ 数组进行升序排列,因此数组 $\textit{nums}$ 从位置 $i$ 开始的所有元素都不能被添加到当前子集,直接返回 $\textit{false}$
256+
- 否则,将第 $i$ 个元素添加到当前子集中,状态变为 $\textit{state} | 2^i$,然后继续对未划分的元素进行搜索。需要注意的是,若 $t + \textit{nums}[i] = s$,说明恰好可以得到一个和为 $s$ 的子集,下一步将 $t$ 归零(可以通过 $(t + \textit{nums}[i]) \bmod s$ 实现),并继续划分下一个子集。
257257

258-
为了避免重复搜索,我们使用一个长度为 $2^n$ 的数组 `f` 记录每个状态下的搜索结果。数组 `f` 有三个可能的值:
258+
为了避免重复搜索,我们使用一个长度为 $2^n$ 的数组 $\textit{f}$ 记录每个状态下的搜索结果。数组 $\textit{f}$ 有三个可能的值:
259259

260260
- `0`:表示当前状态还未搜索过;
261261
- `-1`:表示当前状态下无法划分为 $k$ 个子集;
262262
- `1`:表示当前状态下可以划分为 $k$ 个子集。
263263

264-
时间复杂度 $O(n\times 2^n)$,空间复杂度 $O(2^n)$。其中 $n$ 表示数组 $nums$ 的长度。对于每个状态,我们需要遍历数组 `nums`,时间复杂度为 $O(n)$;状态总数为 $2^n$,因此总的时间复杂度为 $O(n\times 2^n)$。
264+
时间复杂度 $O(n \times 2^n)$,空间复杂度 $O(2^n)$。其中 $n$ 表示数组 $\textit{nums}$ 的长度。对于每个状态,我们需要遍历数组 $\textit{nums}$,时间复杂度为 $O(n)$;状态总数为 $2^n$,因此总的时间复杂度为 $O(n\times 2^n)$。
265265

266266
<!-- tabs:start -->
267267

@@ -355,8 +355,7 @@ public:
355355
int n = nums.size();
356356
int mask = (1 << n) - 1;
357357
vector<int> f(1 << n);
358-
function<bool(int, int)> dfs;
359-
dfs = [&](int state, int t) {
358+
function<bool(int, int)> dfs = [&](int state, int t) {
360359
if (state == mask) {
361360
return true;
362361
}
@@ -428,6 +427,47 @@ func canPartitionKSubsets(nums []int, k int) bool {
428427
}
429428
```
430429

430+
#### TypeScript
431+
432+
```ts
433+
function canPartitionKSubsets(nums: number[], k: number): boolean {
434+
let s = nums.reduce((a, b) => a + b, 0);
435+
if (s % k !== 0) {
436+
return false;
437+
}
438+
s = Math.floor(s / k);
439+
nums.sort((a, b) => a - b);
440+
const n = nums.length;
441+
const mask = (1 << n) - 1;
442+
const f = Array(1 << n).fill(0);
443+
444+
const dfs = (state: number, t: number): boolean => {
445+
if (state === mask) {
446+
return true;
447+
}
448+
if (f[state] !== 0) {
449+
return f[state] === 1;
450+
}
451+
for (let i = 0; i < n; ++i) {
452+
if ((state >> i) & 1) {
453+
continue;
454+
}
455+
if (t + nums[i] > s) {
456+
break;
457+
}
458+
if (dfs(state | (1 << i), (t + nums[i]) % s)) {
459+
f[state] = 1;
460+
return true;
461+
}
462+
}
463+
f[state] = -1;
464+
return false;
465+
};
466+
467+
return dfs(0, 0);
468+
}
469+
```
470+
431471
<!-- tabs:end -->
432472

433473
<!-- solution:end -->
@@ -438,13 +478,13 @@ func canPartitionKSubsets(nums []int, k int) bool {
438478

439479
我们可以使用动态规划的方法求解本题。
440480

441-
我们定义 $f[i]$ 表示当前选取的数字的状态为 $i$ 时,是否存在 $k$ 个子集满足题目要求。初始时 $f[0]=true$,答案为 $f[2^n-1]$。其中 $n$ 表示数组 $nums$ 的长度。另外,我们定义 $cur[i]$ 表示当前选取的数字的状态为 $i$ 时,最后一个子集的和。
481+
我们定义 $f[i]$ 表示当前选取的数字的状态为 $i$ 时,是否存在 $k$ 个子集满足题目要求。初始时 $f[0]= true$,答案为 $f[2^n-1]$。其中 $n$ 表示数组 $nums$ 的长度。另外,我们定义 $cur[i]$ 表示当前选取的数字的状态为 $i$ 时,最后一个子集的和。
442482

443-
我们在 $[0,2^n)$ 的范围内枚举状态 $i$,对于每个状态 $i$,如果 $f[i]$ 为 `false`,我们直接跳过即可。否则,我们枚举 $nums$ 数组中的任意一个数 $nums[j]$,如果 $cur[i] + nums[j] \gt s$,我们直接跳出枚举循环,因为后面的数更大,无法放入当前子集;否则,如果 $i$ 的二进制表示的第 $j$ 位为 $0$,说明当前 $nums[j]$ 还没有被选取,我们可以将其放入当前子集中,此时状态变为 $i | 2^j$,并更新 $cur[i | 2^j] = (cur[i] + nums[j]) \bmod s$,并且 $f[i | 2^j] = true$。
483+
我们在 $[0, 2^n]$ 的范围内枚举状态 $i$,对于每个状态 $i$,如果 $f[i]$ 为 $\textit{false}$,我们直接跳过即可。否则,我们枚举 $\textit{nums}$ 数组中的任意一个数 $\textit{nums}[j]$,如果 $\textit{cur}[i] + \textit{nums}[j] > s$,我们直接跳出枚举循环,因为后面的数更大,无法放入当前子集;否则,如果 $i$ 的二进制表示的第 $j$ 位为 $0$,说明当前 $\textit{nums}[j]$ 还没有被选取,我们可以将其放入当前子集中,此时状态变为 $i | 2^j$,并更新 $\textit{cur}[i | 2^j] = (\textit{cur}[i] + \textit{nums}[j]) \bmod s$,并且 $f[i | 2^j] = \textit{true}$。
444484

445-
最后,我们返回 $f[2^n-1]$ 即可。
485+
最后,我们返回 $f[2^n - 1]$ 即可。
446486

447-
时间复杂度 $O(n \times 2^n)$,空间复杂度 $O(2^n)$。其中 $n$ 表示数组 $nums$ 的长度。
487+
时间复杂度 $O(n \times 2^n)$,空间复杂度 $O(2^n)$。其中 $n$ 表示数组 $\textit{nums}$ 的长度。
448488

449489
<!-- tabs:start -->
450490

@@ -584,6 +624,38 @@ func canPartitionKSubsets(nums []int, k int) bool {
584624
}
585625
```
586626

627+
#### TypeScript
628+
629+
```ts
630+
function canPartitionKSubsets(nums: number[], k: number): boolean {
631+
let s = nums.reduce((a, b) => a + b);
632+
if (s % k !== 0) {
633+
return false;
634+
}
635+
s /= k;
636+
nums.sort((a, b) => a - b);
637+
const n = nums.length;
638+
const f: boolean[] = Array(1 << n).fill(false);
639+
f[0] = true;
640+
const cur: number[] = Array(n).fill(0);
641+
for (let i = 0; i < 1 << n; ++i) {
642+
if (!f[i]) {
643+
continue;
644+
}
645+
for (let j = 0; j < n; ++j) {
646+
if (cur[i] + nums[j] > s) {
647+
break;
648+
}
649+
if (((i >> j) & 1) === 0) {
650+
f[i | (1 << j)] = true;
651+
cur[i | (1 << j)] = (cur[i] + nums[j]) % s;
652+
}
653+
}
654+
}
655+
return f[(1 << n) - 1];
656+
}
657+
```
658+
587659
<!-- tabs:end -->
588660

589661
<!-- solution:end -->

0 commit comments

Comments
 (0)