![](/img/trans.png)
[英]Given a string, count the number of words ending in 'y' or 'z'
[英]Number of ways to recreate a given string using a given list of words
给定的是一个字符串word
和一个包含一些字符串的字符串数组book
。 程序应该给出仅使用book
元素创建word
的可能性数量。 一个元素可以根据需要多次使用,并且程序必须在 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"
如果word
相对较小(<15 个字符),我的程序实现仅在所需的时间内终止。 然而,正如我之前提到的,程序的运行时间限制为6秒,它应该能够处理非常大的word
字符串(> 1000个字符)。 这是一个大输入的例子。
这是我的代码:
1)实际方法:
输入:一个字符串word
和一个 String[] book
输出:仅使用 book 中的字符串可以编写单词的方式数
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
输出:一个列表列表,每个列表包含加起来为输入的子串的组合
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.) 总结():
输入:一个字符串列表input
和一个expected
的字符串
输出:如果input
的元素加起来符合expected
则为真
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;
}
我已经弄清楚我在之前的回答中做错了什么:我没有使用记忆,所以我正在重做大量不必要的工作。
考虑一个 book 数组{"a", "aa", "aaa"}
和一个目标词"aaa"
。 有四种方法可以构建这个目标:
"a" + "a" + "a"
"aa" + "a"
"a" + "aa"
"aaa"
我之前的尝试将分别遍历所有四个。 但相反,人们可以观察到:
"a"
"aa"
, "a" + "a"
或直接使用"aa"
。"aaa"
来构造"aaa"
(1种方式); 或"aa" + "a"
(2 种方式,因为有 2 种方法可以构造"aa"
); 或"a" + "aa"
(1 种方式)。请注意,此处的第三步仅向先前构造的字符串添加一个额外的字符串,为此我们知道可以构造它的方式的数量。
这表明,如果我们计算可以构造word
前缀的方式的数量,我们可以使用它来简单地计算出更长的前缀的方式数量,方法是从book
再添加一个字符串。
我定义了一个简单的 trie 类,因此您可以快速查找与word
中任何给定位置匹配的book
单词的前缀:
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
。
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()];
}
$ time java Ideone
2217093120
real 0m0.126s
user 0m0.146s
sys 0m0.036s
比所需的 6 秒快很多 - 这也包括 JVM 启动时间。
编辑:事实上,trie 是没有必要的。 您可以简单地将“Walk the trie”循环替换为:
for (String b : book) {
if (word.regionMatches(m, b, 0, b.length())) {
result[m + b.length()] += result[m];
}
}
它的执行速度较慢,但仍比 6s 快得多:
2217093120
real 0m0.173s
user 0m0.226s
sys 0m0.033s
一些观察:
x = x + input.get(i);
当您循环时,使用 String+ 不是一个好主意。 使用 StringBuilder 并附加到循环中,最后return builder.toString()
。 或者你遵循安迪的想法。 不需要合并字符串,你已经知道目标词了。 见下文。
那么: List
意味着添加/删除元素可能代价高昂。 所以看看你是否可以摆脱那部分,如果可以使用地图,集合代替。
最后:真正的重点是研究你的算法。 我会尝试“向后”工作。 含义:首先识别那些实际出现在你的目标词中的数组元素。 您可以从一开始就忽略所有其他人。
然后:查看所有 **start*+ 搜索词的数组条目。 在您的示例中,您可以注意到只有两个适合的数组元素。 然后从那里开始工作。
我的第一个观察是,您实际上不需要构建任何东西:您知道要构建的字符串(例如stackoverflow
),因此您真正需要跟踪的是到目前为止您匹配了多少该字符串. 称其为m
。
接下来,匹配m
字符,提供m < word.length()
,您需要从book
选择一个匹配从m
到m + nextString.length()
的word
部分的下一个字符串。
您可以通过依次检查每个字符串来做到这一点:
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)
的word.charAt(m)
:保证缓存的实例仅用于 0-127 范围内的字符。 有一些方法可以解决这个问题,但它们只会使代码变得混乱。
我认为您在优化应用程序方面已经做得很好。 除了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
注意:实现卡在@user1221 提到的测试用例中,正在处理它。
我能想到的是一种基于Trie的方法,它是O(sum of length of words in dict)
空间。 时间不是最佳的。
程序:
O(sum of lengths of all strings in dict)
。stackoverflow
,我们检查是否到达任何字符串的末尾,如果是,则我们到达了该字符串的有效组合。 我们在返回递归时计算这个。 例如:在上面的例子中,我们使用字典作为{"st", "sta", "a", "ck"}
我们构造我们的特里树( $
是哨兵字符,即不在字典中的字符) :
$___s___t.___a.
|___a.
|___c___k.
的.
表示 dict 中的单词在该位置结束。 我们试图找到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.
我们到达 dict 中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.
我们移动到下一个字符。 我们看到我们到达了 dict 中一个单词的末尾,我们在 dict 中启动了对ck
的新搜索。
depth=0
$___s___t.___a(*, 1).
|___a.
|___c___k.
depth=1
$___s___t.___a.
|___a.
|___c(*)___k.
我们到达stack
的末尾并在 dict 中工作,因此是另一个有效的表示。 我们回到depth=1
的调用者
depth=1
$___s___t.___a.
|___a.
|___c___k(*, 1).
没有更多的字符要处理,我们返回结果2
。
depth=0
$___s___t.___a(*, 2).
|___a.
|___c___k.
注意:该实现是在 C++ 中实现的,转换为 Java 应该不会太难,并且这个实现假设所有字符都是小写的,将它扩展到这两种情况是微不足道的。
示例代码(完整版):
/**
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;
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.