通过列表列表递归“所有路径” - Python

时间:2013-04-19 15:58:00

标签: python recursion

一个奇怪的问题,给出一个列表列表(这里最多嵌套一个级别):

[['a', 'b'], 'c', ['d', 'e'], ['f', 'g'], 'h']

..查找所有长度与给定列表相同的列表,并包含子列表中元素的所有可能组合,其中给定子列表的1个元素与原始子列表位于同一位置(很难将其放入话)。也就是说,找到这个:

['a', 'c', 'd', 'f', 'h']
['a', 'c', 'd', 'g', 'h']
['a', 'c', 'e', 'f', 'h']
['a', 'c', 'e', 'g', 'h']
['b', 'c', 'd', 'f', 'h']
['b', 'c', 'd', 'g', 'h']
['b', 'c', 'e', 'f', 'h']
['b', 'c', 'e', 'g', 'h']

现在,我找到了解决方案,但对我来说并不满意:

def all_paths(s, acc=None, result=None):
    # not using usual "acc = acc or []" trick, because on the next recursive call "[] or []" would be
    # evaluated left to right and acc would point to SECOND [], which creates separate accumulator 
    # for each call stack frame
    if acc is None:
        acc = []
    if result is None:
        result = []
    head, tail = s[0], s[1:]
    acc_copy = acc[:]
    for el in head:
        acc = acc_copy[:]
        acc.append(el)
        if tail:
            all_paths(tail, acc=acc, result=result)
        else:
            result.append(acc)
    return result

正如你所看到的,它涉及复制累加器列表TWICE,原因很明显,如果.append()或.extend()方法被调用递归堆栈,累加器将被修改,因为它是通过标签传递的(共享官方语言?)。

我试图制作pop()的解决方案,并在累加器上附加()相关数量的项目,但无法正确使用:

def all_p(s, acc=None, result=None, calldepth=0, seqlen=0):
    if acc is None:
        acc = []
    if result is None:
        seqlen = len(s)
        result = []
    head, tail = s[0], s[1:]
    for el in head:
        acc.append(el)
        if tail:
            all_p(tail, acc=acc, result=result, calldepth=calldepth+1, seqlen=seqlen)
        else:
            result.append(acc[:])
            print acc
            for i in xrange(1+seqlen-calldepth):
                acc.pop()
    return result

结果:

['a', 'c', 'd', 'f', 'h']
['a', 'c', 'd', 'g', 'h']
['a', 'c', 'd', 'e', 'f', 'h']
['a', 'c', 'd', 'e', 'g', 'h']
['a', 'c', 'd', 'e', 'b', 'c', 'd', 'f', 'h']
['a', 'c', 'd', 'e', 'b', 'c', 'd', 'g', 'h']
['a', 'c', 'd', 'e', 'b', 'c', 'd', 'e', 'f', 'h']
['a', 'c', 'd', 'e', 'b', 'c', 'd', 'e', 'g', 'h']
['a', 'c', 'd', 'f', 'h']
['a', 'c', 'd', 'g', 'h']
['a', 'c', 'd', 'e', 'f', 'h']
['a', 'c', 'd', 'e', 'g', 'h']
['a', 'c', 'd', 'e', 'b', 'c', 'd', 'f', 'h']
['a', 'c', 'd', 'e', 'b', 'c', 'd', 'g', 'h']
['a', 'c', 'd', 'e', 'b', 'c', 'd', 'e', 'f', 'h']
['a', 'c', 'd', 'e', 'b', 'c', 'd', 'e', 'g', 'h']

显然,这是因为这里的深度优先递归在调用链中上下跳跃,我无法获得pop()的数量来微调累加器列表。

我确实意识到这很有实际意义,因为复制列表是O(n),而从列表中弹出k项是O(k),所以这里没有那么多差别,但我是好奇,如果可以做到这一点。

(背景:我正在重做电话代码基准测试,http://page.mi.fu-berlin.de/prechelt/phonecode/,这是查找所有单词的部分,但是电话号码的每个部分都可以映射到多个单词,如下所示:

... '4824': ['fort', 'Torf'], '4021': ['fern'], '562': ['mir', 'Mix'] ...

所以我需要通过选定的匹配单词和/或数字列表找到所有可能的“路径”,与给定的电话号码相对应)

问题,要求:

  1. 可以修复不复制累加器的版本吗?

  2. 有没有使用itertools模块的解决方案?

  3. 任何其他更好的解决此特定问题的方法?像非递归解决方案,更快的解决方案,更少的内存密集型解决方案?

  4. 是的,我知道这是一大堆问题,但如果有人解决了非空的一部分,我将不胜感激。 : - )

2 个答案:

答案 0 :(得分:4)

  

有没有使用itertools模块的解决方案?

是的,使用itertools.product()非常简单。这足以满足您的具体示例......

>>> import itertools
>>> l = [['a', 'b'], 'c', ['d', 'e'], ['f', 'g'], 'h']
>>> for i in itertools.product(*l): print list(i)
['a', 'c', 'd', 'f', 'h']
['a', 'c', 'd', 'g', 'h']
['a', 'c', 'e', 'f', 'h']
['a', 'c', 'e', 'g', 'h']
['b', 'c', 'd', 'f', 'h']
['b', 'c', 'd', 'g', 'h']
['b', 'c', 'e', 'f', 'h']
['b', 'c', 'e', 'g', 'h']

...但正如DSM在评论中指出的那样,它只能起作用,因为你的例子使用单字符字符串,它是长度为1的序列对象。如果总是如此,你可以表达这样的列表......

['ab', 'c', 'de', 'fg', 'h']

但是,在一般情况下,您可能希望确保所有列表项都是具有类似内容的序列...

>>> l = [None, int, 0, 'abc', [1, 2, 3], ('a', 'b')]
>>> for i in itertools.product(*[i if isinstance(i, (list, tuple)) else [i] for i in l]): print list(i)
[None, <type 'int'>, 0, 'abc', 1, 'a']
[None, <type 'int'>, 0, 'abc', 1, 'b']
[None, <type 'int'>, 0, 'abc', 2, 'a']
[None, <type 'int'>, 0, 'abc', 2, 'b']
[None, <type 'int'>, 0, 'abc', 3, 'a']
[None, <type 'int'>, 0, 'abc', 3, 'b']
  

对这个特殊问题采取任何其他更好的方法吗?喜欢   非递归解决方案,更快的解决方案,更少的内存密集型解决方案?

任何解决方案可能都必须以某种方式使用递归,如果不是在堆栈上,那么在堆上。

答案 1 :(得分:1)

这是一个较少Pythonesque,但可能是不可滚动的版本:

def all_paths(l):
        if 1 == len(l):
                return [ l ]
        down = all_paths(l[1:])

        # We iterate only lists, not tuples, not strings
        if type(l[0]) in [ list ]:
                return [ [ ll ] + k for k in down for ll in l[0] ]
        return [ [ l[0] ] + k for k in down ]

l = [['a', 'b'], 'c', ['d', 'e'], ['f', 'g'], 'h']

for i in all_paths(l):
        print i

在这种形式中,它在所有方面都不如itertools解决方案:时间的两倍,以及内存影响的四倍以上:

HEAP SUMMARY (itertools)
in use at exit: 1,867,521 bytes in 537 blocks
total heap usage: 12,904 allocs, 12,367 frees, 8,444,917 bytes allocated

HEAP SUMMARY (lists)
in use at exit: 1,853,779 bytes in 498 blocks
total heap usage: 57,653 allocs, 57,155 frees, 9,272,129 bytes allocated

同样展开这个功能 - 至少,以一种非常天真的方式 - 不如itertools(慢三倍),以及两倍的内存分配:

HEAP SUMMARY:
in use at exit: 1,853,779 bytes in 498 blocks
total heap usage: 26,707 allocs, 26,209 frees, 8,812,153 bytes allocated


def all_paths(l):
        m   = len(l)-1
        val = [ i for i in range(0,m) ]
        ndx = [ 0 for i in l ]
        top = [ len(i) if type(i) in [list] else 0 for i in l ]
        while(True):
                path = [ l[i] if top[i] == 0 else l[i][ndx[i]] for i in val ]
                #print path
                n   = m
                ndx[n] = ndx[n] + 1
                while (ndx[n] >= top[n]):
                        ndx[n] = 0
                        n = n - 1
                        if (-1 == n):
                                return
                        ndx[n] = ndx[n] + 1

(使用enumeratetype(l[i])会导致表现更差)。