【问题标题】:Recursion optimization递归优化
【发布时间】:2021-03-28 13:04:13
【问题描述】:

我正在尝试通过我的二维数组找到特定路径

我的数组可能如下所示:

temp = [
    ["placeholder", 2, 0],
    ["placeholder", 1, 7, 3],
    ["placeholder", 4, 5, 8],
    ["placeholder", 6, 3, 5, 2],
    ["placeholder", 7],
    ["placeholder", 3, 0],
]

内部数组包含一个占位符,后跟可变数量的整数。这些整数的取值范围在 0-19 之间(包括 0 和 19)

我想通过这些内部数组找到一条从上到下的路径,其中没有多次使用数字。

temp[0] 我可以选择我的第一个值20
temp[1],我可以选择我的第二个值173
temp[2],我可以选择我的第三个值458
temp[3],我可以选择我的第四个值6352
请注意,如果我在第一步中已经选择了2,则在此步骤中我无法选择2
如果我已经在第二步中选择了 3,我将无法选择 3
如果我已经在第三步中选择了 5,我将无法选择 5
等等

以下是一些合法途径:

  • 2-1-4-6-7-3
  • 0-1-4-5-7-3

这些是一些非法路径:

  • 2-1-4-2-7-3(2被触摸两次)
  • 0-1-4-5-7-0(0 被触摸两次)

我希望我的函数输出一条完全合法的路径。
如果找不到路径,我只想让我的函数返回 false。

我已经尝试编写自己的递归解决方案来解决这个问题,我认为这很有效,但是效率非常低,因此需要很长时间才能完成。
我想要的数组的大小更像是 20 x 1..20

到目前为止,我的代码看起来像这样(用 Python 3 编写)(并带有一个临时数组):

temp= [
    ["placeholder", 7, 3],
    ["placeholder", 3, 3],
    ["placeholder", 4, 3],
    ["placeholder", 3, 8],
    ["placeholder", 1, 3],
]

def findpath(array, path = []):
    
    if path and path.count(path[-1]) > 1:
        return False

    if len(path) == len(array):
        print(path)
        return True
    
    routes = array[len(path)][1:]

    for route in routes:
        path.append(route)
        if findpath(array, path):
            return True
        path.pop(-1)

findpath(temp)

此代码打印:[7, 3, 4, 8, 1]

这种方法是对所有可能性进行暴力破解,直到找到解决方案。在最坏的情况下,我的temp 数组将包含 20 个数组,每个数组都包含 20 个值,即 20 个!可能的解决方案,或者换句话说 2.432.902.008.176.640.000 可能的路径。让我们夸大一下,说我的电脑需要 1 毫秒来处理这个函数的单次迭代。我的数学可能是错误的,但这意味着这个过程可能需要 77,146,816.6 年才能完成(不考虑闰年),老实说,我没有这个时间。

必须有更聪明的方法来解决这个问题。
我注意到的一件事是,如果函数在步骤中遇到内部数组,例如15,仅包含一个 int 例如2,然后该函数将尝试一次调整其最新选择(第一步 14 然后第 13 步),而没有意识到它已经在第 2 步选择了2。在这种情况下,返回第 2 步并更改从2 到其他值的值将节省大量迭代。唯一的问题是我不知道如何实现它,也不知道如何进一步优化它。

这是一个真实的示例数组(不确定它是否包含解决方案)

temp = [
['placeholder', 2, 6, 11, 14, 15, 16, 18], 
['placeholder', 2, 6, 7, 10, 11, 14, 15, 16, 17, 18, 19], 
['placeholder', 2, 6, 10, 11, 14, 15, 16, 18, 19], 
['placeholder', 2, 6, 11, 14, 15, 16, 18, 19], 
['placeholder', 2, 6, 11, 14, 15, 16], 
['placeholder', 2, 6, 7, 10, 11, 14, 15, 16, 18, 19], 
['placeholder', 2, 6, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 19], 
['placeholder', 11, 15, 16], 
['placeholder', 11, 14, 15, 16], 
['placeholder', 0, 1, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 
['placeholder', 11, 15], 
['placeholder', 2, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 
['placeholder', 1, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 
['placeholder', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 
['placeholder', 2, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19], 
['placeholder', 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 
['placeholder', 15], 
['placeholder', 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 
['placeholder', 2, 11, 14, 15, 16], 
['placeholder', 2, 6, 7, 8, 10, 11, 14, 15, 16, 17, 18, 19]
]

希望你能帮助我学习

【问题讨论】:

  • 您能否阐明从一行到下一行和列的有效“移动”是什么?如果没有有效路径,输出是什么?
  • 我已经编辑了这篇文章,希望这更有意义:)
  • 非常感谢,我看看
  • @pluto9800 在看到您的真实输入示例后,我对我的帖子进行了一些编辑。如果您对我的帖子有任何疑问,我很乐意提供帮助:D
  • 非常感谢您的时间和精力。我现在会浏览你的帖子,如果我需要进一步的帮助,请务必告诉你:)

标签: arrays python-3.x recursion


【解决方案1】:

功能传承

递归是一种函数式遗产,因此将其与函数式风格一起使用会产生最佳效果。这意味着要避免像.append.pop 这样的突变以及其他副作用。

下面我们使用set 来有效地跳过无效组合,并使用tuple 来存储路径。为每个递归构造一个 new 集合和元组 -

def traverse(t, s = set(), p = ()):
  if not t:
    yield p
  else:
    [ [_, *values], *more ] = t
    for v in values:
      if v not in s:
        yield from traverse(more, {*s, v}, (*p, v))

使用示例程序中的数据 -

temp = \
  [ ["placeholder", 7, 3]
  , ["placeholder", 3, 3]
  , ["placeholder", 4, 3]
  , ["placeholder", 3, 8]
  , ["placeholder", 1, 3]
  ]

for p in traverse(temp):
  print(p)
(7, 3, 4, 8, 1)
(7, 3, 4, 8, 1)

并使用原始问题中的数据 -

temp = \
  [ ["placeholder", 2, 0]
  , ["placeholder", 1, 7, 3]
  , ["placeholder", 4, 5, 8]
  , ["placeholder", 6, 3, 5, 2]
  , ["placeholder", 7]
  , ["placeholder", 3, 0]
  ]

for p in traverse(temp):
  print(p)
(2, 1, 4, 6, 7, 3)
(2, 1, 4, 6, 7, 0)
(2, 1, 4, 3, 7, 0)
(2, 1, 4, 5, 7, 3)
(2, 1, 4, 5, 7, 0)
(2, 1, 5, 6, 7, 3)
(2, 1, 5, 6, 7, 0)
(2, 1, 5, 3, 7, 0)
(2, 1, 8, 6, 7, 3)
(2, 1, 8, 6, 7, 0)
(2, 1, 8, 3, 7, 0)
(2, 1, 8, 5, 7, 3)
(2, 1, 8, 5, 7, 0)
(2, 3, 4, 6, 7, 0)
(2, 3, 4, 5, 7, 0)
(2, 3, 5, 6, 7, 0)
(2, 3, 8, 6, 7, 0)
(2, 3, 8, 5, 7, 0)
(0, 1, 4, 6, 7, 3)
(0, 1, 4, 5, 7, 3)
(0, 1, 4, 2, 7, 3)
(0, 1, 5, 6, 7, 3)
(0, 1, 5, 2, 7, 3)
(0, 1, 8, 6, 7, 3)
(0, 1, 8, 5, 7, 3)
(0, 1, 8, 2, 7, 3)

生成器很懒

因为我们使用的是生成器,所以我们可以随时停止生成路径。或许你只想找到first的有效路径——

def first_valid_path(t):
  for path in traverse(t):
    return path

temp = \
  [ ["placeholder", 2, 0]
  , ["placeholder", 1, 7, 3]
  , ["placeholder", 4, 5, 8]
  , ["placeholder", 6, 3, 5, 2]
  , ["placeholder", 7]
  , ["placeholder", 3, 0]
  ]

print(first_valid_path(temp))
(2, 1, 4, 6, 7, 3)

优化

我们可以做的一个这样的优化是按照可能性的数量对输入数组进行排序,首先是最小可能性,因为这些是最难实现的 -

temp.sort(key=len)
print(temp)
[ ['placeholder', 15]
, ['placeholder', 11, 15] 
, ['placeholder', 11, 15, 16] 
, ['placeholder', 11, 14, 15, 16] 
, ['placeholder', 2, 11, 14, 15, 16] 
, ['placeholder', 2, 6, 11, 14, 15, 16] 
, ['placeholder', 2, 6, 11, 14, 15, 16, 18] 
, ['placeholder', 2, 6, 11, 14, 15, 16, 18, 19] 
, ['placeholder', 2, 6, 10, 11, 14, 15, 16, 18, 19] 
, ['placeholder', 2, 6, 7, 10, 11, 14, 15, 16, 18, 19] 
, ['placeholder', 2, 6, 7, 10, 11, 14, 15, 16, 17, 18, 19] 
, ['placeholder', 2, 6, 7, 8, 10, 11, 14, 15, 16, 17, 18, 19]
, ['placeholder', 2, 6, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 19] 
, ['placeholder', 2, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19] 
, ['placeholder', 2, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] 
, ['placeholder', 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]  
, ['placeholder', 1, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] 
, ['placeholder', 0, 1, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] 
, ['placeholder', 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] 
, ['placeholder', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
]

我没有测量,但立即找到了解决方案(不到一秒)-

print(first_valid_path(temp))
(15, 11, 16, 14, 2, 6, 18, 19, 10, 7, 17, 8, 9, 13, 12, 4, 1, 0, 5, 3)

要按正确的顺序排列路径,它需要未排序。有多种方法可以做到这一点,稍后我将考虑使用建议的解决方案更新帖子。现在,这留给读者作为练习:D


取消排序

如果你能做到这一点,那就太好了!上面我提出了一个排序和取消排序过程,下面我将展示一种处理取消排序的方法-

def traverse(init):
  def sort(t):
    z = list(enumerate(t))
    z.sort(key=lambda _: len(_[1]))
    return z

  def unsort(p):
    p.sort(key=lambda _: _[0])
    return tuple(v for (_, v) in p)

  def loop(t, s, p):
    if not t:
      yield unsort(p)
    else:
      [ (k, [_, *values]), *more ] = t
      for v in values:
        if v not in s:
          yield from loop(more, {*s, v}, [*p, (k, v)])
  
  yield from loop(sort(init), set(), [])

这通过使用sort 创建一个中间表示来工作 -

[ (16, ['placeholder', 15])
, (10, ['placeholder', 11, 15])
, (7, ['placeholder', 11, 15, 16])
, (8, ['placeholder', 11, 14, 15, 16])
, (18, ['placeholder', 2, 11, 14, 15, 16])
, (4, ['placeholder', 2, 6, 11, 14, 15, 16])
, (0, ['placeholder', 2, 6, 11, 14, 15, 16, 18])
, (3, ['placeholder', 2, 6, 11, 14, 15, 16, 18, 19])
, (2, ['placeholder', 2, 6, 10, 11, 14, 15, 16, 18, 19])
, (5, ['placeholder', 2, 6, 7, 10, 11, 14, 15, 16, 18, 19])
, (1, ['placeholder', 2, 6, 7, 10, 11, 14, 15, 16, 17, 18, 19])
, (19, ['placeholder', 2, 6, 7, 8, 10, 11, 14, 15, 16, 17, 18, 19])
, (6, ['placeholder', 2, 6, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 19])
, (14, ['placeholder', 2, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19])
, (11, ['placeholder', 2, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
, (15, ['placeholder', 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
, (12, ['placeholder', 1, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
, (9, ['placeholder', 0, 1, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
, (17, ['placeholder', 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
, (13, ['placeholder', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
]

路径由元组 (sort_key, value)unsorted 组成,使用 sort_key -

[(16, 15), (10, 11), (7, 16), (8, 14), (18, 2), (4, 6), (0, 18), (3, 19), (2, 10), (5, 7), (1, 17), (19, 8), (6, 9), (14, 13), (11, 12), (15, 4), (12, 1), (9, 0), (17, 5), (13, 3)]

最后sort_key 被移除,只产生我们关心的值路径。现在我们可以按照正确的顺序看到路径。立即计算结果(不到一秒)-

print(first_valid_path(temp))
(18, 17, 10, 19, 6, 7, 9, 16, 14, 0, 11, 12, 1, 3, 13, 4, 15, 5, 2, 8)

所有有效路径

上面我们使用find_first_path,但我们的排序/取消排序技术也可以有效地找到所有解决方案 -

for p in traverse(temp):
  print(p)

对于这个特定的输入,只有一个有效路径。然而,这个程序很快就终止了(不到一秒),表明所有的可能性都已经用尽了。

【讨论】:

  • 对于像帖子这样的小输入,它工作正常。但是,对于较大的 2D 阵列 (20x20),正如@pluto9800 所问的那样,它似乎需要更长的时间——在我的 Acer Aspire E15 PC 中需要 368.528 秒?这是预期的吗?
  • Daniel,7 分钟用尽 所有 解决方案还是只找到 第一个 解决方案?仍然是超过 770 亿年的进步。我想到但没有在这里实现的一个优化是 1) 首先对输入数组进行排序,因为这些要求更难满足,2) 执行遍历,3) 取消对遍历路径的排序以恢复到它们的原始顺序。发布您使用的数据,也许我会在今天晚些时候试一试。感谢您的评论:)
  • 谢谢! @谢谢。这确实是显着的改进。只是想知道是否有进一步提升性能的空间......(因为作者发布了 20x20 阵列的问题)。伟大的想法 - 也会尝试。竖起大拇指!
  • [ [_, *values], *more] = t 行也可以优化。可以使用索引来代替创建中间值的解构。它增加了更多的概念开销,但从计算过程中减去了空间和时间。
  • Daniel,我对排序理论进行了快速测试,它适用于 OP 提供的大型数据集。取消分类步骤不会太困难,并且不会为该过程增加任何重要的时间/空间要求。如果时间允许,我会继续努力。
猜你喜欢
  • 1970-01-01
  • 2019-01-27
  • 2016-07-28
  • 2014-01-10
  • 2019-12-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多