检查字符串是不是是从子字符串列表构建的算法

Posted

技术标签:

【中文标题】检查字符串是不是是从子字符串列表构建的算法【英文标题】:Algorithm for checking if a string was built from a list of substrings检查字符串是否是从子字符串列表构建的算法 【发布时间】:2011-08-25 04:39:35 【问题描述】:

给你一个字符串和一个字符串数组。如何快速检查这个字符串是否可以通过连接数组中的一些字符串来构建?

这是一个理论问题,出于实际原因我不需要它。但我想知道,是否有一些好的算法。

编辑 阅读一些我注意到的答案,这可能是 NP-Complete 问题。即使找到一个字符串子集,它们的长度也相同,因为给定的字符串是一个经典的子集求和问题。

所以我想这个问题没有简单的答案。

编辑

现在看来,这毕竟不是一个 NP 完全问题。那就更酷了:-)

编辑

我想出了一个通过一些测试的解决方案:

def can_build_from_substrings(string, substrings):
    prefixes = [True] + [False] * (len(string) - 1)
    while True:
        old = list(prefixes)
        for s in substrings:
            for index, is_set in enumerate(prefixes):
                if is_set and string[index:].startswith(s):
                    if string[index:] == s:
                        return True
                    prefixes[index + len(s)] = True
        if old == prefixes: # nothing has changed in this iteration
            return False

我相信时间是O(n * m^3),其中n 的长度为substringsm 的长度为string。你怎么看?

【问题讨论】:

感觉像是背包问题的变种。 你可以只使用数组中的字符串一次吗? 【参考方案1】:

注意:我在这里假设您可以多次使用每个子字符串。您可以通过更改我们定义子问题的方式来概括解决方案以包含此限制。这将对空间和预期运行时间产生负面影响,但问题仍然是多项式的。

这是一个动态规划问题。 (这是一个很好的问题!)

如果字符串S 可以使用子字符串列表W 写入,我们将composable(S, W) 定义为真。

S 是可组合的当且仅当:

    SW 中的子字符串 w 开头。 Sw 之后的其余部分也是可组合的。

让我们写一些伪代码:

COMPOSABLE(S, W):
  return TRUE if S = "" # Base case
  return memo[S] if memo[S]

  memo[S] = false

  for w in W:
    length <- LENGTH(w)
    start  <- S[1..length]
    rest   <- S[length+1..-1]
    if start = w AND COMPOSABLE(rest, W) :
      memo[S] = true # Memoize

  return memo[S]

该算法的运行时间为 O(m*n),假设子字符串的长度与字符串本身不是线性的 w/r/t,在这种情况下运行时间为 O(m*n^2)(其中 m是子字符串列表的大小,n 是相关字符串的长度)。它使用 O(n) 空间进行记忆。

(注意,伪代码使用 O(n^2) 空间,但是对记忆键进行散列可以缓解这种情况。)

编辑

这是一个有效的 Ruby 实现:

def composable(str, words)
  composable_aux(str, words, )
end

def composable_aux(str, words, memo)
  return true if str == ""                # The base case
  return memo[str] unless memo[str].nil?  # Return the answer if we already know it

  memo[str] = false              # Assume the answer is `false`

  words.each do |word|           # For each word in the list:
    length = word.length
    start  = str[0..length-1]
    rest   = str[length..-1]

    # If the test string starts with this word,
    # and the remaining part of the test string
    # is also composable, the answer is true.
    if start == word and composable_aux(rest, words, memo)
      memo[str] = true           # Mark the answer as true
    end
  end

  memo[str]                      # Return the answer
end

【讨论】:

这听起来好多了。这是否意味着它不是真正的 NP-Complete 或者这里是运行时伪多项式? 我的回答假设您可以多次使用子字符串,这稍微简化了解决方案。但是,即使您添加了该限制,问题也不是 NP-Complete。 (虽然你会使用更多的内存!)动态编程需要最优的子结构和重叠的子问题。您在这里拥有这两个属性。 @grus:这里的区别在于输入的大小不同,因此对应NP-Complete问题的伪多项式算法,实际上是您的问题的多项式时间算法。例如,如果 Subset-Sum 的输入是一元的,那么它是 P。这就是为什么人们说强 NP-Complete,它仍然是 NP-Complete,即使输入是一元的,例如装箱。 @grus:例如,如果您的字母表只有一个字符“a”。并且输入是一个长度数组(而不是字符串本身),那么您的问题将是 NP-Complete。 酷,我可以看到解决问题的不同方法。然而,还有另一个问题:如果不能两次选择相同的字符串,如何解决这个问题?我想知道,这里是否可以使用与 0-1 背包问题相同的技巧。无论如何,思考这个问题教会了我很多关于 DP 的知识。非常感谢:-)【参考方案2】:

这绝对不是很快,但你有一个想法:

遍历所有字符串,检查目标字符串是否以其中任何一个“开头” 取目标字符串开头的最长字符串,将其从列表中删除并从主字符串中修剪 冲洗,重复

当你得到一个长度为 0 的目标字符串时停止。

正如我之前所说,这绝对不是很快,但应该给你一个基线(“它不应该比这更糟”)。

编辑

正如 cmets 中所指出的,这是行不通的。您将不得不存储部分匹配项,并在您发现没有其他方法时依靠它们。

当您发现一个字符串是目标的头部时,将其推送到一个列表中。建好列表后,自然会尝试目标的最大“头” 当您发现您尝试的头部与剩下的不匹配时,请尝试下一个最佳头部

这样,您最终将探索整个解决方案空间。对于每个候选头部,您将尝试所有可能的尾部。

【讨论】:

根据主字符串的大小,您可能希望使用从该数组中的字符串构造的状态机来发现与数组项的子字符串匹配。运行时,您需要考虑到字符串可能从任何位置开始,因此初始(未检查字符)状态对于每个位置都是可能的 - 需要通常的多状态一次有效非确定性有限自动机样式引擎,即使状态模型可以是确定性的(如果您不想最小化,可能是一个简单的树)。基本上是带有有限选择正则表达式的正则表达式搜索。 一旦你有一组(可能重叠)匹配的子字符串,那就是变得贪婪(或动态)的时候了。 这可能会产生误报 字符串开头不正确的最大子字符串方法...例如,字符串是“abcdefght”,可用的子字符串是“ab”、“abc”、“abcde”、“cdef” , "ght"... 在这种情况下,最大的起始子字符串是“abcde”,但实际上构成字符串的子字符串是“ab”、“cdef”、“ght”...。所以在第一遍你会发现所有的子字符串,然后尝试回溯的变化...... @S M Kamran 很好,你是对的。一次通过是不够的。【参考方案3】:

我会这样做。

    确定目标字符串的长度。 确定子字符串数组中每个字符串的长度 确定哪个子字符串组合会产生与目标字符串长度相同的字符串(如果有,如果没有,你就完成了) 生成步骤 3 中确定的子字符串组合的所有排列。检查它们是否与目标字符串匹配。

生成所有排列是一项繁重的处理器任务,因此如果您可以减少“n”(输入大小),您将获得相当可观的效率。

【讨论】:

第 1 步到第 3 步等效于 subset sum problem,但这仍然是对蛮力的相当大的改进。 :) +1 用于检查是否可以首先形成正确长度的字符串 - 可以节省大量时间。但是,正如您所说,生成排列很慢(因为您得到的排列太多),并且有一些方法可以避免基于子字符串匹配考虑其中的大部分。【参考方案4】:

灵感来自@cnicutars 答案:

函数Possible(array A, string s) 如果s 为空,则返回true。 计算A 中所有以s 为前缀的字符串的数组P。 如果P为空,则返回false。 对于P 中的每个字符串p: 如果Possible(A with p removed, s with prefix p removed) 返回真 返回假

【讨论】:

s with p removed -> removed where,多次出现不一样,如果你在一个地方删除,或者在另一个地方它变化很大,你可以给出假阴性:“1234123”:“12 ","34","123" p 应该从S 的开头删除,因为它是一个前缀。我已经更新了我的答案以澄清。【参考方案5】:

想到了两个选项,但它们看起来都不是很优雅。

1) 蛮力:像使用密码生成器一样进行操作,即 word1+word1+word1 > word1+word1+word2 > word1+word1+word3 等等等等

诀窍在于长度,因此您必须尝试 2 个或更多单词的所有组合,而您不知道在哪里设置限制。非常耗时。

2) 获取有问题的字符串,并为您每次拥有 1 个单词的每个单词运行查找。也许检查长度,如果它大于 0 再做一次。继续这样做,直到你达到零它找不到更多的结果。如果你击中 0 则为胜利,如果不是则为失败。我认为这种方法会比第一种方法好很多,但我想有人会有更好的建议。

【讨论】:

【参考方案6】:

在我看来,一个问题可以通过简单的线性遍历数组和比较来解决。但是可能有多次通过。你可以设计一个策略来减少传球次数。例如,在第一遍中构造原始字符串的所有子字符串的子数组。然后线性尝试不同的变化。

【讨论】:

类似这样的东西 - 称为 N 的排列 - 对于大小与原始字符串相等的子字符串中大小为 N 的每个排列,检查它,如果没有,则对 N+1 项进行排列,以此类推 感谢您给这个关键字:P【参考方案7】:

这是一个应该可行的粗略想法。

    将源字符串复制到新字符串中 虽然复制字符串仍有数据且仍有子字符串 一种。抓取一个子字符串,如果 copy.contains(substr) copy.remove(substr) 如果副本现在为空,那么可以,您可以构造字符串 如果 copy 不为空,则丢弃从字符串中删除的第一个 substr 并重复。 如果所有子字符串都消失了并且副本仍然不为空,那么不,您无法构造它。

编辑: 一种可能改善这一点的方法是首先迭代所有子字符串并丢弃任何未包含在主字符串中的子字符串。然后完成以上步骤。

【讨论】:

你当然可以通过长度检查和其他东西来改进它,这只是一个粗略的想法。 子字符串可以出现在多个位置,从一个位置或另一个位置删除是不一样的,这和其他想法一样会产生假阴性 @Marino 打得好,没想到一路走来。【参考方案8】:

让我建议使用 Suffix Trees(使用 Ukkonen 的在线算法来构建它),这似乎适合在两个文本中搜索公共子字符串。 您可以在***/特殊来源中找到更多信息。 任务是

Find all z occurrences of the patterns P1..Pn of total length m
enter code hereas substrings in O(m + z) time.

所以你看到存在非常酷的解决方案。希望这对你有用。 这实际上更适合重复扫描,而不是单次扫描。

【讨论】:

【参考方案9】:

如果每个子字符串只能使用一次,但不是全部都必须使用...

对于大小等于原始字符串的子字符串中大小为 N 的每个排列,检查它,如果没有,则对 N+1 项进行排列,依此类推,直到用完所有排列。

当然 NP 完全,慢得要命,但我认为不存在正常的解决方案。

解释为什么从原始字符串中删除子字符串的解决方案永远行不通:

有一个字符串“1234123”和数组“12”、“34”、“123”。如果您从一开始就删除“123”,那么您就有了误报。从末尾删除的类似示例是:“1234123”:“23”、“41”、“123”。

使用贪心回溯:(m string length 7, n num elements 3) - 最长:123 - 从第一次出现 O(3) 中删除它 - 尝试其他两个:no go + O((n-1)*(m-3)) - 回溯 O(1) - 从第二个移除:O(m-3) - 尝试其他两个 O((n-1)*m-3) = O(30)

1 + 2 + 3 = O(3) + O(4) + O(6) = O(13) 的排列。 因此,对于小的子集长度,排列实际上比回溯更快。如果您要求查找大量子字符串(在大多数情况下但不是全部),这将改变。

您可以仅从数组中删除不存在的子字符串,以将每个已删除的不存在子字符串的排列数从 n^n 减少到 n^(n-1)。

【讨论】:

【参考方案10】:

您正在寻找的是解析器。解析器将检查某个单词是否属于某种语言。我不确定您的问题的确切计算复杂性。以上一些似乎是正确的(根本不需要详尽的搜索)。可以肯定的一件事,它不是 NP-Complete。

您的语言的字母表将是所有小的子字符串。 您要查找的单词是您拥有的字符串。 正则表达式可以是一个简单的 Kleene 星号,也可以是一个非常简单的上下文无关文法,只不过是 Or's。

算法中的主要问题是:如果某些子字符串实际上是其他子字符串的子字符串......也就是说,如果我们有子字符串:“ab”,“abc”,“abcd”,...... . , 在这种情况下,检查子串的顺序会改变复杂度。为此,我们有 LR 解析器。我想他们是解决此类问题的最佳人选。

我会尽快为您找到确切的解决方案。

【讨论】:

以上是关于检查字符串是不是是从子字符串列表构建的算法的主要内容,如果未能解决你的问题,请参考以下文章

如何检查字符串是不是包含字符列表?

python如何检查字符串是不是是字符串列表的元素[重复]

如何检查列表列表中的所有元素是不是都是字符串

从子字符串结果中删除可能的尾随空格

检查一对字符串是不是是压缩列表的子字符串 - python

如何检查字符串是不是是字符串列表中项目的子字符串?