C++二分查找或并集查找:交换得到字典序最小的数组

简介: C++二分查找或并集查找:交换得到字典序最小的数组

本文涉及的基础知识点

二分查找算法合集

题目

给你一个下标从 0 开始的 正整数 数组 nums 和一个 正整数 limit 。

在一次操作中,你可以选择任意两个下标 i 和 j,如果 满足 |nums[i] - nums[j]| <= limit ,则交换 nums[i] 和 nums[j] 。

返回执行任意次操作后能得到的 字典序最小的数组 。

如果在数组 a 和数组 b 第一个不同的位置上,数组 a 中的对应字符比数组 b 中的对应字符的字典序更小,则认为数组 a 就比数组 b 字典序更小。例如,数组 [2,10,3] 比数组 [10,2,3] 字典序更小,下标 0 处是两个数组第一个不同的位置,且 2 < 10 。

示例 1:

输入:nums = [1,5,3,9,8], limit = 2

输出:[1,3,5,8,9]

解释:执行 2 次操作:

  • 交换 nums[1] 和 nums[2] 。数组变为 [1,3,5,9,8] 。
  • 交换 nums[3] 和 nums[4] 。数组变为 [1,3,5,8,9] 。
    即便执行更多次操作,也无法得到字典序更小的数组。
    注意,执行不同的操作也可能会得到相同的结果。
    示例 2:
    输入:nums = [1,7,6,18,2,1], limit = 3
    输出:[1,6,7,18,1,2]
    解释:执行 3 次操作:
  • 交换 nums[1] 和 nums[2] 。数组变为 [1,6,7,18,2,1] 。
  • 交换 nums[0] 和 nums[4] 。数组变为 [2,6,7,18,1,1] 。
  • 交换 nums[0] 和 nums[5] 。数组变为 [1,6,7,18,1,2] 。
    即便执行更多次操作,也无法得到字典序更小的数组。
    示例 3:
    输入:nums = [1,7,28,19,10], limit = 3
    输出:[1,7,28,19,10]
    解释:[1,7,28,19,10] 是字典序最小的数组,因为不管怎么选择下标都无法执行操作。
    参数范围
    1 <= nums.length <= 105
    1 <= nums[i] <= 109
    1 <= limit <= 109

分析

时间复杂度

O(nlogn),枚举每个元素,每个枚举都需要二分查找。

代码

分析

setValue是nums按降序排序,如果一个数x1能替换比它大的数,那么一定存在一个比它大的数x2,且x2-x1 <= limit。setNot记录所有不存在x2的x1。

求一个数x能不替换成的最小数y:

在setValue中,y在x的右边。

setNot中 y在setNot.upper_bound(n)的左边

核心代码

class Solution {
public:
  vector<int> lexicographicallySmallestArray(vector<int>& nums, int limit) {
    std::multiset<int,std::greater<>> setValue(nums.begin(), nums.end());
    std::set<int, std::greater<>> setNot;
    for ( auto it = setValue.begin(); it != setValue.end(); ++it )
    {
       auto itNext = std::next(it);
       if ((setValue.end() != itNext) && (*it - *itNext > limit))
       {
         setNot.emplace(*itNext);
       }
    }
    for ( auto& n : nums)
    {
      auto it = setNot.upper_bound(n);
      int iEnd = (setNot.end() == it) ? -1 : *it;
      auto it2 = setValue.lower_bound(iEnd);
      if( setValue.begin() != it2 )     
      {
        n = *std::prev(it2);
        setValue.erase(setValue.find(n));
        continue;
      }
      setValue.erase(setValue.find(n));
      auto ij = setValue.lower_bound(n);
      if ((setValue.end() != ij) && (setValue.begin() != ij))
      {
        if (*std::prev(ij) - *ij > limit)
        {
          setNot.emplace(*ij);
        }
      }
    }
    return nums;
  }
};

测试用例

template
void Assert(const T& t1, const T& t2)
{
assert(t1 == t2);
}
template
void Assert(const vector& v1, const vector& 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 nums, res;
int limit;
{
nums = { 1, 5, 3, 9, 8 };
limit = 2;
Solution slu;
res = slu.lexicographicallySmallestArray(nums, limit);
Assert(res, vector{1, 3, 5, 8, 9});
}
{
nums = { 1, 7, 6, 18, 2, 1 };
limit = 3;
Solution slu;
res = slu.lexicographicallySmallestArray(nums, limit);
Assert(res, vector{1, 6, 7, 18, 1, 2});
}
{
nums = { 1, 7, 28, 19, 10 };
limit = 3;
Solution slu;
res = slu.lexicographicallySmallestArray(nums, limit);
Assert(res, vector{1, 7, 28, 19, 10});
}
//CConsole::Out(res);

}

并集查找

周赛时,没想到并集查找,用自己想的办法,每个细节都需要斟酌、尝试,非常花时间。建议尽量用现有算法。

分析

规则一:如果a,b能交换,b,c能交换,则a,b,c可以交换。

a b c
b a c
c a b
c b a

规则二:如果a b c能互换,c d 能互换,则a 和d 能互换。

a b c d
**c b a ** d
d b a c
d b c a

规则一,a b ,b c => 可以调整成任何顺序

规则二: a b,b c ,c d=>可以调整成任何顺序

规则三:增加a e可以互换

a ??? e
e ??? a
根据规则二,???a 可以换成任意顺序

总结

利用图论知识,a b能互换则a b连通,利用并集查找解决问题。同一个并集查找升序排序。

代码

class CUnionFind
{
public:
CUnionFind(int iSize) :m_vNodeToRegion(iSize)
{
for (int i = 0; i < iSize; i++)
{
m_vNodeToRegion[i] = i;
}
m_iConnetRegionCount = iSize;
}
int GetConnectRegionIndex(int iNode)
{
int& iConnectNO = m_vNodeToRegion[iNode];
if (iNode == iConnectNO)
{
return iNode;
}
return iConnectNO = GetConnectRegionIndex(iConnectNO);
}
void Union(int iNode1, int iNode2)
{
const int iConnectNO1 = GetConnectRegionIndex(iNode1);
const int iConnectNO2 = GetConnectRegionIndex(iNode2);
if (iConnectNO1 == iConnectNO2)
{
return;
}
m_iConnetRegionCount–;
if (iConnectNO1 > iConnectNO2)
{
UnionConnect(iConnectNO1, iConnectNO2);
}
else
{
UnionConnect(iConnectNO2, iConnectNO1);
}
}
bool IsConnect(int iNode1, int iNode2)
{
return GetConnectRegionIndex(iNode1) == GetConnectRegionIndex(iNode2);
}
int GetConnetRegionCount()const
{
return m_iConnetRegionCount;
}
vector GetNodeCountOfRegion()//各联通区域的节点数量
{
const int iNodeSize = m_vNodeToRegion.size();
vector vRet(iNodeSize);
for (int i = 0; i < iNodeSize; i++)
{
vRet[GetConnectRegionIndex(i)]++;
}
return vRet;
}
std::unordered_map<int, vector> GetNodeOfRegion()
{
std::unordered_map<int, vector> ret;
const int iNodeSize = m_vNodeToRegion.size();
for (int i = 0; i < iNodeSize; i++)
{
ret[GetConnectRegionIndex(i)].emplace_back(i);
}
return ret;
}
private:
void UnionConnect(int iFrom, int iTo)
{
m_vNodeToRegion[iFrom] = iTo;
}
vector m_vNodeToRegion;//各点所在联通区域的索引,本联通区域任意一点的索引,为了增加可理解性,用最小索引
int m_iConnetRegionCount;
};
class Solution {
public:
vector lexicographicallySmallestArray(vector& nums, int limit) {
m_c = nums.size();
auto vSort = nums;
sort(vSort.begin(), vSort.end());
CUnionFind uf(m_c);
for (int i = m_c - 1; i > 0; i–)
{
if (vSort[i] - vSort[i - 1] <= limit)
{
uf.Union(i, i - 1);
}
}
unordered_map<int, int> mValueToRegion;
unordered_map<int, vector> mReginToValues;
for (int i = 0; i < m_c; i++)
{
const int iRegion = uf.GetConnectRegionIndex(i);
mValueToRegion[vSort[i]] = iRegion;
mReginToValues[iRegion].emplace_back(vSort[i]);
}
for ( auto& [region, v] : mReginToValues)
{
std::sort(v.begin(), v.end(), std::greater<>());
}
for (int i = 0; i < m_c; i++)
{
const int iRegion = mValueToRegion[nums[i]];
nums[i] = mReginToValues[iRegion].back();
mReginToValues[iRegion].pop_back();
}
return nums;
}
int m_c;
};


/

优化

各连通区域是连续的,所以直接处理更简单。mValueToRegion[i] 记录第i个连通区域的数。比如:limit是4

14 12 8 3 1 ,可以分成{14 12 8} {3 1}.

代码

class Solution {
public:
vector lexicographicallySmallestArray(vector& nums, int limit) {
m_c = nums.size();
auto vSort = nums;
sort(vSort.begin(), vSort.end());
vector<vector> vGroupNum;
unordered_map<int, int> mValueToRegion;
for (int i = 0 ; i < m_c ; i++ )
{
if ((0 == i) || (vSort[i] - vSort[i - 1] > limit))
{
vGroupNum.emplace_back();
}
vGroupNum.back().emplace_back(vSort[i]);
mValueToRegion[vSort[i]] = vGroupNum.size() - 1;
}
for (auto& v : vGroupNum)
{
std::sort(v.begin(), v.end(), std::greater<>());
}
for (int i = 0; i < m_c; i++)
{
const int iRegion = mValueToRegion[nums[i]];
nums[i] = vGroupNum[iRegion].back();
vGroupNum[iRegion].pop_back();
}
return nums;
}
int m_c;
};

测试环境

操作系统:win7 开发环境: VS2019 C++17

或者 操作系统:win10 开发环境:

VS2022 C++17


相关文章
|
2月前
|
搜索推荐 编译器 C语言
【C++核心】特殊的元素集合-数组与字符串详解
这篇文章详细讲解了C++中数组和字符串的基本概念、操作和应用,包括一维数组、二维数组的定义和使用,以及C风格字符串和C++字符串类的对比。
81 4
|
2月前
|
C++
C++(十一)对象数组
本文介绍了C++中对象数组的使用方法及其注意事项。通过示例展示了如何定义和初始化对象数组,并解释了栈对象数组与堆对象数组在初始化时的区别。重点强调了构造器设计时应考虑无参构造器的重要性,以及在需要进一步初始化的情况下采用二段式初始化策略的应用场景。
|
3月前
|
算法 C++
c++学习笔记04 数组
这篇文章是C++学习笔记4,主题是数组。
43 4
|
3月前
|
C++ 索引
C++数组、vector求最大值最小值及其下标
C++数组、vector求最大值最小值及其下标
121 0
|
4月前
|
C++ 索引 运维
开发与运维数组问题之在C++中数组名和指针是等价如何解决
开发与运维数组问题之在C++中数组名和指针是等价如何解决
32 6
|
4月前
|
存储 C++ 容器
开发与运维数组问题之C++标准库中提供数据容器作为数组的替代如何解决
开发与运维数组问题之C++标准库中提供数据容器作为数组的替代如何解决
55 5
|
3月前
|
安全 编译器 C语言
C++入门-数组
C++入门-数组
|
5天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
25 5
|
11天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
40 4
|
12天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
36 4