作者推荐
【数位dp】【动态规划】【状态压缩】【推荐】1012. 至少有 1 位重复的数字
本文涉及知识点
状态压缩 记忆化搜索
1681. 最小不兼容性
给你一个整数数组 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
动态规划
对nums按升序排序。
动态规划的状态表示
pre[mask][end] 记录最小不兼容性和。mask表示nums中那些元素已经选择,选择的数优先放组号小的组。1组满了后,才放2组;2组满了,才放三组⋯ \cdots⋯
动态规划的转移方程
mask1 = mask | (1 << j )
end1 = j
j必须符合以下条件:
- j未被使用。
- 如果是某个组的首元素,可以选择任意元素。
- 如果不是某个组的首元素,j > end。且nums[j] != nums[end]
{ d p [ m a s k 1 ] [ j ] = d p [ m a s k ] [ e n d ] 某组的首元素 d p [ m a s k 1 ] [ j ] = d p [ m a s k ] [ e n d ] + n u m s [ j ] − n u m s [ e n d ] 非组首元素 \begin{cases} dp[mask1][j]= dp[mask][end] & 某组的首元素\\ dp[mask1][j]= dp[mask][end] + nums[j]-nums[end] & 非组首元素 \end{cases}{dp[mask1][j]=dp[mask][end]dp[mask1][j]=dp[mask][end]+nums[j]−nums[end]某组的首元素非组首元素
动态规划的初始值
dp[0][0]全部为0,其它全部为10000。
动态规划的填表顺序
mask从小到大,枚举前置条件。
动态规划的返回值
dp.back()的最小值。
代码
核心代码
class Solution { public: int minimumIncompatibility(vector<int>& nums, int k) { m_c = nums.size(); m_iMaskCount = 1 << m_c; sort(nums.begin(), nums.end()); vector<int> vBitCount(m_iMaskCount); for (int i = 1; i < m_iMaskCount; i++) { vBitCount[i] = 1 + vBitCount[i & (i - 1)]; } vector<vector<int>> dp(m_iMaskCount, vector<int>(m_c, m_iNotMay)); dp[0][0] = 0; for (int mask = 0; mask < m_iMaskCount; mask++) { bool bGroupFirst = (0 == vBitCount[mask] % (m_c / k)); for (int end = 0; end < m_c; end++) { for (int j = bGroupFirst ? 0 : (end + 1); j < m_c; j++) { if ((1 << j) & mask) { continue;//已经选择 } if ((nums[j] == nums[end])&& (!bGroupFirst)) { continue;//相同 } const int iNew = dp[mask][end] + (bGroupFirst ? 0 : (nums[j]-nums[end])); dp[mask | (1 << j)][j] = min(dp[mask | (1 << j)][j],iNew); } } } const int iRet = *std::min_element(dp.back().begin(), dp.back().end()); return (iRet >= m_iNotMay) ? -1 : iRet; } int m_c, m_iMaskCount,m_iNotMay=10000; };
测试用例
template<class T> void Assert(const T& t1, const T& t2) { assert(t1 == t2); } template<class T> void Assert(const vector<T>& v1, const vector<T>& v2) { if (v1.size() != v2.size()) { assert(false); return; } for (int i = 0; i < v1.size(); i++) { Assert(v1[i], v2[i]); } } int main() { vector<int> nums; int k; { Solution sln; nums = { 1 }, k = 1; auto res = sln.minimumIncompatibility(nums, k); Assert(res, 0); } { Solution sln; nums = { 1,1 }, k = 1; auto res = sln.minimumIncompatibility(nums, k); Assert(res, -1); } { Solution sln; nums = { 1, 2, 1, 4 }, k = 2; auto res = sln.minimumIncompatibility(nums, k); Assert(res, 4); } { Solution sln; nums = { 6, 3, 8, 1, 3, 1, 2, 2 }, k = 4; auto res = sln.minimumIncompatibility(nums, k); Assert(res, 6); } { Solution sln; nums = { 5,3,3,6,3,3 }, k = 3; auto res = sln.minimumIncompatibility(nums, k); Assert(res, -1); } { Solution sln; nums = { 11,11,3,4,2,16,14,13,6,14,2,5,10,13,5,7 }, k = 8; auto res = sln.minimumIncompatibility(nums, k); Assert(res, 12); } }
记忆化搜索+动态规划
从后置条件倒推前置条件,可以省去大量不必要的状态。运行速度提高500%。缺点可理解性大幅降低。
mask 选择了那些数,end 是最一个数。如果本组只有一个数,则最小不兼容性和就是 除本数外 的前几个完整的组的最小不兼容性和。
如果完整的组,最后一个元素一定是最大值。最大值一定是某个组的最后一个。将此组调到最后一组,结果不变。
EndZeroCount 从右到左为1的第一个下标(从0开始)。为了一致,nums降序排序。
每组一个元素要特殊处理。
int EndZeroCount(unsigned x ) { for (int i = 0; i < 32; i++) { if ((1 << i) & x) { return i; } } return 32; } class Solution { public: int minimumIncompatibility(vector<int>& nums, int k) { m_c = nums.size(); m_iMaskCount = 1 << m_c; m_pre = m_c / k; if (1 == m_pre) { return 0; } sort(nums.begin(), nums.end(),std::greater<>()); m_nums = nums; m_vBitCount.resize(m_iMaskCount); for (int i = 1; i < m_iMaskCount; i++) { m_vBitCount[i] = 1 + m_vBitCount[i & (i - 1)]; } m_dp.assign(m_iMaskCount, vector<int>(m_c, m_iNotMay)); const int iRet = Rec(m_iMaskCount-1); return (iRet >= m_iNotMay) ? -1 : iRet; } int Rec(int mask, int end) { if (0 == mask) { return 0; } auto& res = m_dp[mask][end]; if (m_iNotMay != res) { return res; } const int iPreMask = mask ^ (1 << end); const int cnt = m_vBitCount[mask] % m_pre;//最后一组数量 if (1 == cnt ) { return res = Rec(iPreMask); } for (int i = end+1 ; i < m_c; i++) { if ((1 << i) & mask) { if (m_nums[i] != m_nums[end]) { res = min(res, Rec(iPreMask,i)+ m_nums[end]-m_nums[i]); } } } return res; } int Rec(int mask) { return Rec(mask, EndZeroCount(mask)); } int m_c, m_iMaskCount,m_iNotMay=10000, m_pre; vector<int> m_vBitCount; vector<vector<int>> m_dp; vector<int> m_nums; };
2023年2月版
class Solution {
public:
int minimumIncompatibility(vector& nums, int k) {
m_c = nums.size();
if (k == m_c)
{
return 0;
}
if (1 == k)
{
std::set setNums(nums.begin(), nums.end());
if (nums.size() != setNums.size())
{
return -1;
}
return *setNums.rbegin() - *setNums.begin();
}
std::sort(nums.begin(),nums.end());
m_iMaskNum = (1 << m_c )*m_c;
m_vMaskByBits.resize(m_c + 1);
m_vMaskByBits[0].push_back(0);
vector vMaskBits(m_iMaskNum);
for (int mask = 1; mask < m_iMaskNum; mask++)
{
const int iSelMask = mask / m_c;
vMaskBits[mask] = vMaskBits[(iSelMask&(iSelMask - 1))m_c] + 1;
m_vMaskByBits[vMaskBits[mask]].push_back(mask);
}
m_vMaskGroupFirstToMin.resize(m_iMaskNum, m_iNotMay);
m_vMaskGroupFirstToMin[0] = 0;
for (int i = 0; i < nums.size(); i++)
{
vector dp(m_iMaskNum, m_iNotMay);
for (int iMask : m_vMaskByBits[i])
{
if (m_iNotMay == m_vMaskGroupFirstToMin[iMask])
{
continue;
}
const int iSelMask = iMask / m_c;
const int iPreSel = iMask% m_c;
if (0 == i % (m_c/k))
{//新组
for (int j = 0; j < m_c; j++)
{
if (iSelMask & (1 << j))
{
continue;
}
const int iNewMask = JoinMask(iSelMask | (1 << j), j);
dp[iNewMask] = min(dp[iNewMask], min(m_vMaskGroupFirstToMin[iMask],dp[iMask]));
}
}
else
{
for (int j = iPreSel+1; j < m_c; j++)
{
if (iSelMask & (1 << j))
{
continue;
}
const int iAdd = nums[j] - nums[iPreSel];
if (0 == iAdd)
{
continue;
}
const int iNewMask = JoinMask(iSelMask | (1 << j), j);
dp[iNewMask] = min(dp[iNewMask], min(m_vMaskGroupFirstToMin[iMask], dp[iMask]) + iAdd);
}
}
}
m_vMaskGroupFirstToMin.swap(dp);
}
std::set setRet;
for (int iPre = 0; iPre < m_c; iPre++)
{
int iIndex = (1 << m_c) - 1;
iIndex = iIndexm_c + iPre;
setRet.insert(m_vMaskGroupFirstToMin[iIndex]);
}
int iMin = setRet.begin();
return (m_iNotMay == iMin) ? -1 : iMin;
}
int JoinMask(int iSelMask, int iNewSelIndex)
{
return iSelMaskm_c + iNewSelIndex;
}
vector m_vMaskGroupFirstToMin;
int m_c;
int m_iMaskNum;
vector<vector> m_vMaskByBits;
const int m_iNotMay = 1000 * 1000;
};
2023年9月版
class Solution {
public:
int minimumIncompatibility(vector& nums, int k) {
m_c = nums.size();
if (k == m_c)
{
return 0;
}
m_iMaskNum = 1 << m_c;
if (0 != m_c % k)
{
return -1;
}
const int iNumOfAGroup = m_c / k;
vector<vector> vBitMask(m_c+1);
vBitMask[0].emplace_back(0);
for (int mask = 1; mask < m_iMaskNum; mask++)
{
vBitMask[bitcount((unsigned int)mask)].emplace_back(mask);
}
std::unordered_map<int, int> mMaskCom;
for (int mask : vBitMask[iNumOfAGroup])
{
int iMax = INT_MIN, iMin = INT_MAX;
unordered_set setValues;
for (int j = 0; j < m_c; j++)
{
if (mask & (1 << j))
{
MaxSelf(&iMax, nums[j]);
MinSelf(&iMin, nums[j]);
setValues.emplace(nums[j]);
}
}
if (setValues.size() != iNumOfAGroup)
{
continue;
}
mMaskCom[mask] = iMax - iMin;
}
int pre[1 << 16] = { 0 };
for (const auto& it : mMaskCom)
{
pre[it.first] = it.second;
}
for (int i = 2; i <= k; i++)
{
int dp[1 << 16] = { 0 };
for (const int& mask : vBitMask[iNumOfAGroup*i])
{
for (int sub = mask; sub; sub = (sub - 1) & mask)
{
if ((0 != pre[sub])&& mMaskCom.count(mask - sub))
{
int iNew = pre[sub] + mMaskCom[mask - sub];
if (0 != dp[mask])
{
iNew = min(iNew, dp[mask]);
}
dp[mask] = iNew;
}
}
}
memcpy(pre, dp, sizeof(dp));
}
return pre[m_iMaskNum - 1] ? pre[m_iMaskNum - 1] : -1;
}
int m_c, m_iMaskNum;
};