有效地找到不在大小为 40、400 或 4000 的集合中的整数

Posted

技术标签:

【中文标题】有效地找到不在大小为 40、400 或 4000 的集合中的整数【英文标题】:Efficiently find an integer not in a set of size 40, 400, or 4000 【发布时间】:2019-05-19 00:11:53 【问题描述】:

与经典问题find an integer not among four billion given ones相关但不完全相同。

为了澄清,整数我真正的意思只是其数学定义的一个子集。也就是说,假设只有有限数量的整数。说在 C++ 中,它们是int,在[INT_MIN, INT_MAX] 的范围内。

现在给定一个std::vector<int>(不重复)或std::unordered_set<int>,其大小可以是40、400、4000左右,但不能太大,如何高效生成一个保证不在给定中的数字那些?

如果不担心溢出,那么我可以将所有非零值相乘,然后将乘积加 1。但确实存在。攻击者的测试用例可能故意包含INT_MAX

我更赞成简单、非随机的方法。有吗?

谢谢!

更新:为了消除歧义,假设一个未排序的 std::vector<int> 保证没有重复项。所以我问是否有比 O(n log(n)) 更好的东西。另请注意,测试用例可能同时包含INT_MININT_MAX

【问题讨论】:

对向量进行排序可以在O(n log(n))中完成,不知道能不能得到更高效的方法 向量排序了吗?对于集合,它是微不足道的,因为它是排序的。 @user463035818 如果你能对O(log(n))中的一个向量进行排序,你确实应该变得很富有了。 @Walter meh 这是一个错字,不幸的是我没有数百万 排序后,你会:Last + 1? 【参考方案1】:

随机生成 x (INT_MIN..INT_MAX) 并针对所有人进行测试。在失败时测试 x++(40/400/4000 的情况非常罕见)。

【讨论】:

他正在寻找一种有效的方法来做到这一点,这将是 O(n^2)。是的,大多数时候它会好得多,但在最坏的情况下仍然非常低效(并且 OP 特别要求 O,这是最坏的情况) @dquijada 不,不是。大 O 表示法描述了函数的渐近行为,但它确实意味着它是最坏的情况。换句话说,一个算法可能有不同的上限,具体取决于您正在研究的输入。 @Acorn 通常使用 big-O 符号来描述算法的最坏情况时间复杂度,即对于 所有 输入可能具有的最坏时间。当然,输入的一个子集可能会以较小的复杂性运行,但最坏情况意味着考虑所有可能性。 Big-O 也可以用来描述平均-case 复杂度。但是,据我所知,“这个算法需要 O(f(n))”这个句子的“默认含义”是 O(f(n)) 是 worst-case情景,而不是一般情况。 @Bakuriu Big O 表示法与分析的复杂性类型无关。它甚至可能不是时间复杂度。当然,您可以争辩说,人们可能假设我们谈论的是最坏情况的时间复杂度,dquijada 说“OP 专门要求 O,这是最坏的情况 ",这根本不是真的。 OP 没有具体说明最坏情况(恰恰相反:我们只能假设),“O”也没有表示最坏情况(这根本不是真的)。​​ 此外,在这个特定问题中,这个解决方案实际上在实践中非常有效,因为你几乎从来没有遇到最坏的情况(和/或如果你检测到你可能遇到了,你可以简单地回退到另一个解决方案它,例如经过几次尝试;很容易给你一个最坏情况的 O(n) 算法)。因此,通过说它是 O(n^2) 并声称 OP 想要一个好的最坏情况算法来驳斥这个解决方案是完全错误的。【参考方案2】:

第 1 步: 对向量进行排序

这可以在 O(n log(n)) 内完成,你可以在网上找到几种不同的算法,使用你最喜欢的一种。

第 2 步: 找到不在向量中的第一个 int

轻松地从 INT_MIN 迭代到 INT_MIN + 40/400/4000 检查向量是否具有当前 int:

伪代码:

SIZE = 40|400|4000 // The one you are using
for (int i = 0; i < SIZE; i++) 
    if (array[i] != INT_MIN + i)
        return INT_MIN + i;

解决方案是 O(n log(n) + n) 意思是:O(n log(n))


编辑: 只是阅读您的编辑要求O(n log(n)) 更好的东西,对不起。

【讨论】:

感谢您的回答。一个小问题:虽然它是伪代码,但当iINT_MIN 开始时,array[i] 似乎没有任何意义。 您可以使用 O(log N) 中的二进制搜索进行搜索。 @rici:您将如何对“丢失的数字”进行二进制搜索?如果您知道丢失的数字,您可以在 log(n) 时间内在排序数组中找到它所属的位置。我认为你不能比线性做得更好,尽管可能具有较低的常数因子。在均匀分布的情况下,线性搜索平均会在O(n / max_n) 时间出现差距,但最坏的情况是n(直到最后都没有差距。) 如果您使用选择排序进行排序,您可以即时检查间隙,并且很有可能会在第一遍或第二遍中找到不重复的内容。 (特别是如果您还要检查随机猜测候选者。)对于小尺寸,O(n) 表示法不太有意义/有用。 @peter:与进行线性搜索的方式相同,将i + minvec[i] 进行比较。如果不相等,则i前面有缺失值,所以你一分为二;否则你一分为二。二分搜索适用于任何单调谓词。【参考方案3】:

随机方法在这里确实非常有效。

如果我们想使用确定性方法,并且假设大小 n 不太大,例如 4000,那么我们可以创建一个大小为 @ 的向量 x 987654321@(或者大一点,比如4096,方便计算),初始化为0。

对于范围内的每个i,我们只需设置 x[array[i] modulo m] = 1。

然后在 x 中进行简单的 O(n) 搜索将提供一个不在 array

中的值

注意:模运算不完全是“%”运算

编辑:我提到在这里选择 4096 的大小会使计算变得更容易。更具体地说,这意味着模运算是通过简单的&amp; 运算来执行的

【讨论】:

【参考方案4】:

您可以只返回输入中未包含的N+1 候选整数中的第一个。最简单的候选者是数字0N。这需要O(N) 空间和时间。

 int find_not_contained(container<int> const&data)
 
     const int N=data.size();
     std::vector<char> known(N+1, 0);   // one more candidates than data
     for(int i=0; i< N; ++i)
         if(data[i]>=0 && data[i]<=N)
             known[data[i]]=1;
     for(int i=0; i<=N; ++i)
         if(!known[i])
             return i;
     assert(false);                     // should never be reached.
 

随机方法可以更节省空间,但在最坏的情况下可能需要更多的数据传递。

【讨论】:

我喜欢这个。它避免了排序和搜索。 这是查找不在给定集合中的最小整数的标准算法 @DreamConspiracy 这是有道理的。不过我不知道。 有趣的事实:在具有分散存储的机器上,例如带有 AVX512F 的 x86,该算法可以向量化。 (至少对于 x86 样式的散点图,其中掩码控制实际散布的元素)。与直方图问题不同的是,您必须对命中相同索引的多个矢量元素进行冲突检测,您只需要存储一个 1 就可以了。 (x86 只能以 32 位或 64 位粒度散布,因此您必须将 known 提升为 int,对于较大的 N,这可能不值得。如果 N 足够大,您将想要使用位图而不是 char 数组。) 最后的搜索可以很容易地矢量化,使用适用于手动优化strlen 的任何技巧。 (例如,现代 x86 应该能够检查每个时钟周期 16 到 64 个字节,并使用 SSE2 或 AVX2 进行良好调整的循环,具体取决于硬件。)【参考方案5】:

如果允许使用以下算法对输入向量重新排序,则可以使用 O(1) 辅助空间在 O(N) 时间内找到最小的未使用整数。 [注1](如果向量包含重复数据,该算法也有效。)

size_t smallest_unused(std::vector<unsigned>& data) 
  size_t N = data.size(), scan = 0;
  while (scan < N) 
    auto other = data[scan];
    if (other < scan && data[other] != other) 
      data[scan] = data[other];
      data[other] = other;
    
    else
      ++scan;
  
  for (scan = 0; scan < N && data[scan] == scan; ++scan)  
  return scan;

第一遍保证如果[0, N) 范围内的一些k 在位置k 之后找到,那么它现在出现在位置k。这种重新排列是通过交换来完成的,以避免丢失数据。扫描完成后,值与其索引不同的第一个条目不会在数组中的任何位置引用。

该断言可能不是 100% 明显的,因为可以从较早的索引中引用条目。但是,在这种情况下,条目不能是不等于其索引的第一个条目,因为较早的条目将满足该标准。

要看到这个算法是 O(N),应该观察到第 6 行和第 7 行的交换只有在目标条目不等于其索引时才会发生,并且在交换之后目标条目相等到它的索引。所以最多可以执行N 交换,并且第5 行的if 条件将是true 最多N 次。另一方面,如果if 条件为假,scan 将递增,这也只会发生N 次。所以if 语句最多被评估2N 次(即O(N))。


注意事项:

    我在这里使用了无符号整数,因为它使代码更清晰。该算法可以很容易地针对有符号整数进行调整,例如通过将有符号整数从 [INT_MIN, 0) 映射到无符号整数 [INT_MAX, INT_MAX - INT_MIN) (减法是数学的,而不是根据不允许表示结果的 C 语义。) 2 的补码,这是相同的位模式。当然,这会改变数字的顺序,这会影响“最小未使用整数”的语义;也可以使用保序映射。

【讨论】:

要处理 OP 的要求,最小的正数未使用就足够了,因此处理有符号整数的更简单方法是在数字为负数时不交换。 @taemyr:如果您使用负整数的“2 的补码”映射会发生这种情况,这不需要额外的测试(假设 C 中的强制转换是无操作的)。通常,如果您想在某个范围内找到最小的可用数字,您可以忽略该范围之外的任何条目,并从有用的条目中减去该范围的开头。我打算添加这个观察结果,但我最感兴趣的是原始算法的优雅。【参考方案6】:

对于在std::unordered_set&lt;int&gt;(而不是std::vector&lt;int&gt;)中提供整数的情况,您可以简单地遍历整数值的范围,直到遇到一个不存在于unordered_set&lt;int&gt;。在std::unordered_set&lt;int&gt; 中搜索整数的存在非常简单,因为std::unodered_set 确实提供了通过其find() 成员函数进行的搜索。

这种方法的空间复杂度将是 O(1)


如果您从int(即std::numeric_limits&lt;int&gt;::min())的最低可能值开始遍历,您将获得最低int未包含在std::unordered_set&lt;int&gt;:

int find_lowest_not_contained(const std::unordered_set<int>& set) 
   for (auto i = std::numeric_limits<int>::min(); ; ++i) 
      auto it = set.find(i); // search in set
      if (it == set.end()) // integer not in set?
         return *it;
   

类似地,如果您从int(即std::numeric_limits&lt;int&gt;::max())的最大可能值开始遍历,您将获得最低int而不是包含在std::unordered_set&lt;int&gt;:

int find_greatest_not_contained(const std::unordered_set<int>& set) 
   for (auto i = std::numeric_limits<int>::max(); ; --i) 
      auto it = set.find(i); // search in set
      if (it == set.end()) // integer not in set?
         return *it;
   


假设ints被散列函数一致映射到unordered_set&lt;int&gt;的桶中,则可以在恒定时间内完成对unordered_set&lt;int&gt;的搜索操作。运行时复杂度将是 O(M ),其中 M 是您正在寻找非包含值的整数范围的大小。 M 的上限为 unordered_set&lt;int&gt; 的大小(即在您的情况下为 M )。

确实,通过这种方法,选择任何大于unordered_set 大小的整数范围,都可以保证遇到unordered_set&lt;int&gt; 中不存在的整数值。

【讨论】:

以上是关于有效地找到不在大小为 40、400 或 4000 的集合中的整数的主要内容,如果未能解决你的问题,请参考以下文章

有效地找到最近的字典键

Spark DataSet 有效地获取整行的长度大小

java有效地获取文件大小

有效地在矩阵中找到其元素总和为目标数的路径? [关闭]

SKSpriteNode()纹理的大小是否有限制? (4000 x 4000)

如何在大数据帧的每组中有效地随机标记行?