Push_swap:在两个可旋转堆栈上使用一组有限的指令对给定值进行排序的最有效方法是什么?

Push_swap: what is the most efficient way to sort given values using a limited set of instructions on two rotatable stacks?

提问人:Emperor_Udan 提问时间:1/13/2023 最后编辑:trincotEmperor_Udan 更新时间:11/9/2023 访问量:2508

问:

对于学校 42 的 push_swap 项目,我有一个较差的解决方案:

您可以使用一组 int 值、2 个堆栈和一组指令来操作这两个堆栈。

用 C 语言编写 [a program]:

[...]调用,它使用对收到的整数参数进行排序的指令语言计算并在标准输出上显示最小的程序:[...]push_swapPush_swap

  • sa: swap - 交换堆栈顶部的前 2 个元素。如果只有一个元素或没有元素,则不执行任何操作)。aa
  • sb: swap - 交换堆栈顶部的前 2 个元素。如果只有一个元素或没有元素,则不执行任何操作)。bb
  • ss:同时。sasb
  • pa: push - 将第一个元素放在 的顶部,并将其放在 的顶部。如果为空,则不执行任何操作。abab
  • pb: push - 将第一个元素放在 的顶部,并将其放在 的顶部。如果为空,则不执行任何操作。baba
  • ra: rotate - 将堆栈的所有元素上移 1。第一个元素成为最后一个元素。aa
  • rb: rotate - 将堆栈的所有元素上移 1。第一个元素成为最后一个元素。bb
  • rr:同时。rarb
  • rra:反向旋转 - 将堆栈的所有元素向下移动 1。最后一个元素成为第一个元素。aa
  • rrb:反向旋转 - 将堆栈的所有元素向下移动 1。最后一个元素成为第一个元素。bb
  • rrr:同时。rrarrb

我知道一个几乎与这里已经问过的问题相同的问题, 但我没有找到 提供的答案非常有帮助,因为练习的目标在问题中没有明确说明(在其原始版本中)。

我设计了一个简单的算法来解决上面描述的练习,但它太慢了。我怎样才能改进它或 设计更有效的算法?

本练习的目标是找到堆栈指令的最短列表,以便在遵循时对堆栈 A 进行排序。最重要的是列表的大小,而不是我们用来查找此类列表的算法的复杂性。

算法

现在我已经实现了这个非常简单的算法:

  • 收集数组中的所有数字并对其进行排序,使 最小的数字位于索引 0 处。
  • 取排序数组中的第一个数字,我们称之为 x。我们需要将 x 移动到堆栈的顶部,然后将其推送到 B,以便:
    • 如果 x 处于第二位,则交换。
    • 如果 x 更靠近堆栈的顶部,请旋转直到 x 位于顶部。
    • 如果 x 更靠近堆栈的底部,则反向直到 x 位于顶部。
  • 每次操作后,检查堆栈是否已排序。
    • 如果不是,则将堆栈的第一个元素推到 B 上,取数组中的下一个元素并重复。
  • 当 A 中只剩下两个元素时,检查它们是否被排序,如果没有交换它们。
  • 将 B 中的所有元素推回 A。

当 n 变小时,这个算法效果很好,但当 n 变大时,时间太长了。平均而言,我得到:

  • n = 10 时有 30 条指令。
  • n = 100 时有 2500 条指令。
  • n = 500 时有 60000 条指令。
  • n = 10000 时有 250000 条指令。

我想在 n = 500 时低于 5000 步,在 n = 100 时低于 500 步。

算法 排序 堆栈 与语言无关的 推送交换

评论

1赞 Eugene Sh. 1/13/2023
这并不是一个真正的 C 编程问题,因此提供的 C 代码是完全无关紧要的。
0赞 Emperor_Udan 1/13/2023
这是真的,但我认为最好提供尽可能多的细节,而不是太少,不得不编辑帖子以添加更多内容。
2赞 Eugene Sh. 1/13/2023
顺便说一句,您的操作列表不包含任何排序所必需的比较/条件操作。问题描述似乎不完整
1赞 btilly 1/14/2023
由于问题发生了变化,我更新了我的解决方案,提出了一些进一步优化的想法。根据信封背面的估计,我认为 6000 步是可行的。
1赞 rcgldr 1/15/2023
如果这是来自的挑战,则应更改标题和问题。虽然叫栈,但不是栈,而是数组,允许拷贝到外部数组,可以使用其他数组。例如,使用第二个外部索引数组来生成一组排序的索引。堆栈不能轮换,但队列、循环数组或循环链表可以轮换。push swapschool 42

答:

2赞 btilly 1/13/2023 #1

这是您已经拒绝的 https://stackoverflow.com/a/38165541/585411 的变体。但希望你能理解我对如何更好地进行自下而上的合并排序的解释。

运行是按排序顺序排列的一组数字。起初,你有很多次运行,大概大多数都是小长度的。当你在堆栈中运行一次时,你就完成了。A

首先,当底部元素是顶部时,继续向后旋转。这会将运行的开始位置在 的顶部。A<=A

接下来,我们需要在 和 之间平均分配运行。我们的做法是经历一次,寻找运行。第一次运行在 的底部,第二次运行在 的底部,依此类推。(放置在底部只需要,直到运行完成。放在底部意味着 .)ABAABAraBpbrb

一旦我们拆分了运行,我们要么只是在底部放置一个运行,并且比 多一个运行,要么我们只是在底部放置一个运行,并且它们具有相同的运行次数。AABB

现在开始合并运行,同时继续在 和 之间切换。每次合并时,如果您合并到,则最终再运行一次。如果合并到,则运行次数相同。ABAAB

将运行合并到如下所示:B

if top of A < top of B:
    pb
rb
while bottom of B <= top of B:
    if top of A < top of B:
        pb
    rb
while bottom of B <= top of A:
    pb
    rb

将运行合并到与此类似,只是颠倒了堆栈的角色。A

继续,直到为空。此时有 0 次运行,而有一次。这意味着已排序。BBAA

该算法将进行比较。O(n log(n))


自从我第一次回答这个问题以来,这个问题已经发生了很大变化,所以这里有一些优化的想法。

首先,在拆分时,我们可以做得更好,而不仅仅是处理 run to 和 。具体来说,我们可以将上升的运行放在 的底部,并将下降的运行推到(使它们上升)。偶尔会延长跑步时间。这些操作可以交错进行,因此,例如,我们可以处理然后将它们合并,从而使用 11 个操作对其进行排序。(这可能不是最佳的,但它给了你一个想法。如果你对此很聪明,你可能会从两堆的平均运行长度开始,大约是 4 个(也许更好)。在拆分过程中,您可以提前查看几条指令,以弄清楚如何有效地完成更长的运行。(如果您在 4 次运行中有 500 个元素,那就是 125 次运行。合并排序过程现在应该能够在 7 次传递中完成。ABABsa5 2 3 1 4pb ra ra pb rapa ra ra ra pa ra

我们是否已经找到了潜在的优化?当然不是。

当我们开始合并传递时,我们现在有不均匀的运行次数和不均匀数量的元素。我们将合并成对的运行,将它们放在某个地方,再次合并成对,将它们放在某个地方,等等。完成传递后,我们希望有两件事是正确的:

  1. 两个堆栈中的平均运行长度应大致相同(合并相似长度的运行效率更高)。
  2. 我们希望使用尽可能少的操作。由于合并到执行操作,因此我们把合并放在哪里很重要。nm2n+m

我们可以使用动态规划来解决这两个约束。为此,我们使用以下信息构建数据结构:

by the number of merged runs created:
    by the number of runs put in `A`
        by the number of elements put in `A`
            minimal number of required steps
            last stack merged into

然后,我们可以查看创建运行次数最多的部分,并找出使平均运行大小尽可能接近的原因。然后回过头来弄清楚哪个合并序列以最少的步数到达那里。然后我们可以计算出我们采取了哪些步骤的顺序,以及我们在哪里结束。

当你把所有这些放在一起时,我怀疑你只能用 5000 个步骤对 5000 个元素进行排序。但是,如果您不能将其平均降低到6000以下,我会感到惊讶。

一旦你拥有了所有这些,你就可以开始寻找更好的优化。(“我们不在乎需要多少分析才能产生动作”是邀请我们花费无限的精力进行优化。

评论

0赞 John Bollinger 1/13/2023
显然,需要优化的不是比较的数量,而是堆栈操作的数量。我的直觉是,这仍然会比 OP 的方法做得更好,但我不太清楚堆栈操作的数量是否会是 O(n log n)。
1赞 btilly 1/13/2023
@JohnBollinger 会的。运行的初始拆分将执行堆栈操作。离开运行。我们正在并行循环使用两个堆栈。每次轮换最多执行堆栈操作,并将运行次数减少一半。轮换后,我们只剩下一次堆栈操作。O(n)O(n)2nO(log(n))O(n log(n))
0赞 John Bollinger 1/13/2023
太好了,我接受。但是,我认为答案中的这种说法是错误的:“每次合并时,如果您合并到,那么最终会再运行一次。如果你合并到,你的运行次数是一样的。难道不是这样吗,每次在任一方向上合并一对运行时,源堆栈中的运行次数都会减少一次,总体上会减少一次?AAB
0赞 btilly 1/13/2023
@JohnBollinger 不,没错。每次合并时,都会销毁每个堆栈中的一个运行,并在合并到的堆栈中创建合并的运行。这会将另一个堆栈减少 1。因此,如果两者都有 99 次运行,则合并为以 98 次离开。然后合并,使它们并列在 98 处。然后合并到 97 离开。然后合并,让他们在 97 处并列。依此类推,直到运行 1 次且运行 0。ABABBABBAB
1赞 John Bollinger 1/13/2023
哦,我明白。你的意思是 A 最终比 B 多运行一次,而不是 A 之前多运行一次。对我来说,这不是对答案措辞的自然解读,但没关系。
3赞 rcgldr 1/14/2023 #2

问题需要编辑。该练习称为“推送交换”,这是针对 42 学校(未经认证的学校)学生的项目。该项目的第二部分称为“检查器”,用于验证“推送交换”的结果。

以下是描述推送交换质询的链接 |项目。剧透警告:它还包括作者对 100 和 500 数字的方法,因此您可能希望在 3 和 5 数字示例之后停止阅读。

https://medium.com/@jamierobertdawson/push-swap-the-least-amount-of-moves-with-two-stacks-d1e76a71789a

术语堆栈被错误地用于描述容器 ab(可能是法语到英语的翻译问题),因为交换和轮换不是本机堆栈操作。常见的实现使用循环双向链表。

Push Swap:输入一组要放入 A 中的整数,并生成导致 A 排序的 11 个操作的列表。其他变量和数组可用于生成操作列表。一些网站在整数集中没有提到重复项。如果存在重复项,我会假设一个稳定的排序,其中重复项将保持其原始顺序。否则,如果要寻求最佳解决方案,则需要测试所有重复项的排列。

checker:验证 push swap 的结果。

这两个程序都需要验证输入并产生结果。

一个网站列出了推送交换的评分方式。

required: sort   3 numbers with <=     3 operations
required: sort   5 numbers with <=    12 operations
scored:   sort 100 numbers with <=   700 operations   max score
                                     900 operations
                                    1100 operations
                                    1300 operations
                                    1500 operations   min score
scored:   sort 500 numbers with <=  5500 operations   max score
                                    7000 operations
                                    8500 operations
                                   10000 operations
                                   11500 operations   min score

下面给出了算法中允许生成操作列表的概念。第一步是将 a 中的值转换为等级(这些值永远不会再次使用)。如果是重复项,请在转换为等级时使用重复项的顺序(稳定排序),因此没有重复的等级。排名的值是排名在排序数组中所属的位置:

    for(i = 0; i < n; i++)
        sorted[rank[i]] = rank[i].

例如,值 {-2 3 11 9 -5} 将转换为 {1 2 4 3 0}:-2 属于 sorted[1],3 属于 sorted[2],...、-5 属于 sorted[0]。对于允许重复的稳定排序,值 {7 5 5 1 5} 将转换为 {4 1 2 0 3}。

如果 a 有 3 个等级,则等级有 6 种排列,最多需要 2 次运算才能对 a 进行排序:

{0 1 2} : already sorted
{0 2 1} : sa ra
{1 0 2} : sa
{1 2 0} : rra
{2 0 1} : ra
{2 1 0} : sa rra

对于 5 个等级,可以使用 2 个操作将 2 个移动到 b,剩下的 3 个在排序中最多 2 个操作,留下至少 8 个操作将 2 个等级从 b 插入 a 中,最终得到排序的 a。只有 20 种可能的方法可以将 2 个等级从 b 移动到 a,小到足以创建一个包含 20 个优化操作集的表。

对于 100 和 500 号码,有多种策略。

剧情透露:

Youtube 视频显示 n=100 时有 510 次操作,n=500 时显示 3750 次操作。

https://www.youtube.com/watch?v=2aMrmWOgLvU

描述转换为英文:

Initial stage :
 - parse parameters
 - Creation of a stack A which is a circular doubly linked list (last.next = first; first.prec = last
 - Addition in the struct of a rank component, integer from 1 to n.
This will be much more practical later.
Phase 1 :
 - Split the list into 3 (modifiable parameter in the .h).
 - Push the 2 smallest thirds into stack B and do a pre-sort. do ra with others
 - Repeat the operation until there are only 3 numbers left in stack A.
 - Sort these 3 numbers with a specific algo (2 operations maximum)
Phase2:
 (Only the ra/rra/rb/rrb commands are used. sa and sb are not used in this phase)
 - Swipe B and look for the number that will take the fewest moves to be pushed into A.
 There are each time 4 ways to bring a number from B to A: ra+rb, ra+rrb, rra+rb, rra+rrb. We are looking for the mini between these 4 ways.
 - Then perform the operation.
 - Repeat the operation until empty B.
Phase 3: If necessary rot stack A to finalize the correct order. The shorter between ra or rra.
The optimization comes from the fact of the maximum use of the double rotations rr and rrr

解释:

Replace all values in a by rank.
For n = 100, a 3 way split is done:
ranks 0 to 32 are moved to the bottom of b,
ranks 33 to 65 are moved to the top of b,
leaving ranks 66 to 99 in a.
I'm not sure what is meant by "pre-sort" (top | bottom split in b?).
Ranks 66 to 99 in a are sorted, using b as needed.
Ranks from b are then inserted into a using fewest rotates.
For n = 500, a 7 way split is done:
Ranks 0 to 71 moved to bottom of b, 72 to 142 to top of b, which
will end up in the middle of b after other ranks moved to b.
Ranks 143 to 214 to bottom of b, 215 to 285 to top of b.
Ranks 286 to 357 to bottom of b, 358 to 428 to top of b.
Leaving ranks 429 to 499 in a.
The largest ranks in b are at the outer edges, smallest in the middle,
since the larger ranks are moved into sorted a before smaller ranks.
Ranks in a are sorted, then ranks from b moved into a using fewest rotates.

评论

0赞 Emperor_Udan 1/17/2023
我知道您不是描述的作者,但您知道作者所说的“将 2 个最小的三分之一推入堆栈 B 并进行预排序”是什么意思吗?最小的三分之二是什么?排名之和最小的三分之二?元素最少的三分之二?
1赞 rcgldr 1/18/2023
@Emperor_Udan - 我更新了我的答案,并为视频添加了解释。