魔方排序矩阵(又称圆环拼图)


16

这种的想法很简单:给定整数矩阵,让我们通过应用Rubik风格的移动对其进行排序。这意味着您可以选择单个行或列,并向任意方向旋转其元素:

[1, 3, 2, 4]  => [3, 2, 4, 1] (rotate left for rows/up for columns)
[1, 3, 2, 4]  => [4, 1, 3, 2] (rotate right for rows/down for columns)

因此,给定任意维度的整数矩阵,仅应用这些Rubik样式的转换对其元素进行排序。矩阵

[一种11一种12一种13一种14一种21一种22一种23一种24一种31一种32一种33一种34]

如果其元素符合以下限制,则将被视为已排序:

一种11一种12一种13一种14一种21一种22一种23一种24一种31一种32一种33一种34

输入输出

  • 输入将是没有重复值的正整数矩阵。
  • 输出将是对它进行排序所需的动作。由于这不是高尔夫挑战赛的代码,并且您不必担心它的长度,因此,建议的每次运动格式是要移动的行或列的编号#[UDLR]在哪里#(0索引),并且[UDLR]在其中是单个字符指定移动是向上/向下(对于列)还是向左/向右(对于行)的范围。因此,1U这意味着“将第1列向上移动”,但1R将“将第1列向右移动”。运动将以逗号分隔,因此将这样表示一个解决方案:1R,1U,0L,2D

计分

尝试以这种方式对矩阵进行排序可能会很昂贵,因为存在许多可能的移动组合,并且还有许多可能的移动列表可以对其进行排序,因此目标是编写一些对N *进行排序的代码下面的N个矩阵。比分将是最大的大小N,你可以在合理的时间量解决1没有错误(越大矩阵的大小来解决,效果更好)。如果是平局,则平局决胜局将是您找到的路径中的移动次数(路径越短越好)。

示例:如果用户A找到了N = 5的解决方案,而用户B找到了N = 6的解决方案,则无论两条路径的长度如何,B都会获胜。如果他们都找到N = 6的解,但是A的解有50步,B的解有60步,则A获胜。

强烈建议您对代码的工作方式进行解释,请发布找到的解决方案,以便我们对其进行测试。如果解决方案太大,则可以使用Pastebin或类似工具。另外,对您的代码查找解决方案所花费时间的估计将不胜感激。

测试用例

从已经排序的矩阵开始,通过以10K随机,Rubik风格的移动对其进行加扰,创建了以下矩阵(Pastebin链接为可复制粘贴的版本):

[85611101个31513]
[211012161762214851926132431个]
[1个1381659402126221124143928321937310301736734]
[342140223541183331301243191139242823441个36538451417916132683476254]
[2036171个155018726734103235542439630613928415427235704813251246586352378453314685965岁5673606422]
[855652758944416827158791323739736764199978461642216310041个72131197309328403365070258058960845496172943342335776182482943866]
[567990617112211031551144284851个3061884433866113249620102756858880983510077132164108106011440234727310682321202636539369104541911111762172788733491558116951125711891514265岁45]

明文测试用例:

[[8, 5, 6], [11, 10, 1], [3, 15, 13]]

[[21, 10, 12, 16], [17, 6, 22, 14], [8, 5, 19, 26], [13, 24, 3, 1]]

[[1, 13, 8, 16, 5], [9, 40, 21, 26, 22], [11, 24, 14, 39, 28], [32, 19, 37, 3, 10], [30, 17, 36, 7, 34]]

[[34, 21, 40, 22, 35, 41], [18, 33, 31, 30, 12, 43], [19, 11, 39, 24, 28, 23], [44, 1, 36, 5, 38, 45], [14, 17, 9, 16, 13, 26], [8, 3, 47, 6, 25, 4]]

[[20, 36, 17, 1, 15, 50, 18], [72, 67, 34, 10, 32, 3, 55], [42, 43, 9, 6, 30, 61, 39], [28, 41, 54, 27, 23, 5, 70], [48, 13, 25, 12, 46, 58, 63], [52, 37, 8, 45, 33, 14, 68], [59, 65, 56, 73, 60, 64, 22]]

[[85, 56, 52, 75, 89, 44, 41, 68], [27, 15, 87, 91, 32, 37, 39, 73], [6, 7, 64, 19, 99, 78, 46, 16], [42, 21, 63, 100, 4, 1, 72, 13], [11, 97, 30, 93, 28, 40, 3, 36], [50, 70, 25, 80, 58, 9, 60, 84], [54, 96, 17, 29, 43, 34, 23, 35], [77, 61, 82, 48, 2, 94, 38, 66]]

[[56, 79, 90, 61, 71, 122, 110, 31, 55], [11, 44, 28, 4, 85, 1, 30, 6, 18], [84, 43, 38, 66, 113, 24, 96, 20, 102], [75, 68, 5, 88, 80, 98, 35, 100, 77], [13, 21, 64, 108, 10, 60, 114, 40, 23], [47, 2, 73, 106, 82, 32, 120, 26, 36], [53, 93, 69, 104, 54, 19, 111, 117, 62], [17, 27, 8, 87, 33, 49, 15, 58, 116], [95, 112, 57, 118, 91, 51, 42, 65, 45]]

如果您解决了所有问题,请询问更多。:-)非常感谢在沙箱中帮助我应对挑战的人们。


1合理的时间:在测试您的解决方案时不会削弱我们的耐心的任何时间。请注意,TIO仅将代码运行60秒,超过该限制的任何时间将使我们在计算机中测试代码。示例:我效率不高的算法要花几毫秒的时间才能解决3x3和4x4阶矩阵,但是我刚刚用5x5矩阵对其进行了测试,并且解决该问题花费了317秒的时间(超过500万次移动,如果考虑解决基质加扰 10K次)。我试图将移动次数减少到少于10K,但在执行代码30分钟后就投降了。


1
好挑战!但是,我有几个要求/问题:1)您能以更易于复制粘贴的格式提供测试用例吗?(例如pastebin)2)您能否提供更精确的时限顺序定义?3)矩阵是否保证为正方形?(测试用例表明是这样,但定义没有。)
Arnauld

@Arnauld 1)我在上面。2)我不想建立时间限制,并且在挑战位于沙箱中时没有人提出任何限制。如果需要一个,您会认为30分钟是合理的限制吗?3)是的,测试矩阵是所示的,如果需要更多的矩阵,它们将都是正方形的。
查理

存在一个(相对易于实现的)O(输入大小)算法来应对这一挑战,因此它并不像初看起来那样有趣。
user202729

@ user202729那么您的输入大小将是O(input size)多少?对于5x5矩阵,它将是O(25)什么?这似乎非常快,所以我将非常有兴趣看到您的算法或实现。编辑:您确实意识到我们输入了“加扰”矩阵并输出了运动,对吗?并非相反。
凯文·克鲁伊森

4
我认为,这类似于这种算法
Kirill L.

Answers:


8

尼姆

import algorithm, math, sequtils, strutils

let l = split(stdin.readLine())
var m = map(l, parseInt)
let n = int(sqrt(float(len(m))))
let o = sorted(m, system.cmp[int])

proc rotations(P, Q: int): tuple[D, L, R, U: string, P, Q: int]=
  result = (D: "D", L: "L", R: "R", U: "U", P: P, Q: Q)
  if P > n - P:
    result.D = "U"
    result.U = "D"
    result.P = n - P
  if Q > n - Q:
    result.L = "R"
    result.R = "L"
    result.Q = n - Q

proc triangle(r: int): string=
  let p = r div n
  let q = r mod n
  let i = find(m, o[r])
  let s = i div n
  let t = i mod n
  var u = s
  var v = q
  if s == p and t == q:
    return ""
  var patt = 0
  if p == s: 
    u = s + 1
    patt = 4
  elif q == t:
    if q == n - 1:
      v = t - 1
      patt = 8
    else:
      u = p
      v = t + 1
      patt = 3
  elif t > q:
    patt = 2
  else:
    patt = 7
  var Q = abs(max([q, t, v]) - min([q, t, v]))
  var P = abs(max([p, s, u]) - min([p, s, u]))
  let x = p*n + q
  let y = s*n + t
  let z = u*n + v
  let w = m[x]
  m[x] = m[y]
  m[y] = m[z]
  m[z] = w
  let R = rotations(P, Q)

  result = case patt:
    of 2:
      repeat("$#$#," % [$v, R.D], R.P) & 
        repeat("$#$#," % [$u, R.L], R.Q) &
        repeat("$#$#," % [$v, R.U], R.P) & 
        repeat("$#$#," % [$u, R.R], R.Q)
    of 3:
      repeat("$#$#," % [$q, R.U], R.P) & 
        repeat("$#$#," % [$p, R.L], R.Q) &
        repeat("$#$#," % [$q, R.D], R.P) & 
        repeat("$#$#," % [$p, R.R], R.Q)
    of 4:
      repeat("$#$#," % [$p, R.L], R.Q) & 
        repeat("$#$#," % [$q, R.U], R.P) &
        repeat("$#$#," % [$p, R.R], R.Q) & 
        repeat("$#$#," % [$q, R.D], R.P)
    of 7:
      repeat("$#$#," % [$v, R.D], R.P) & 
        repeat("$#$#," % [$u, R.R], R.Q) &
        repeat("$#$#," % [$v, R.U], R.P) & 
        repeat("$#$#," % [$u, R.L], R.Q)
    of 8:
      repeat("$#$#," % [$s, R.R], R.Q) & 
        repeat("$#$#," % [$t, R.D], R.P) &
        repeat("$#$#," % [$s, R.L], R.Q) & 
        repeat("$#$#," % [$t, R.U], R.P)
    else: ""

proc Tw(p, t, P, Q: int): string =
  let S = P + Q
  result = "$#D,$#$#U,$#$#D,$#$#U," % [
    $t, if P > n - P: repeat("$#L," % $p, n - P) else: repeat("$#R," % $p, P),
    $t, if S > n - S: repeat("$#R," % $p, n - S) else: repeat("$#L," % $p, S), 
    $t, if Q > n - Q: repeat("$#L," % $p, n - Q) else: repeat("$#R," % $p, Q), 
    $t]

proc line(r: int): string=
  let p = n - 1
  let q = r mod n
  let i = find(m, o[r])
  var t = i mod n
  if t == q: 
    return ""
  let j = t == n - 1
  var P = t - q
  let x = p*n + q
  let y = x + P
  let z = y + (if j: -1 else: 1)
  let w = m[x]
  m[x] = m[y]
  m[y] = m[z]
  m[z] = w
  if j:
    let R = rotations(1, P)
    result = "$#D,$#$#U,$#$#R,$#D,$#L,$#U," % [
      $t, repeat("$#$#," % [$p, R.R], R.Q), 
      $t, repeat("$#$#," % [$p, R.L], R.Q), 
      $p, $t, $p, $t]
  else:
    result = Tw(p, t, P, 1)  
  
proc swap: string=
  result = ""
  if m[^1] != o[^1]:
    m = o
    for i in 0..(n div 2-1):
      result &= Tw(n - 1, n - 2*i - 1, 1, 1)
    result &= "$#R," % $(n - 1)
  
var moves = ""
for r in 0..(n^2 - n - 1):
  moves &= triangle(r)
if n == 2 and m[^1] != o[^1]:
  m = o
  moves &= "1R"
else:
  for r in (n^2 - n)..(n^2 - 3):
    moves &= line(r)
  if n mod 2 == 0:
    moves &= swap()
  if len(moves) > 0:
    moves = moves[0..^2]
  
echo moves

在线尝试!

我在评论中提到的在Algorithms 2012,5,18-29中发表的文章快速尝试实现Torus拼图解决方案算法。

接受以展平形式输入的矩阵,以空格分隔的数字作为一行。

这也是Python 2中的验证器。它以两行作为输入:与主代码形式相同的原始加扰矩阵,以及建议的移动顺序。验证器的输出是通过应用这些移动得到的矩阵。

说明

在算法的第一部分,我们对除最后一行之外的所有行进行排序。

proc triangle[pq][sŤ][pq][üv]

在图2中,作者提供了8种可能的模式和相应的移动顺序,但是在我的代码中,所有情况实际上仅由5种模式覆盖,因此没有。不使用1、5和6。

在第二部分中,通过在行(proc line)上执行“三个元素旋转”来对除最后两个元素之外的最后一行进行排序,该旋转每个由两个三角形旋转组成(请参见文章的图3)。

[pq] 作为左点,包含目标值的单元格 [sŤ] 作为中心点 [sŤ+1个]作为正确的观点。该西行运动的名称为Ťw ^在本文中,我的字符串形成过程也是如此。如果Ť 已经是最后一列了,所以 Ť+1个 不存在,我们采取 [sŤ-1个] 作为第三点,并相应地修改操作:模式7和8(而不是原始模式7和1)执行了两个三角形旋转 Ťw ^ 顺序)。

最后,如果 ñ奇怪的是,其余两个元素必须已经存在,因为我们保证存在解决方案。如果ñ 是偶数,而剩余的两个元素不在适当位置,则根据引理1(第22页),可以将它们替换为一系列 Ťw ^ 向东移动一档(=[R)。由于提供的示例交换了前两个条目,而我们需要交换后两个,因此我们proc swap执行Ťw ^ 反向移动。

在边缘情况下 ñ=2 我们根本不需要所有这些复杂的过程-如果在第1部分之后没有最后一行的元素,则单个 1个[R 移动足以使矩阵完全有序。

更新:添加了新功能proc rotations,如果可以减少步数,则可以反转移动方向。


令人印象深刻!然后,我将创建更多的测试用例。同时,我确信此解决方案可以优化,因为对于9x9矩阵,存在诸如7L,7L,7L,7L,7D,7D,7D,7D可以减少和8R,8R,8R,8R,8R,8R,8R可以转换8L,8L为的块。
查理

我已经尝试过使用100x100矩阵的算法,并且可以在不到4秒的时间内解决它。我真的没想到这个问题会有线性时间解决方案。将来我会尝试编写更好的挑战!
查理

也许最好以单个固定矩阵作为唯一测试用例提出此挑战,并将获胜标准设置为找到解决该问题的路径的大小,如果我之前知道此问题为O (n ^ 2)个解决方案。您是否考虑将这种答案移植到具有此类获胜标准的新问题上?
查理

@Charlie虽然我仍将尝试稍微改进当前的解决方案,但我真的不知道如何解决总体路径优化问题……
Kirill

5

Python 2,在TIO上的<30秒内大小为100

import random
def f(a):
 d = len(a)
 r = []
 def V(j, b = -1):
  b %= d
  if d - b < b:
   for k in range(d - b):
    if r and r[-1] == "U%d" % j:r.pop()
    else:r.append("D%d" % j)
    b = a[-1][j]
    for i in range(len(a) - 1):
     a[-1 - i][j] = a[-2 - i][j]
    a[0][j] = b
  else:
   for k in range(b):
    if r and r[-1] == "D%d" % j:r.pop()
    else:r.append("U%d" % j)
    b = a[0][j]
    for i in range(len(a) - 1):
     a[i][j] = a[i + 1][j]
    a[-1][j] = b
 def H(i, b = -1):
  b %= d
  if d - b < b:
   for k in range(d - b):
    if r and r[-1] == "L%d" % i:r.pop()
    else:r.append("R%d" % i)
    a[i] = a[i][-1:] + a[i][:-1]
  else:
   for k in range(b):
    if r and r[-1] == "R%d" % i:r.pop()
    else:r.append("L%d" % i)
    a[i] = a[i][1:] + a[i][:1]
 b = sorted(sum(a, []))
 for i in range(d - 1):
  for j in range(d):
   c = b.pop(0)
   e = sum(a, []).index(c)
   if e / d == i:
    if j == 0:H(i, e - j)
    elif j < e % d:
     if i:
      V(e % d, 1)
      H(i, j - e)
      V(e % d)
      H(i, e - j)
     else:
      V(e)
      H(1, e - j)
      V(j, 1)
   else:
    if j == e % d:
     H(e / d)
     e += 1
     if e % d == 0:e -= d
    if i:
     V(j, i - e / d)
    H(e / d, e - j)
    V(j, e / d - i)
 c = [b.index(e) for e in a[-1]]
 c = [sum(c[(i + j) % d] < c[(i + k) % d] for j in range(d) for k in range(j)) % 2 and d * d or sum(abs(c[(i + j) % d] - j) for j in range(d)) for i in range(d)]
 e = min(~c[::-1].index(min(c)), c.index(min(c)), key = abs)
 H(d - 1, e)
 for j in range(d - 2):
  e = a[-1].index(b[j])
  if e > j:
   c = b.index(a[-1][j])
   if c == e:
    if e - j == 1:c = j + 2
    else:c = j + 1
   V(e)
   H(d - 1, j - e)
   V(e, 1)
   H(d - 1, c - j)
   V(e)
   H(d - 1, e - c)
   V(e, 1)
 return r

在线尝试!Link包含三个具有完整输出的小型测试用例,以及一个100x100的静默测试,以证明代码有效(移动输出将超过TIO的限制)。说明:该代码尝试对数组执行插入排序,并按升序建立它。对于除最后一行以外的所有行,有很多情况:

  • 该元素在正确的行中,但属于第0列。在这种情况下,只需旋转它就可以到达第0列。
  • 元素在正确的位置。在这种情况下,什么也不会发生。(如果元素属于列0,则也是如此,在这种情况下只是发生了0旋转。)
  • 该元素在第一行,但在错误的列中。在这种情况下,先向下旋转,然后水平旋转直到元素在正确的列中,然后再次向上旋转。
  • 元素在正确的行中但在错误的列中。在这种情况下,它将向上旋转,然后将行旋转到其列,然后向下旋转,然后将行向后旋转。(实际上,这是下一种情况的轮换。)
  • 元素在正确的列中但在错误的行中。在这种情况下,该行将向右旋转以将其减少到最后一种情况。
  • 元素在错误的行和错误的列中。在这种情况下,将正确的列旋转到错误的行(跳过第一行),然后将该行旋转到正确的列,然后将该列旋转回去。

上面的旋转是在使方向数最小的任何方向上进行的;无论上面的描述如何,总是使用向左和向上移动来解决2平方大小的问题。

在最底行完成之前,将其旋转以最大程度地减少总距离,但也要确保最底行的奇偶校验是均匀的,因为算法的最后部分无法更改它。如果具有相同最小距离的旋转不止一个,则选择移动次数最少的旋转。

最下面一行的算法依赖于7个运算序列,该序列交换三列中的元素。该顺序应用于底行的每个剩余数字,以依次将它们带到所需位置;如果可能,则将该位置中的元素移动到其所需位置,但是如果需要直接交换,则将该元素简单地移动到最近的可用列中,希望可以在下次对其进行修复。


尼尔,非常感谢您的回答!但是请记住,这不是代码高尔夫球。而不是代码的长度,您应该指出已解决的NxN矩阵的最大大小N,以及解决该矩阵的动作列表的长度。
查理

@Charlie好吧,那是6岁,但是只是因为我太懒了,无法粘贴到更大的矩阵中。尽管它是蛮力的,但它会随面积线性缩放,因此应该能够处理大型矩阵。
尼尔

实际上,最坏的情况可能是面积二次方。
尼尔,

1
@Charlie TIO链接现在可以解决100x100随机矩阵。
尼尔

1
@Charlie我现在为下一行提出了一个主要的优化,但是我认为这是我要对这个答案做出的最后更改。
尼尔,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.