【动态规划】【记忆化搜索】【状态压缩】1681. 最小不兼容性

简介: 【动态规划】【记忆化搜索】【状态压缩】1681. 最小不兼容性

作者推荐

【数位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 = iIndex
m_c + iPre;

setRet.insert(m_vMaskGroupFirstToMin[iIndex]);

}

int iMin = setRet.begin();
return (m_iNotMay == iMin) ? -1 : iMin;
}
int JoinMask(int iSelMask, int iNewSelIndex)
{
return iSelMask
m_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;

};


相关文章
|
7月前
|
算法 测试技术 C#
【贪心】【分类讨论】2499. 让数组不相等的最小总代价
【贪心】【分类讨论】2499. 让数组不相等的最小总代价
|
7月前
|
算法 JavaScript Java
【状态压缩】【动态规划】【C++算法】1125.最小的必要团队
【状态压缩】【动态规划】【C++算法】1125.最小的必要团队
|
7月前
|
消息中间件 Kubernetes NoSQL
动态规划-状态压缩、树形DP问题总结
动态规划-状态压缩、树形DP问题总结
|
算法
快排三种递归及其优化,非递归和三路划分
快排三种递归及其优化,非递归和三路划分
68 0
|
7月前
|
测试技术
【动态规划】【组合数学】1866. 恰有 K 根木棍可以看到的排列数目
【动态规划】【组合数学】1866. 恰有 K 根木棍可以看到的排列数目
|
7月前
|
算法
DAY-7 | 牛客-BM21 寻找旋转数组的最小元素:二分法分治思想真的很可靠
这是一个关于编程题目的摘要:题目是牛客网上的&quot;BM21 旋转数组的最小数字&quot;,要求找到旋转数组中的最小数字。题解介绍了使用二分查找算法来解决此问题,因为其时间复杂度优于暴力搜索的线性时间复杂度。二分查找的核心是通过比较中间元素与右端元素的大小,不断缩小搜索范围,最终找到最小值。代码示例展示了如何实现这个算法。总结中强调了二分查找适用于部分有序数组,并指出了解决这类问题的关键在于理解数组的二段单调性。
48 1
|
7月前
|
算法 测试技术 C++
【动态规划 状态机dp 性能优化】3098. 求出所有子序列的能量和
【动态规划 状态机dp 性能优化】3098. 求出所有子序列的能量和
【动态规划 状态机dp 性能优化】3098. 求出所有子序列的能量和
|
7月前
【编程题-错题集】连续子数组最大和(动态规划 - 线性 dp)
【编程题-错题集】连续子数组最大和(动态规划 - 线性 dp)
|
7月前
|
搜索推荐 算法 索引
【排序算法】深入解析快速排序(霍尔法&&三指针法&&挖坑法&&优化随机选key&&中位数法&&小区间法&&非递归版本)
【排序算法】深入解析快速排序(霍尔法&&三指针法&&挖坑法&&优化随机选key&&中位数法&&小区间法&&非递归版本)
201 4
|
7月前
|
算法 测试技术 C#
【状态压缩 动态规划 数论】1799. N 次操作后的最大分数和
【状态压缩 动态规划 数论】1799. N 次操作后的最大分数和

热门文章

最新文章