Leetcode P1681 最小不兼容性
给你一个整数数组 nums 和一个整数 k 。你需要将这个数组划分到 k 个相同大小的子集中,使得同一个子集里面没有两个相同的元素。一个子集的 不兼容性 是该子集里面最大值和最小值的差。请你返回将数组分成 k 个子集后,各子集 不兼容性 的 和 的 最小值 ,如果无法分成分成 k 个子集,返回 -1 。
子集的定义是数组中一些数字的集合,对数字顺序没有要求。
示例 1:
输入:nums = [1,2,1,4], k = 2
输出:4
解释:最优的分配是 [1,2] 和 [1,4] 。
不兼容性和为 (2-1) + (4-1) = 4 。
注意到 [1,1] 和 [2,4] 可以得到更小的和,但是第一个集合有 2 个相同的元素,所以不可行
示例 2:
输入:nums = [6,3,8,1,3,1,2,2], k = 4
输出:6
解释:最优的子集分配为 [1,2],[2,3],[6,8] 和 [1,3] 。
不兼容性和为 (2-1) + (3-2) + (8-6) + (3-1) = 6 。
示例 3:
输入:nums = [5,3,3,6,3,3], k = 3
输出:-1
解释:没办法将这些数字分配到 3 个子集且满足每个子集里没有相同数字。
提示:
- 1 <= k <= nums.length <= 16
- nums.length 能被 k 整除。
- 1 <= nums[i] <= nums.length
阿里周赛的最后一题。前三题做得很快很顺利,结果这个第四题居然没做出来。长度小于等于16,所以肯定贪心不了了。当时想用dfs回溯的方法,想先穷举第一次取的子集,然后穷举第二次的…这样显然会超时,也没想到什么好办法。看了零神的解法,知道了这种题的比较标准的解法。能用壮压dp还是用壮压好。
用dp[i]来表示,状态为i的最小不兼容性。i是二进制表示取的元素,比如i = 11, 对应二进制1011,意思就是取下标为0,1,3三个元素的时候,最小不兼容性是多少(针对所有子集的划分方法)。
根据最后一个子集的划分来写dp方程:
d
p
[
i
]
=
m
i
n
v
a
l
i
d
s
u
b
{
d
p
[
i
⊕
s
u
b
]
+
i
n
c
o
m
[
s
u
b
]
}
dp[i] = min_{valid sub}\{dp[i \oplus sub] + incom[sub]\}
dp[i]=minvalidsub{dp[i⊕sub]+incom[sub]}
穷举最后一个子集sub ,这个sub需要满足一些条件。
⊕
\oplus
⊕表示异或,i和sub的异或其实是从i中删去sub,剩余的元素的dp。 incom[sub]表示对于一个大小为
n
k
\frac{n}{k}
kn的集合的不兼容性。这个可以预处理出来。
思路就出来了,先预处理sub,然后对每一个dp[i],穷举最后一个子集,用上面的方程来计算。
class Solution {
public:
int minimumIncompatibility(vector<int>& nums, int k) {
int n = nums.size();
int dp[1 << n], incom[1 << n];
for(int i = 0; i < (1 << n); i++) {
if(__builtin_popcount(i) == n / k) {
int flag = 1, now = 0;
int sub[20];
for(int j = 0; j < n; j++) {
if((i >> j) & 1) {
sub[now++] = nums[j];
}
}
sort(sub, sub + now);
for(int j = 1; j < now; j++) {
if(sub[j] == sub[j - 1]) {
flag = 0;
break;
}
}
if(!flag) {
incom[i] = -1;
continue;
}
incom[i] = sub[now - 1] - sub[0];
} else {
incom[i] = -1;
}
}
dp[0] = 0;
for(int i = 1; i < (1 << n); i++) {
dp[i] = -1;
if(__builtin_popcount(i) % (n / k) == 0) {
for(int sub = i; sub; sub = (sub - 1) & i) {
if(dp[i ^ sub] != -1 && incom[sub] != -1) {
if(dp[i] == -1) {
dp[i] = dp[i ^ sub] + incom[sub];
} else {
dp[i] = min(dp[i], dp[i ^ sub] + incom[sub]);
}
}
}
}
}
return dp[(1 << n) - 1];
}
};
当时特别疑惑sub = (sub - 1) & i是什么。其实就是穷举子集的一种方式,并且删掉了一些不必要的情况。原来的话可能复杂度是 O ( 4 n ) O(4^n) O(4n),采用这种方式可以证明复杂度压缩到 O ( 3 n ) O(3^n) O(3n)。