带递归和memoization算法的运行时错误

时间:2018-04-14 20:52:29

标签: string algorithm recursion runtime-error memoization

我正在尝试解决https://www.hackerrank.com/challenges/password-cracker/problem问题 这是问题陈述

在CuteKittens.com网站上注册了N个用户。它们中的每一个都有一个唯一的密码,由pass [1],pass [2],...,pass [N]表示。由于这是一个非常可爱的网站,许多人想要访问那些可爱的小猫可爱的照片。但坚定的管理员不希望这个网站可供一般公众使用。所以只有拥有密码的人才能访问它。

Yu是一个很棒的黑客,他们在密码验证系统中发现了一个漏洞。密码验证系统也接受以任何顺序串联一个或多个密码的字符串。任何密码都可以在该字符串中出现0次或更多次。他可以访问每个N个密码,并且还有一个字符串loginAttempt,他必须告诉该字符串是否被网站的密码验证系统接受。

例如,如果有3个用户使用密码{“abra”,“ka”,“dabra”},那么一些有效组合是“abra”(通过[1]),“kaabra”(通过[ 2] +传递[1]),“kadabraka”(传球[2] +传球[3] +传球[2]),“kadabraabra”(传球[2] +传球[3] +传球[1])等等上。

输入格式

第一行包含整数T,即测试用例的总数。然后是T测试案例。 每个测试用例的第一行包含N,即具有密码的用户数。第二行包含N个空格分隔的字符串,传递[1]传递[2] ...传递[N],表示每个用户的密码。第三行包含一个字符串loginAttempt,Yu必须判断它是否被接受

我尝试使用递归和memoization来解决它 这是我在python3中的解决方案

#!/bin/python3

import sys

def passwordCracker(passwords, attempt, memo):

    if attempt in memo:
        return memo[attempt]

    for pwd in passwords:
        if attempt.startswith(pwd):
            if len(pwd) == len(attempt):
                memo[attempt] = [pwd]

                return [pwd]

            prefix_size = len(pwd)
            cracked_pwds = passwordCracker(passwords, attempt[prefix_size:], memo)

            if len(cracked_pwds) != 0:              
                cracked_pwds.append(pwd)               
                memo[attempt] = list(cracked_pwds)

                return cracked_pwds

    memo[attempt] = []

    return []

def print_passwords(passwords, attempt, memo):

    #passwords.sort(key = len)
    result = passwordCracker(passwords, attempt, memo)
    result.reverse()
    #print (memo)
    if len(result) == 0:
        print ("WRONG PASSWORD")
    else:
        res_str = result[0] 
        for w in result[1:]:
            res_str = res_str + " " + w
        print(res_str)

if __name__ == "__main__":
    t = int(input().strip())
    for a0 in range(t):
        memo = {}
        n = int(input().strip())
        passwords = input().strip().split(' ')
        attempt = input().strip()
        print_passwords(passwords, attempt, memo)

但是,在运行时错误或超出时间限制的情况下,很多测试用例都失败了。 失败的一个特定测试用例是输入

1
10
a aa aaa aaaa aaaaa aaaaaa aaaaaaa aaaaaaaa aaaaaaaaa aaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab

我无法弄清楚如何改善这一点。 任何人都可以建议一个更好的替代解决方案。

3 个答案:

答案 0 :(得分:2)

您的memo[]缓存非常低效。

本练习所要做的只是在attempt中插入空格以便将其分隔为有效密码,或者如果不可能则返回WRONG PASSWORD

因此,您需要跟踪的唯一信息是应添加这些空格的位置。

您可以在没有dict的情况下执行此操作,也无需创建任何匹配字符串的部分列表。

从简单的数字列表开始(例如memo = [-1] * len(attempt))。每当找到匹配项时,请存储下一个不匹配字符的索引。如果此索引恰好等于len(attempt),那么您显然可以停止搜索。如果特定位置不存在匹配项,也存储该结果。

获得成功匹配后,请逐步查看字符串偏移列表,找出需要插入空格的位置。

修改

我在HackerRank解决了这个问题。上面的方法基本上工作正常,但需要一些小的调整来通过所有的测试。特别是,您可能需要调用sys.setrecursionlimit以允许更高级别的递归。

答案 1 :(得分:2)

我想在@ squeamish-ossifrage的回答中添加一些提示。您正在探索大量的可能性。假设attempt的大小为n,您有k个密码。您可以将您的探索想象成一棵大树,其中每个节点都有k个子节点,即序列中的下一个可能的密码。如果密码的最小长度为l,则您的算法的时间复杂度大约为O(k^n/l),因为您将构建每个n/l密码字符串,这就是您的解决方案以{结尾{ {1}}。

正如@ squeamish-ossifrage所写,你可以改进你的记忆技巧。但还有其他一些要考虑的问题。

首先,在您的示例中,Time limit exceeded包含缺少每个密码的attempt。更正式地说,如果b的字符集是密码字符集的严格超集,那么您确定无法找到解决方案。如果attempt是密码的最大长度,则时间复杂度为O(n + kL)。

其次,尝试修剪研究树(即减少每个节点的子节点数)。这该怎么做?只需尝试限制可接受的密码数量。

基本思想是,如果L不包含密码,则应从探索中排除此密码。您可以在每个步骤重复此操作,因为attempt可能不包含attempt[prefix_size:]中包含的密码。时间复杂度:在最佳情况下使用Python搜索算法的O(kn / l),每一步。

更复杂的想法是删除由其他密码组成的密码。由于您有attempt,因此您不需要aaa等。如何?您只需将aaapasswordCracker一起作为尝试,将其余密码应用为p(我在此不再详述)。如果您发现一系列其他密码等于passwords,则不需要p,并且可以将其从p列表中删除。它应该在开始时完成一次。

只使用这些技术和增加递归限制,保持passwords缓存,才能解决这一挑战。

答案 2 :(得分:2)

这里有一些传递代码。我们不一定需要检查超集或使用聪明的预处理启发式方法。一个诀窍是,尽管可能存在过多的可能性,但它足以返回其中一个。这意味着,如果我们有一个解决方案直到索引i,我们可以自由地忽略形成相同长度前缀loginAttempt的所有其他可能性。这个Python代码使用两个列表,一个用于启动我们已访问过的索引,另一个用于保存所选择的密码序列,这些密码构成前缀,直到我们要检查的下一个起始索引。

def passwordCracker(passwords, loginAttempt):
    memo = [[]] + [None for i in xrange(len(loginAttempt))]
    stack = [0]
    visited = set()

    while stack:
      i = stack.pop() # next index

      if i in visited:
        continue

      visited.add(i)

      for idx, p in enumerate(passwords):
        if i + len(p) > len(loginAttempt):
          continue

        if loginAttempt[i:i + len(p)] == p:
          memo[i + len(p)] = memo[i][:] + [idx]
          stack.append(i + len(p))

    if memo[len(memo) - 1]:
      return " ".join(map(lambda i: passwords[i], memo[len(memo) - 1]))

    else:
      return "WRONG PASSWORD"