给出的是一个字符串单词,是一个包含一些字符串的类型为字符串 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;
}
答案 0 :(得分:2)
我在my previous answer中发现自己做错了什么:我没有使用备忘录,所以我在重做很多不必要的工作。
考虑书籍阵列{"a", "aa", "aaa"}
和目标单词"aaa"
。有四种方法可以构造此目标:
"a" + "a" + "a"
"aa" + "a"
"a" + "aa"
"aaa"
我以前的尝试将分别遍历所有四个。但是相反,人们可以观察到:
"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
中选择下一个字符串,该字符串与word
中m
的部分匹配到m + nextString.length()
。
您可以依次检查每个字符串来做到这一点:
if (word.matches(m, nextString, 0, nextString.length()) { ...}
但是,您可以通过预先确定不匹配的字符串来做得更好:您附加的下一个字符串将具有以下属性:
word.charAt(m) == nextString.charAt(0)
(下一个字符匹配)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)
空间。时间不是最佳时间。
程序:
O(sum of lengths of all strings in dict)
。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;
}