【Leetcode】P1681 最小不兼容性

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[isub]+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)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值