【问题标题】:Create given string from dictionary entries从字典条目创建给定的字符串
【发布时间】:2019-03-16 20:44:45
【问题描述】:

在最近的一次求职面试中,我被要求给出一个解决以下问题的方法:

给定一个字符串s(不带空格)和一个字典,返回字典中组成字符串的单词。

例如,s= peachpie, dic= {peach, pie}, result={peach, pie}

我会问这个问题的决策变化:

如果s 可以由 字典返回yes,否则 返回no

我的解决方案是回溯(用 Java 编写)

public static boolean words(String s, Set<String> dictionary)
{
    if ("".equals(s))
        return true;

    for (int i=0; i <= s.length(); i++)
    {
        String pre = prefix(s,i); // returns s[0..i-1]
        String suf = suffix(s,i); // returns s[i..s.len]
        if (dictionary.contains(pre) && words(suf, dictionary))
            return true;
    }
    return false;
}

public static void main(String[] args) {
    Set<String> dic = new HashSet<String>();
    dic.add("peach");
    dic.add("pie");
    dic.add("1");

    System.out.println(words("peachpie1", dic)); // true
    System.out.println(words("peachpie2", dic)); // false
}

这个解决方案的时间复杂度是多少? 我在 for 循环中递归调用,但只针对字典中的前缀。

有什么想法吗?

【问题讨论】:

  • 单词是否只需要由字典中的单词组成?例如,如果 apple 不在字典中,applepie 的结果是什么?
  • @Gumbo 查看他的示例,最后一行包含一些但不是所有单词(缺少#2),因此字典必须包含所有单词。允许复读吗?例如,给定示例中的字典,“peachpeach”应该是真还是假?
  • @Mackey Repitition 没有定义,但我假设它是允许的
  • @Gumbo,applepie 的结果显然是错误的。看问题的定义:如果s可以由字典中的单词组成,则返回yes。 apple 不在字典中
  • @Matt 之所以有效,是因为我在后缀上递归调用 words

标签: algorithm complexity-theory backtracking


【解决方案1】:

您可以轻松创建一个程序至少需要指数级时间才能完成的案例。让我们只用一个词aaa...aaab,其中a 重复n 次。字典将只包含两个词,aaa

b 最后确保函数永远不会找到匹配项,因此永远不会过早退出。

在每次执行 words 时,将产生两个递归调用:suffix(s, 1)suffix(s, 2)。因此,执行时间像斐波那契数一样增长:t(n) = t(n - 1) + t(n - 2)。 (您可以通过插入一个计数器来验证它。)因此,复杂性肯定不是多项式的。 (这甚至不是最糟糕的输入)

但您可以使用Memoization 轻松改进您的解决方案。请注意,函数words 的输出仅取决于一件事:我们从原始字符串的哪个位置开始。即,如果我们有一个字符串 abcdefg 并且调用了 words(5),那么 abcde 的确切组成并不重要(如 ab+c+dea+b+c+d+e 或其他东西)。因此,我们不必每次都重新计算words("fg")
在原始版本中,可以这样完成

public static boolean words(String s, Set<String> dictionary) {
    if (processed.contains(s)) {
        // we've already processed string 's' with no luck
        return false;
    }

    // your normal computations
    // ...

    // if no match found, add 's' to the list of checked inputs
    processed.add(s);
    return false;
}

PS 尽管如此,我还是鼓励您将words(String) 更改为words(int)。这样您就可以将结果存储在数组中,甚至可以将整个算法转换为 DP(这将使其更简单)。

编辑 2
由于除了工作我没有太多事情要做,这里是DP(动态编程)的解决方案。和上面的想法一样。

    String s = "peachpie1";
    int n = s.length();
    boolean[] a = new boolean[n + 1];
    // a[i] tells whether s[i..n-1] can be composed from words in the dictionary
    a[n] = true; // always can compose empty string

    for (int start = n - 1; start >= 0; --start) {
        for (String word : dictionary) {
            if (start + word.length() <= n && a[start + word.length()]) {
                // check if 'word' is a prefix of s[start..n-1]
                String test = s.substring(start, start + word.length());
                if (test.equals(word)) {
                    a[start] = true;
                    break;
                }
            }
        }
    }

    System.out.println(a[0]);

【讨论】:

  • 不是那么容易.. 我用 n=20 运行它,它需要 28656 次迭代。因此,如果 x == ~1.67, (1.67^20 == 28656)... 这看起来并非如此
  • @Itsik 您无法根据一种测量来估计算法的复杂性:总是有更小的常数。否则,我总是可以使用 n=3 并说复杂性是 O(1) :) 尝试为 n=3035 运行它,看看它的增长情况。
  • @Itsik 为什么2 很重要?也许我在给你的第一条评论中问了一个错误的问题:我应该问x = 1.67 有什么问题。 (或任何其他数字)1.67^n 仍然呈指数增长。看,你甚至不能在合理的时间内处理n = 40 :)
  • @Itsik 我仍然认为这个例子中的复杂性是用斐波那契数来近似的(在每一步你调用words 两次:子字符串短一个字符,两个字符短)。问题是 1) 斐波那契数函数本身不是指数函数,但它可以从下面以指数函数为边界(易于显示)和 2) 为这个例子找到复杂度函数不会给你算法的复杂性:这很远从最坏的例子。
  • @Saeed 您可以阅读帖子,而不是阅读第一条评论。我说得很清楚,这是对于这个特定输入的复杂性。我还明确指出,有更糟糕的输入,但即使对于这一点,复杂性已经是指数级的。
【解决方案2】:

这是一个动态编程解决方案,它计算将字符串分解为单词的方式总数。它解决了您最初的问题,因为如果分解次数为正,则字符串是可分解的。

def count_decompositions(dictionary, word):
    n = len(word)
    results = [1] + [0] * n
    for i in xrange(1, n + 1):
        for j in xrange(i):
            if word[n - i:n - j] in dictionary:
                results[i] += results[j]
    return results[n]

存储 O(n),运行时间 O(n^2)。

【讨论】:

  • 我写信给 Nikita 时,我喜欢动态编程方法,它快得多。但我问这个问题是因为我无法分析自己的算法。
  • @Paul:你能解释一下这个算法是如何工作的,或者至少“结果”的内容是什么意思吗?我很难理解这应该实现的目标,以及它如何计算分解次数。
  • @divegeek:我会试一试。 results[i] 是使用字典单词分解字符串的最后一个 i 字符的方式数。我们从字符串的末尾向后工作,总是使用我们已经计算的后缀的分解计数。如果results[i] 仅存储 1 表示“最后 i 个字符的至少 1 次分解是可能的”,则更容易理解,否则为 0,在这种情况下,最里面的 if 将被写入 if word[n-i:n-j] in dictionary and results[j] == 1 then results[i] = 1
  • @divegeek:在我提出的简化算法中,外层循环按长度递增的顺序遍历每个后缀。内部 (j) 循环本质上是在询问:“是否有任何较短的 j 字符可分解后缀可用于获得 i 字符的可分解后缀?” (记住j &lt; i。)事实上,不仅可以计算这是否可能,而且实际上不同分解的数量只是一些花哨的炫耀:)
  • @j_random_hacker:您可以通过将代码中的+= 更改为|= 来完成更简单的版本。
【解决方案3】:

所有字符串的循环将采用n。查找所有后缀和前缀将采用n + (n - 1) + (n - 2) + .... + 1n 用于第一次调用words(n - 1) 用于第二次等等),即

n^2 - SUM(1..n) = n^2 - (n^2 + n)/2 = n^2 / 2 - n / 2

在复杂性理论中相当于 n^2。

在正常情况下检查 HashSet 中是否存在是 Theta(1),但在最坏的情况下是 O(n)。

因此,算法的正常情况复杂度为 Theta(n^2),最坏情况 - O(n^3)。

编辑:我混淆了递归和迭代的顺序,所以这个答案是错误的。实际上,时间以指数方式取决于n(例如,与斐波那契数的计算相比)。

更有趣的是如何改进算法的问题。传统上使用 suffix tree 进行字符串操作。您可以使用字符串构建后缀树,并在算法开始时将所有节点标记为“未跟踪”。然后遍历集合中的字符串,每次使用某个节点时,将其标记为“已跟踪”。如果集合中的所有字符串都在树中找到,则意味着原始字符串包含集合中的所有子字符串。如果所有节点都标记为已跟踪,则意味着该字符串由集合中的子字符串组成。

这种方法的实际复杂度取决于许多因素,例如建树算法,但至少它允许将问题分成几个独立的子任务,因此可以通过最昂贵的子任务的复杂度来衡量最终的复杂度。

【讨论】:

  • 很不错O(n^3):你得等一分钟才能回复n = 40 :)
猜你喜欢
  • 2021-12-01
  • 2023-04-04
  • 1970-01-01
  • 2014-04-19
  • 2016-12-02
  • 2011-07-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多