使用给定的单词列表重新创建给定的字符串的方法数量

时间:2019-06-02 10:04:17

标签: java

给出的是一个字符串单词,是一个包含一些字符串的类型为字符串 String [] book 的数组。该程序应该给出仅使用 book 中的元素来创建单词的可能性。一个元素可以根据需要 使用多次,并且程序必须在6秒内终止

例如,输入:

String word = "stackoverflow";

String[] book = new String[9];
book[0] = "st";
book[1] = "ck";
book[2] = "CAG";
book[3] = "low";
book[4] = "TC";
book[5] = "rf";
book[6] = "ove";
book[7] = "a";
book[8] = "sta";

输出应该为“ 2”,因为我们可以通过两种方式创建字符串stackoverflow:

1:“ st” +“ a” +“ ck” +“ ove” +“ rf” +“ low”

2:“ sta” +“ ck” +“ ove” +“ rf” +“ low”

仅当单词相对较小(<15个字符)时,我的程序实现才会在要求的时间终止。但是,如前所述,该程序的运行时间限制为6秒,它应该能够处理非常大的 word 字符串(> 1000个字符)。 Here是大量输入的示例。

这是我的代码:

1)实际方法:

input:一个String单词和一本String []书

输出:仅可以使用书中的字符串写单词的方式

public static int optimal(String word, String[] book){
    int count = 0;

    List<List<String>> allCombinations = allSubstrings(word);

    List<String> empty = new ArrayList<>();

    List<String> wordList = Arrays.asList(book);

    for (int i = 0; i < allCombinations.size(); i++) {

        allCombinations.get(i).retainAll(wordList);

        if (!sumUp(allCombinations.get(i), word)) {
            allCombinations.remove(i);
            allCombinations.add(i, empty);
        }
        else count++;
    }

    return count;
}

2)allSubstrings():

input:字符串输入

output:列表列表,每个列表包含加起来等于输入的子字符串组合

static List<List<String>> allSubstrings(String input) {

    if (input.length() == 1) return Collections.singletonList(Collections.singletonList(input));

    List<List<String>> result = new ArrayList<>();

    for (List<String> temp : allSubstrings(input.substring(1))) {

        List<String> firstList = new ArrayList<>(temp);
        firstList.set(0, input.charAt(0) + firstList.get(0));
        if (input.startsWith(firstList.get(0), 0)) result.add(firstList);

        List<String> l = new ArrayList<>(temp);
        l.add(0, input.substring(0, 1));
        if (input.startsWith(l.get(0), 0)) result.add(l);
    }

    return result;
}

3。)sumup():

input:一个字符串列表输入和一个预期的字符串

输出:如果输入中的元素合计为预期值,则为true

public static boolean sumUp (List<String> input, String expected) {

    String x = "";

    for (int i = 0; i < input.size(); i++) {
        x = x + input.get(i);
    }
    if (expected.equals(x)) return true;
    return false;
}

5 个答案:

答案 0 :(得分:2)

我在my previous answer中发现自己做错了什么:我没有使用备忘录,所以我在重做很多不必要的工作。

考虑书籍阵列{"a", "aa", "aaa"}和目标单词"aaa"。有四种方法可以构造此目标:

"a" + "a" + "a"
"aa" + "a"
"a" + "aa"
"aaa"

我以前的尝试将分别遍历所有四个。但是相反,人们可以观察到:

  • 有1种方法来构造"a"
  • 您可以用两种方式构造"aa",既可以使用"a" + "a",也可以直接使用"aa"
  • 您可以直接使用"aaa"(1种方式)来构造"aaa";或"aa" + "a"(有两种方式,因为构造"aa"有两种方式);或"a" + "aa"(1种方式)。

请注意,此处的第三步仅将一个附加字符串添加到先前构造的字符串中,对此我们知道其构造方式的数量。

这表明,如果我们计算可构造word前缀的方式数量,则可以通过从{{ 1}}。

我定义了一个简单的trie类,因此您可以快速查找与book中任何给定位置匹配的book单词的前缀:

word

对于class TrieNode { boolean word; Map<Character, TrieNode> children = new HashMap<>(); void add(String s, int i) { if (i == s.length()) { word = true; } else { children.computeIfAbsent(s.charAt(i), k -> new TrieNode()).add(s, i + 1); } } } 中的每个字母,这将创建s的实例,并为后续字符等存储TrieNode

TrieNode

对于very long input given by OP,这给出了输出:

static long method(String word, String[] book) {
  // Construct a trie from all the words in book.
  TrieNode t = new TrieNode();
  for (String b : book) {
    t.add(b, 0);
  }

  // Construct an array to memoize the number of ways to construct
  // prefixes of a given length: result[i] is the number of ways to
  // construct a prefix of length i.
  long[] result = new long[word.length() + 1];

  // There is only 1 way to construct a prefix of length zero.
  result[0] = 1;

  for (int m = 0; m < word.length(); ++m) {
    if (result[m] == 0) {
      // If there are no ways to construct a prefix of this length,
      // then just skip it.
      continue;
    }

    // Walk the trie, taking the branch which matches the character
    // of word at position (n + m).
    TrieNode tt = t;
    for (int n = 0; tt != null && n + m <= word.length(); ++n) {
      if (tt.word) {
        // We have reached the end of a word: we can reach a prefix
        // of length (n + m) from a prefix of length (m).
        // Increment the number of ways to reach (n+m) by the number
        // of ways to reach (m).
        // (Increment, because there may be other ways).
        result[n + m] += result[m];
        if (n + m == word.length()) {
          break;
        } 
      }
      tt = tt.children.get(word.charAt(n + m));
    }
  }

  // The number of ways to reach a prefix of length (word.length())
  // is now stored in the last element of the array.
  return result[word.length()];
}

比所需的6秒要快一点-这还包括JVM启动时间。


编辑:实际上,不需要Trie。您只需将“ Walk the trie”循环替换为:

$ time java Ideone

2217093120

real    0m0.126s
user    0m0.146s
sys 0m0.036s

它的执行速度较慢,但​​仍比6s快:

for (String b : book) {
  if (word.regionMatches(m, b, 0, b.length())) {
    result[m + b.length()] += result[m];
  }
}

答案 1 :(得分:1)

一些观察结果:

x = x + input.get(i);

在循环时,使用String +不是一个好主意。使用StringBuilder并将其追加到循环内以及结尾return builder.toString()内。或者您遵循Andy的想法。无需合并字符串,您已经知道目标词。见下文。

然后:List表示添加/删除元素可能会很昂贵。因此,请查看是否可以删除该部分,以及是否有可能使用地图,而是使用集合。

最后:真正的重点是研究您的算法。我会尝试“向后”。含义:首先确定目标词中实际出现的那些数组元素。您可以从一开始就忽略所有其他内容。

然后:查看所有以“ *开始” +您的搜索词开始的数组条目。在您的示例中,您会注意到只有两个适合的数组元素。然后从那里开始。

答案 2 :(得分:1)

我的第一个观察结果是您实际上不需要构建任何东西:您知道要构建的字符串(例如stackoverflow),因此,您真正需要跟踪的是到目前为止您已匹配的字符串。将此称为m

接下来,提供m,匹配了m < word.length()个字符,您需要从book中选择下一个字符串,该字符串与wordm的部分匹配到m + nextString.length()

您可以依次检查每个字符串来做到这一点:

if (word.matches(m, nextString, 0, nextString.length()) { ...}

但是,您可以通过预先确定不匹配的字符串来做得更好:您附加的下一个字符串将具有以下属性:

  1. word.charAt(m) == nextString.charAt(0)(下一个字符匹配)
  2. m + nextString.length() <= word.length()(添加下一个字符串不应使构造的字符串长于word

因此,您可以通过构建字母到以该字母开头的单词的映射(第1点)来减少书中可能要检查的单词。如果您以递增的顺序存储带有相同起始字母的单词,则可以在长度太大时停止检查该字母(第2点)。

您可以一次构建地图并重复使用:

Map<Character, List<String>> prefixMap =
    Arrays.asList(book).stream()
        .collect(groupingBy(
            s -> s.charAt(0),
            collectingAndThen(
                toList(),
                ss -> {
                  ss.sort(comparingInt(String::length));
                  return ss;
                })));

您可以递归计算方法的数量,而无需构造任何其他对象(*):

int method(String word, String[] book) {
  return method(word, 0, /* construct map as above */);
}

int method(String word, int m, Map<Character, List<String>> prefixMap) {
  if (m == word.length()) {
    return 1;
  }

  int result = 0;
  for (String nextString : prefixMap.getOrDefault(word.charAt(m), emptyList())) {
    if (m + nextString.length() > word.length()) {
      break;
    }

    // Start at m+1, because you already know they match at m.
    if (word.regionMatches(m + 1, nextString, 1, nextString.length()-1)) {
      // This is a potential match!
      // Make a recursive call.
      result += method(word, m + nextString.length(), prefixMap);
    }
  }
  return result;
}

(*)由于Character的装箱,可能会构造word.charAt(m)的新实例:保证缓存的实例只能用于0-127范围内的字符。有多种方法可以解决此问题,但是它们只会使代码混乱。

答案 3 :(得分:0)

我认为您已经在优化应用程序方面做得很好。除了 GhostCat 的答案以外,还有一些我自己的建议:

public static int optimal(String word, String[] book){

    int count = 0;

    List<List<String>> allCombinations = allSubstrings(word);
    List<String> wordList = Arrays.asList(book);

    for (int i = 0; i < allCombinations.size(); i++)
    {
        /*
         * allCombinations.get(i).retainAll(wordList);
         * 
         * There is no need to retrieve the list element
         * twice, just set it in a local variable
         */
        java.util.List<String> combination = allCombinations.get(i);
        combination.retainAll(wordList);
        /*
         * Since we are only interested in the count here
         * there is no need to remove and add list elements
         */
        if (sumUp(combination, word)) 
        {
            /*allCombinations.remove(i);
            allCombinations.add(i, empty);*/
            count++;
        }
        /*else count++;*/
    }
    return count;
}

public static boolean sumUp (List<String> input, String expected) {

    String x = "";

    for (int i = 0; i < input.size(); i++) {
        x = x + input.get(i);
    }
    // No need for if block here, just return comparison result
    /*if (expected.equals(x)) return true;
    return false;*/
    return expected.equals(x);
}

并且由于您对查看方法的执行时间感兴趣,因此建议您实施某种基准测试系统。这是一个快速的模型:

private static long benchmarkOptima(int cycles, String word, String[] book) {

    long totalTime = 0;
    for (int i = 0; i < cycles; i++)
    {
        long startTime = System.currentTimeMillis();

        int a = optimal(word, book);

        long executionTime = System.currentTimeMillis() - startTime;
        totalTime += executionTime;
    }
    return totalTime / cycles;
}

public static void main(String[] args)
{
    String word = "stackoverflow";
    String[] book = new String[] {
            "st", "ck", "CAG", "low", "TC",
            "rf", "ove", "a", "sta"
    };

    int result = optimal(word, book);

    final int cycles = 50;
    long averageTime = benchmarkOptima(cycles, word, book);

    System.out.println("Optimal result: " + result);
    System.out.println("Average execution time - " + averageTime + " ms");
}

输出

2
Average execution time - 6 ms

答案 4 :(得分:0)

注意:该实现被困在@ user1221提到的测试用例中,正在研究中。

我能想到的是一种基于Trie的方法,即O(sum of length of words in dict)空间。时间不是最佳时间。

程序:

  1. 构建字典中所有单词的特里。这是一项预处理任务,需要O(sum of lengths of all strings in dict)
  2. 我们尝试在曲折中找到您要在特里制作的字符串。我们首先搜索字符串的前缀。如果在树状结构中获得前缀,则从顶部开始递归搜索,并继续寻找更多前缀。
  3. 当我们到达输出字符串的末尾,即stackoverflow时,我们检查是否到达任何字符串的末尾,如果是,则达到此字符串的有效组合。我们在递归递归时计算这一点。

例如: 在上述情况下,我们将字典用作{"st", "sta", "a", "ck"} 我们构造了特里($是哨兵char,即不在字典中的char):

$___s___t.___a.
|___a.
|___c___k.

.表示字典中的一个单词在该位置结束。 我们尝试找到stack的构造编号。

我们开始在特里搜索stack

depth=0
$___s(*)___t.___a.
|___a.
|___c___k.

我们看到我们在一个单词的结尾,我们从顶部开始搜索剩下的字符串ack

depth=0
$___s___t(*).___a.
|___a.
|___c___k.

同样,我们在字典中只剩下一个字。我们开始对ck进行新搜索。

depth=1
$___s___t.___a.
|___a(*).
|___c___k.
depth=2
$___s___t.___a.
|___a.
|___c(*)___k.

我们到达stack的结尾和字典中一个单词的结尾,因此我们有1个有效的stack表示形式。

depth=2
$___s___t.___a.
|___a.
|___c___k(*).

我们返回到depth=2的呼叫方

下一个字符不可用,我们返回到depth=1的调用方。

depth=1
$___s___t.___a.
|___a(*, 1).
|___c___k.
depth=0
$___s___t(*, 1).___a.
|___a.
|___c___k.

我们移到下一个字符。我们看到我们在字典中到达了一个单词的结尾,我们在字典中对ck进行了新搜索。

depth=0
$___s___t.___a(*, 1).
|___a.
|___c___k.
depth=1
$___s___t.___a.
|___a.
|___c(*)___k.

我们到达stack的结尾,然后是字典中的作品,因此是另一种有效的表示形式。我们返回到depth=1

的调用方
depth=1
$___s___t.___a.
|___a.
|___c___k(*, 1).

没有更多的字符了,我们返回结果2

depth=0
$___s___t.___a(*, 2).
|___a.
|___c___k.

注意:该实现是用C ++编写的,不要太难转换为Java,并且该实现假定所有字符都为小写,将这两种情况都扩展很简单。

示例代码(full version):

/**
Node *base: head of the trie
Node *h   : current node in the trie
string s  : string to search
int idx   : the current position in the string
*/
int count(Node *base, Node *h, string s, int idx) {
    // step 3: found a valid combination.
    if (idx == s.size()) return h->end;

    int res = 0;
    // step 2: we recursively start a new search.
    if (h->end) {
        res += count(base, base, s, idx);
    }
    // move ahead in the trie.
    if (h->next[s[idx] - 'a'] != NULL) { 
        res += count(base, h->next[s[idx] - 'a'], s, idx + 1);
    }

    return res;
}