【问题标题】:The amortized complexity of std::next_permutation?std::next_permutation 的摊销复杂度?
【发布时间】:2011-06-25 18:38:21
【问题描述】:

我刚刚阅读了this other question about the complexity of next_permutation,虽然我对响应 (O(n)) 感到满意,但似乎该算法可能有一个很好的摊销分析,显示出较低的复杂性。有人知道这样的分析吗?

【问题讨论】:

  • 较低的复杂性将要求您在每个分摊时间 o(1) 内返回元素。对于意味着您必须能够在 0 个时钟周期内返回一些元素的大型数据集。这是不可能的。
  • @btilly- 不一定。 一次调用 next_permutation 的复杂度为 O(n),而不是一系列调用。因此,我怀疑这可以在摊销 o(n) 中完成。
  • 只为算法迷发表评论。我在这里 (home.roadrunner.com/~hinnant/combinations.html) 对 next_permutation 进行了重新表述,称为 for_each_permutation,它的运行速度通常快 25% 到 50%,因为它不必进行比较即可找到下一次迭代。此实现还允许您一次置换 N 个项目 r。

标签: c++ algorithm stl big-o permutation


【解决方案1】:

所以看起来我会肯定地回答我自己的问题 - next_permutation 在 O(1) 摊销时间内运行。

在我对此进行正式证明之前,先快速回顾一下算法的工作原理。首先,它从范围的末尾向开头向后扫描,识别范围内以最后一个元素结束的最长连续递减子序列。例如,在0 3 4 2 1 中,算法会将4 2 1 识别为该子序列。接下来,它查看该子序列之前的元素(在上例中为 3),然后在子序列中找到比它大的最小元素(在上例中为 4)。然后,它交换这两个元素的位置,然后反转识别的序列。因此,如果我们从 0 3 4 2 1 开始,我们将交换 3 和 4 以生成 0 4 3 2 1,然后将最后三个元素反转以生成 0 4 1 2 3

为了证明该算法在摊销 O(1) 中运行,我们将使用潜在方法。将 Φ 定义为序列末尾最长连续递减子序列长度的三倍。在此分析中,我们将假设所有元素都是不同的。鉴于此,让我们考虑一下该算法的运行时间。假设我们从序列的末尾向后扫描,发现最后 m 个元素是递减序列的一部分。这需要 m + 1 次比较。接下来,我们找到该序列的元素中,哪个比该序列前面的元素大。在最坏的情况下,这需要与使用线性扫描的递减序列的长度成比例的时间进行另一 m 比较。例如,交换元素需要 1 个学分的时间,然后反转序列最多需要 m 次操作。因此,这一步的实际运行时间大约是 3m + 1。但是,我们必须考虑潜力的变化。在我们反转这个长度为 m 的序列之后,我们最终将范围末尾的最长递减序列的长度减少为长度 1,因为在末尾反转递减序列会使范围的最后一个元素按升序排序.这意味着我们的电位从 Φ = 3m 变为 Φ' = 3 * 1 = 3。因此,电位的净下降为 3 - 3m,因此我们的净摊销时间为 3m + 1 + (3 - 3m) = 4 = O(1)。

在前面的分析中,我做了一个简化的假设,即所有值都是唯一的。据我所知,这个假设是必要的,以使这个证明有效。我会考虑一下,看看是否可以修改证明以在元素可能包含重复项的情况下工作,一旦我完成了详细信息,我将发布对此答案的编辑。

【讨论】:

  • 奇怪,你比我早几秒钟发帖。我确认 O(1)!当然,文献声称有更好的算法,所以我不确定这是否正确。当然,他们可能在谈论常数,如果 n!参与其中。
  • @Moron- STL 还保证排列按字典顺序返回。如果你打破这个限制,你可能会做得更好。
  • 是的,事实上,我似乎记得有一种算法在每次调用时只进行一次交换。也许它和那个 wiki 页面所说的一样。
  • 在向量包含单个0n-1 1s的情况下试试这个算法;每次迭代显然需要 O(n) 次交换。另一方面,如果向量包含每个符号的 n/2,则需要 O(1) 次交换(常数是 4 而不是 e,如果所有元素都是不同的,则该常数是常数)。我很想找到可以使用此算法以恒定时间置换方式置换的向量分类,但到目前为止还没有运气。实验表明,如果向量包含 k 每个 m 符号,则交换次数会收敛到 e,但甚至不知道如何证明它。
【解决方案2】:

我不太确定 std::next_permutation 的确切实现,但如果它与此处 wiki 中描述的 Narayana Pandita 算法相同:http://en.wikipedia.org/wiki/Permutation#Systematic_generation_of_all_permutations

假设元素是不同的,看起来它是 O(1) 摊销的! (当然,下面可能会有错误)

让我们计算完成的交换总数。

我们得到递归关系

T(n+1) = (n+1)T(n) + Θ(n2)

(n+1)T(n) 来自于固定第一个元素并为剩余的 n 进行交换。

Θ(n2) 来自于改变第一个元素。在我们更改第一个元素时,我们进行 Θ(n) 交换。这样做 n 次,你得到 Θ(n2)。

现在让X(n) = T(n)/n!

然后我们得到

X(n+1) = X(n) + Θ(n2)/(n+1)!

即有一些常数 C 使得

X(n+1) 2/(n+1)!

写下这样的不等式给我们

X(n+1) - X(n) 2/(n+1)!

X(n) - X(n-1) 2/(n)!

X(n-1) - X(n-2) 2/(n-1)!

...

X(2) - X(1) 2/(1+1)!

把这些加起来给我们X(n+1) - X(1) <= C(\sum j = 1 to n (j^2)/(j+1)!)

由于无限级数\sum j = 1 to infinity j^2/(j+1)!收敛到C',比如说,我们得到X(n+1) - X(1) <= CC'

请记住,X(n) 计算所需的平均交换次数 (T(n)/n!)

因此平均交换次数为 O(1)。

由于找到要交换的元素与交换次数呈线性关系,因此即使考虑其他操作,它也是 O(1) 摊销的。

【讨论】:

  • 我不是反对者,但对于第二个等式,我能得到的最接近的是 X(n+1) = X(n)/(n+1) + Θ(n^2)/(n+1)!,即 RHS 的第一项应该除以 (n+1) methinks。我通过将T(n) 替换为X(n)n!,将T(n+1) 替换为X(n+1)(n+1)!,并将所有术语除以(n+1)! 来得到这个。
  • @j_r: 我们得到X(n+1)(n+1)! = (n+1)n!X(n) + theta(n^2)X(n+1) = X(n) + theta(n^2)/(n+1)!。您似乎忘记了 (n+1)T(n) 项中的 (n+1) 。无论如何,你得到的递归也意味着 X(n) = O(1)。
  • 但我仍然对下一步感到困惑——你能把它拼出来吗?另外,在我看来,显示 X(n) = O(1) 仅表明 T(n) = O(1)*n! -- 显然这很荒谬,我错过了什么?
  • @j_r:我已经添加了更多说明。此外,X(n) 计算平均交换次数,这是我们在考虑摊销复杂度时感兴趣的(至少在这种情况下)。之前有一个错字,其中 X(n) 据说是总数,而不是平均值。我也更正了。
【解决方案3】:

这里n 代表容器中元素的数量,而不是可能排列的总数。该算法必须在每次调用时遍历所有元素的顺序;它需要一对双向迭代器,这意味着要到达一个元素,算法必须首先访问它之前的那个(除非它的第一个或最后一个元素)。双向迭代器允许向后迭代,因此该算法可以(实际上必须)执行的交换次数是元素数量的一半。我相信该标准可以为前向迭代器提供重载,这将以n 交换而不是一半n 交换为代价来支持更笨的迭代器。但很可惜,它没有。

当然,对于n 可能的排列,算法在 O(1) 中运行。

【讨论】:

  • 也许我遗漏了一些东西,但是对于 n 个可能的排列,这在 O(1) 中如何工作?此外,这是否回答了关于摊销复杂性的原始问题?
  • 计算下一个排列独立于之后的排列或任何其他排列。因此,它是恒定的时间。
  • 是的,我确实回答了你原来的问题。不,这里没有比 O(n) 更好的摊销时间。
猜你喜欢
  • 2012-12-03
  • 2023-03-31
  • 2013-03-02
  • 2013-02-11
  • 1970-01-01
  • 2014-02-20
  • 2015-10-31
  • 2015-02-06
  • 1970-01-01
相关资源
最近更新 更多