// 优化四:指令集优化,让CPU使用POPCNT指令,从而加速__builtin_popcount
#pragma GCC target ("sse4.2")
__builtin_popcount(state) == sz; 该指令计算state中二进制1的个数
5619. 最小不兼容性
给你一个整数数组 nums 和一个整数 k 。你需要将这个数组划分到 k 个相同大小的子集中,使得同一个子集里面没有两个相同的元素。 一个子集的 不兼容性 是该子集里面最大值和最小值的差。 请你返回将数组分成 k 个子集后,各子集 不兼容性 的 和 的 最小值 ,如果无法分成分成 k 个子集,返回 -1 。 子集的定义是数组中一些数字的集合,对数字顺序没有要求。
1 <= k <= nums.length <= 16
nums.length
能被k
整除。1 <= nums[i] <= nums.length
思路
- N有限定范围,即,显然可以使用状态压缩来表示数组所有的子集(状态),总状态数为.递归求解每个状态数所对应的最小不兼容性,最后可得总集即状态数为状态数的最小不兼容性。
在枚举分组方案时,若朴素的递归,总复杂度
- 采用分治,即,尝试所有可能将数组分成两堆的方案,并对左堆和右堆递归进行求解,为了避免重复计算,需要使用记忆化。
- 如何将数组尝试所有可能分成两堆?
对于某个状态state的所有位来说,仅有当前位为1的情况下需要考虑。
- `state & (state - 1)`:`state`从左到右第一个1为界,将其分为两堆。有
- `leftState=state & (state-1)`
- `rightState=leftState^state`
- `(leftState - 1) & state`:`state`从左往右第二个1位界,将其分为两堆。`rightState`求法如上。
- 递归终止条件
当前状态state中1的个数与规定子集长度相同时,判断该子集是否符合条件,若不符合,则设置其不兼容性为INF,否则求得其不兼容性。
对于优化条件,可使用5种优化来优化上述递归程序。
- 判断是否存在不兼容性
- 子集长度=1
- 数组中有元素数量>k
- 记忆化
记录每个状态的最小不兼容性。
- 子集枚举优化
- 若state中1的个数不能整数k,则该子集不具备不兼容性,continue
- 若求得leftState的不兼容性已>当前总和的最优值,显然该状态已不具备最优,continue
- 排序
不兼容性=集合中最大值和最小值只差,故将整个数组排序,所有子集均为排序数组,则每个子集的不兼容性即为首尾相减的值
算法
设N=nums.size(),sz=k/N; 记忆化列表为memo[]=-1
- recur(state):
- 当前状态是否在记忆化列表中,
memo[state]!=-1
- 当前子集长度是否等于sz。
- 得到对应的子集,并判断子集中是否有重复元素
- 若无,则更新
memo[state]=subset[sz-1]-subset[0]
- 遍历所有分成两堆的状态,并求解左右两堆
- 当前状态是否在记忆化列表中,
- 主函数:
const int INF = 0x3f3f3f3f; int memo[65536], v[16];
class Solution {
int n, k, sz;
vector
// 边界条件:当前集合刚好包含n/k个元素,不需要继续划分
if (__builtin_popcount(state) == sz) {
//__builtin_popcount:计算二进制1个数
for (int i = 0,idx=0; i < n; ++i)
if (state & (1 << i))//subset数组元素索引,用v来存储
v[idx++] = i;
//判断v中是否存在相同元素,若存在则为INF;
for (int i = 0; i + 1 < sz; ++i)
if (nums[v[i]] == nums[v[i + 1]])
return memo[state] = INF;
//否则则为首尾相减
return memo[state] = nums[v[sz - 1]] - nums[v[0]];
}
int ans = INF;
//如下为重点!!!
// 优化二:子集枚举优化
for (int sub = state & (state - 1); sub; sub = (sub - 1) & state) {
if (__builtin_popcount(sub) % sz != 0)
continue;
int left = solve(sub);
// 优化三:如果左边的最优值已经达到了当前总和的最优值,则不需要继续计算右边。
if (left >= ans)
continue;
int right = solve(state ^ sub);
ans = min(ans, left + right);
}
return memo[state] = ans;
}
public:
int minimumIncompatibility(vector
// 优化一:每个子集的大小为1,不兼容性显然为0,总和也是0。
// 这种情况的筛除非常重要,因为它需要最多的枚举次数。
if (sz == 1)
return 0;
sort(nums.begin(), nums.end());
//判断不兼容性是否存在
vector<int> cnt(n + 1);
for (int num : nums) {
cnt[num]++;
if (cnt[num] > k)
return -1;
}
this->nums = nums;
for (int i = 0; i < (1 << n); ++i)
memo[i] = -1;
return solve((1 << n) - 1);
}
};
复杂度分析
- 时间复杂度:遍历所有的状态,即[1,….,stateMax],此复杂度为,在每个状态递归(对每个状态分组)中,最糟糕的情况下,复杂度为,故整体复杂度为
- 空间复杂度:存储16位的所有状态需要