@@ -56,13 +56,13 @@ tags:
56
56
57
57
### 方法一:DFS + 剪枝
58
58
59
- 根据题意,我们需要将数组 ` nums ` 划分为 $k$ 个子集,且每个子集的和相等。因此,先累加 ` nums ` 中所有元素的和,如果不能被 $k$ 整除,说明无法划分为 $k$ 个子集,提前返回 ` false ` 。
59
+ 根据题意,我们需要将数组 $\textit{ nums}$ 划分为 $k$ 个子集,且每个子集的和相等。因此,先累加 $\textit{ nums}$ 中所有元素的和,如果不能被 $k$ 整除,说明无法划分为 $k$ 个子集,提前返回 $\textit{ false}$ 。
60
60
61
- 如果能被 $k$ 整除,不妨将每个子集期望的和记为 $s$,然后创建一个长度为 $k$ 的数组 ` cur ` ,表示当前每个子集的和。
61
+ 如果能被 $k$ 整除,不妨将每个子集期望的和记为 $s$,然后创建一个长度为 $k$ 的数组 $\textit{ cur}$ ,表示当前每个子集的和。
62
62
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] $ 的时候已经完成了搜索,也可以跳过当前的搜索。
64
64
65
- 如果能将所有元素都加入到 ` cur ` 中,说明可以划分为 $k$ 个子集,返回 ` true ` 。
65
+ 如果能将所有元素都加入到 $\textit{ cur}$ 中,说明可以划分为 $k$ 个子集,返回 $\textit{ true}$ 。
66
66
67
67
<!-- tabs:start -->
68
68
@@ -145,8 +145,7 @@ public:
145
145
s /= k;
146
146
int n = nums.size();
147
147
vector<int > cur(k);
148
- function<bool(int)> dfs;
149
- dfs = [ &] (int i) {
148
+ function<bool(int)> dfs = [ &] (int i) {
150
149
if (i == n) {
151
150
return true;
152
151
}
@@ -210,31 +209,32 @@ func canPartitionKSubsets(nums []int, k int) bool {
210
209
211
210
``` ts
212
211
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 ;
226
215
}
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 ;
230
219
}
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 ;
234
223
}
224
+ cur [j ] -= nums [i ];
235
225
}
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 ;
236
233
}
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 );
238
238
}
239
239
```
240
240
@@ -246,22 +246,22 @@ function canPartitionKSubsets(nums: number[], k: number): boolean {
246
246
247
247
### 方法二:状态压缩 + 记忆化搜索
248
248
249
- 与方法一相同,我们依然先判断数组 ` nums ` 是否有可能被划分为 $k$ 个子集。如果不能被 $k$ 整除,直接返回 ` false ` 。
249
+ 与方法一相同,我们依然先判断数组 $\textit{ nums}$ 是否有可能被划分为 $k$ 个子集。如果不能被 $k$ 整除,直接返回 $\textit{ false}$ 。
250
250
251
- 我们记 $s$ 为每个子集期望的和,当前元素被划分的情况为 ` state ` 。对于第 $i$ 个数,若 ` (( state >> i) & 1) ` 等于 $0$,说明第 $i$ 个元素未被划分。
251
+ 我们记 $s$ 为每个子集期望的和,当前元素被划分的情况为 $\textit{ state}$ 。对于第 $i$ 个数,若 $\textit{ state}$ 的第 $i$ 位为 $0$,说明第 $i$ 个元素未被划分。
252
252
253
253
我们的目标是从全部元素中凑出 $k$ 个和为 $s$ 的子集。记当前子集的和为 $t$。在未划分第 $i$ 个元素时:
254
254
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$ 实现),并继续划分下一个子集。
257
257
258
- 为了避免重复搜索,我们使用一个长度为 $2^n$ 的数组 ` f ` 记录每个状态下的搜索结果。数组 ` f ` 有三个可能的值:
258
+ 为了避免重复搜索,我们使用一个长度为 $2^n$ 的数组 $\textit{f}$ 记录每个状态下的搜索结果。数组 $\textit{f}$ 有三个可能的值:
259
259
260
260
- ` 0 ` :表示当前状态还未搜索过;
261
261
- ` -1 ` :表示当前状态下无法划分为 $k$ 个子集;
262
262
- ` 1 ` :表示当前状态下可以划分为 $k$ 个子集。
263
263
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)$。
265
265
266
266
<!-- tabs: start -->
267
267
@@ -355,8 +355,7 @@ public:
355
355
int n = nums.size();
356
356
int mask = (1 << n) - 1;
357
357
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) {
360
359
if (state == mask) {
361
360
return true;
362
361
}
@@ -428,6 +427,47 @@ func canPartitionKSubsets(nums []int, k int) bool {
428
427
}
429
428
```
430
429
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
+
431
471
<!-- tabs: end -->
432
472
433
473
<!-- solution: end -->
@@ -438,13 +478,13 @@ func canPartitionKSubsets(nums []int, k int) bool {
438
478
439
479
我们可以使用动态规划的方法求解本题。
440
480
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$ 时,最后一个子集的和。
442
482
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} $。
444
484
445
- 最后,我们返回 $f[ 2^n- 1] $ 即可。
485
+ 最后,我们返回 $f[ 2^n - 1] $ 即可。
446
486
447
- 时间复杂度 $O(n \times 2^n)$,空间复杂度 $O(2^n)$。其中 $n$ 表示数组 $nums$ 的长度。
487
+ 时间复杂度 $O(n \times 2^n)$,空间复杂度 $O(2^n)$。其中 $n$ 表示数组 $\textit{ nums} $ 的长度。
448
488
449
489
<!-- tabs: start -->
450
490
@@ -584,6 +624,38 @@ func canPartitionKSubsets(nums []int, k int) bool {
584
624
}
585
625
```
586
626
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
+
587
659
<!-- tabs: end -->
588
660
589
661
<!-- solution: end -->
0 commit comments