【问题标题】:Python recursion memoizationPython递归记忆
【发布时间】:2018-04-25 18:26:30
【问题描述】:

我有以下作业:给定一个包含 n 个整数的列表,列表中的每个整数都是唯一的并且大于 0。我还得到了一个数字 K——它是一个大于 0 的整数。 不允许任何形式的列表切片

我需要检查是否存在总和为 K 的子集。 例如:对于列表 [1,4,8] 和 k=5,我返回 True,因为我们有子集 {1,4}。

现在我需要使用递归来实现它: 所以我做了,但是我需要实现记忆:

我想知道这些函数的代码有什么区别: 我的意思是,两者似乎都实现了记忆,但是第二个应该更好,但事实并非如此。我真的很感激一些帮助:)

def subsetsum_mem(L, k):
    '''
    fill-in your code below here according to the instructions
    '''
    sum_dict={}
    return s_rec_mem(L,0,k,sum_dict)



def s_rec_mem(L, i, k, d):
    '''
    fill-in your code below here according to the instructions
    '''
    if(k==0):
        return True
    elif(k<0 or i==len(L)):
        return False
    else:
        if k not in d:
            res_k=s_rec_mem(L,i+1,k-L[i],d) or s_rec_mem(L,i+1,k,d)
            d[k]=res_k
            return res_k


def subsetsum_mem2(L, k):
    '''
    fill-in your code below here according to the instructions
    '''
    sum_dict={}
    return s_rec_mem2(L,0,k,sum_dict)


def s_rec_mem2(L, i, k, d):
    '''
    fill-in your code below here according to the instructions
    '''
    if(k==0):
        return True
    elif(k<0 or i==len(L)):
        return False
    else:
        if k not in d:
            res_k=s_rec_mem2(L,i+1,k-L[i],d) or s_rec_mem2(L,i+1,k,d)
            d[k]=res_k
            return res_k
        else:
            return d[k]

【问题讨论】:

  • 在第一个中,如果kd中,你永远不会返回任何东西
  • “工作得更好”是什么意思?
  • 第一个没有做正确的事情,因为如果值已经在缓存中,它会从末尾掉下来并返回None而不是d[k]。由于None 是错误的,这很可能使您的代码运行得更快,只需不做大部分必要的工作并很快返回错误的答案。
  • 顺便说一下,您可能想看看使用@functools.lru_cache 进行记忆。即使您正在编写自己的 memoization 作为学习练习(或试图超越 lru_cache... 的性能),也值得拥有经过良好测试的标准实现,以便在单元测试和基准测试中比较您的版本。跨度>
  • @abarnert 哦,我明白了,那么第一个返回一些 None 值可能会使代码运行得更快,但实际上并不起作用。所以问题是,第二个代码是否真的让这个过程更有效率?还是我错过了什么? abarnert - 关于 Iru_cache,我会调查一下,谢谢 :)

标签: python recursion memoization


【解决方案1】:

你的记忆有两个问题。

首先,您只使用k 作为缓存键。但是该函数对i 的不同值执行不同的操作,并且您将忽略缓存中的i,因此您最终将返回来自L, 9, 1, d 的值以获取L, 1, 1, d

第二,只在s_rec_mem,你永远不会回d[k];如果它存在,你就从最后掉下来并返回None(这是错误的)。

所以,第二个确实更接近工作 - 但它实际上仍然不起作用。

你可以这样修复它:

def s_rec_mem2(L, i, k, d):
    '''
    fill-in your code below here according to the instructions
    '''
    if(k==0):
        return True
    elif(k<0 or i==len(L)):
        return False
    else:
        if (i, k) not in d:
            res_k=s_rec_mem2(L,i+1,k-L[i],d) or s_rec_mem2(L,i+1,k,d)
            d[i, k]=res_k
            return res_k
        else:
            return d[i, k]

... 或者只使用lru_cache,或者通过传递tuple(L) 而不是L(因为元组与列表不同,可以作为键散列,并且您的递归函数不关心它是什么样的序列获取),或者通过闭包使其成为一个本地函数来查看L,而不是将其作为参数传递。


最后,快速浏览一下您的代码:

  • 看起来您对每组参数最多只能评估 s_rec_mem 两次(假设您正确缓存在 i, k 而不仅仅是 k),这意味着 memoization 只能提供 2x充其量是不断加速。要获得更多,您需要重新考虑您的缓存或算法。
  • 您只是在每个单独的顶级运行中记忆,但在 tuple(L), i, k 上切换到 lru_cache 意味着您在所有运行中记忆,直到您重新启动程序(或 REPL 会话)——所以第一个测试可能需要很长时间,但随后在同一 L 上运行(即使使用不同的 k)可能会受益于之前的缓存。
  • 您似乎正在尝试解决子集和问题的一个小变化。在一般情况下,该问题可证明是 NP 完全的,这意味着 保证 需要指数级的时间。你的变化似乎比标准问题更难,而不是更容易。如果是这样,不仅您的恒定因子加速不会有太大的好处,而且真的没有什么可以做得更好。在现实生活中,解决等效问题的人通常使用优化(例如,通过动态规划)或在任意指定限制内​​进行逼近,这两种方法在大多数情况下都允许多项式(或至少是伪多项式)解,但可以不能解决所有情况。或者,您可以在多项式时间内解决输入的子类,如果幸运的话,您可以证明您的输入属于这些子类之一。如果我猜对了你在做什么,并且你想追求这三个选项中的任何一个,你需要做一些研究。 (也许 Wikipedia 已经足够好,或者这里或 compsci 或 math SE 网站上有很好的答案可以帮助您入门。)

【讨论】:

  • 非常感谢!在我自己经历了一段时间(太多)之后,我已经找到了你上面提到的解决方案。但是,将它与 lru_cache 进行比较似乎表明它运行得不是很好——因为对于各种不同的输入,这个函数在有和没有记忆的情况下的运行时间几乎相同。考虑到问题的限制,您认为还有什么可以做的吗?
  • 非常感谢您的周到回复!我会调查的:)
  • @Guy 最后一条评论:当我在最后一个要点中说“大多数情况”时,这实际上可能不是真的。许多优化解决方案实际上只适用于所有可能情况的极小部分,但这些情况包括您通常真正关心的大多数情况。 (这是几乎所有函数都不是连续的事实的有限版本,但你想到的大多数函数都是)​​所以你必须很好地理解你的输入空间和算法(或者交叉你的手指并测试)一般。
  • 我明白了,想想这很有趣,感谢您的周到的 cmets :)
猜你喜欢
  • 1970-01-01
  • 2012-11-12
  • 2012-09-27
  • 1970-01-01
  • 2018-09-08
  • 2022-12-03
  • 2015-05-03
  • 2020-12-04
  • 2014-05-03
相关资源
最近更新 更多