查找数组中每个字符串的最小唯一子字符串
Posted
技术标签:
【中文标题】查找数组中每个字符串的最小唯一子字符串【英文标题】:Find the smallest unique substring for each string in an array 【发布时间】:2012-06-30 00:32:41 【问题描述】:(我是在 javascript 的上下文中编写的,但会接受任何语言的算法正确答案)
如何在字符串数组中找到每个元素的最短子字符串,其中子字符串不包含在任何其他元素中,忽略大小写?
假设我有一个输入数组,例如:
var names = ["Anne", "Anthony", "LouAnn", "Kant", "Louise", "ark"];
输出应该是这样的:
var uniqueNames = ["ne", "h", "ua", "ka", "i", "r"];
出于我的目的,您可以放心地假设没有元素会完全包含在另一个元素中。
我的想法: 似乎有人可能会按照以下方式强制执行此操作:
var names = ["Anne", "Anthony", "LouAnn", "Kant", "Louise", "ark"];
var uniqueNames = [], nameInd, windowSize, substrInd, substr, otherNameInd, foundMatch;
// For each name
for (nameInd = 0; nameInd < names.length; nameInd++)
var name = names[nameInd];
// For each possible substring length
windowLoop:
for (windowSize = 1; windowSize <= name.length; windowSize++)
// For each starting index of a substring
for (substrInd = 0; substrInd <= name.length-windowSize; substrInd++)
substr = name.substring(substrInd,substrInd+windowSize).toLowerCase();
foundMatch = false;
// For each other name
for (otherNameInd = 0; otherNameInd < names.length; otherNameInd++)
if (nameInd != otherNameInd && names[otherNameInd].toLowerCase().indexOf(substr) > -1)
foundMatch = true;
break;
if (!foundMatch)
// This substr works!
uniqueNames[nameInd] = substr;
break windowLoop;
但我必须想象有一个更优雅的解决方案,使用尝试/前缀树、后缀数组或类似的东西。
编辑: 我相信这是所选答案在 JavaScript 中以编程方式采用的形式:
var names = ["Anne", "Anthony", "LouAnn", "Kant", "Louise", "ark"];
var uniqueNames = [], permutations = , permutation, nameInd, windowSize, substrInd, substr;
// For each name
for (nameInd = 0; nameInd < names.length; nameInd++)
var name = names[nameInd];
// For each possible substring length
windowLoop:
for (windowSize = 1; windowSize <= name.length; windowSize++)
// For each starting index of a substring
for (substrInd = 0; substrInd <= name.length-windowSize; substrInd++)
substr = name.substring(substrInd,substrInd+windowSize).toLowerCase();
permutations[substr] = (typeof permutations[substr] === "undefined")?nameInd:-1;
for (substr in permutations)
permutation = permutations[substr];
if (permutation !== -1 && ((typeof uniqueNames[permutation] === "string" && substr.length < uniqueNames[permutation].length) || typeof uniqueNames[permutation] === "undefined"))
uniqueNames[permutation] = substr;
【问题讨论】:
您的样本输出不正确吗?我在那里看不到s
和y
,而看到i, h
和r
...
@Icarus 啊,好点子。 s
和 y
不存在只是因为我不是在寻找所有符合标准的最小子字符串,而是任何一个都足够好。我会接受一个返回所有它们的二维数组的答案,但我真的不需要那种详细程度的细节。同样有效的输出可能是var uniqueNames = ["ne", "y", "ua", "ka", "i", "s"];
是否可以将您的输入字母限制为 26 个字符(或类似的,只是限制它)?
@SaeedAmiri 我不太确定你要走哪条路线,但我的实际输入仅包含 [0-9a-zA-Z_-'&,\.\s] 中的字符在输入中,您可以将输出限制为仅包含字母数字字符,尽管我可能会选择限制较少的答案而不是限制较多的答案,你知道吗?
@Patrick 有一个使用后缀数组的 O(M) 解决方案;其中 M 是所有字符串的长度之和。
【参考方案1】:
这个问题可以在 O(N*L*L*L) 复杂度中解决。该方法将使用后缀尝试。 trie 的每个节点还将存储前缀计数,该计数表示从根遍历到该节点时形成的子字符串出现在所有插入的后缀中的次数。
我们将构建 N+1 次尝试。第一个 trie 将是全局的,我们将在其中插入所有 N 字符串的所有后缀。对于包含相应后缀的 N 个字符串,接下来的 N 个尝试将是本地的。
构造尝试的这个预处理步骤将在 O(N*L*L) 中完成。
现在,一旦构建了尝试,对于每个字符串,我们可以开始查询子字符串(从最小长度开始)在全局 trie 和对应于该字符串的 trie 中出现的次数。如果两者都相同,则意味着它不包含在除自身之外的任何其他字符串中。这可以在 O(N*L*L*L) 中实现。复杂度可以解释为 N 代表每个字符串,L*L 代表考虑每个子串,L 代表在 trie 中执行查询。
【讨论】:
【参考方案2】:如果你构建一个通用后缀树,你只需要找到每个字符串的中缀从其他字符串的中缀分支的最浅点,并将标签带到该分支点加上一个“区分”字符.关键是必须有这样一个额外的字符(它可能只在每个字符串末尾的元字符处分支),并且分支点可能不会导致叶子,它可能会导致子树with 全部来自同一个字符串(因此必须考虑内部节点)。
对于每个字符串 S,找到最浅的(按父标签深度)节点 N,它只包含来自 S 的叶子,并且其边缘标签包含至少一个字符。从根到 N 的父节点的路径标签,加上从通往 N 的边缘标签的一个字符,是 S 的最短中缀,在其他字符串中找不到。
我相信只包含一个字符串中的叶子的节点的标记可以在构造期间或通过 GST 的 O(N) 扫描来完成;那么扫描最终的树并为每个字符串保持运行最小值是一件简单的事情。所以都是O(N)。
(编辑——我还不能回复 cmets)
为了澄清,后缀树中的每个后缀都有一个节点,它从其他后缀分支出来;这里的目标是找到每个字符串的 /a 后缀,该字符串在最小深度处从所有其他字符串的后缀 分支出来,该深度由到该节点的路径标签来衡量。我们所需要的只是在该点之后的一个额外字符,以拥有一个不会出现在任何其他字符串中的子字符串。
例子:
字符串:abbc、abc
使用 Ukonnen 算法,在第一个字符串之后,我们有一个仅包含该字符串后缀的后缀树;我将在这里用 [1] 标记它们:
abbc[1]
b
bc[1]
c[1]
c[1]
接下来我们插入字符串 2 的后缀:
ab
bc[1]
c[2]
b
bc[1]
c
[1]
[2]
c
[1]
[2]
现在我们要找到最短的字符串,它可以通向一个只有 [1] 的分支;我们可以通过扫描所有 [1] 并查看他们的直系父母来做到这一点,我将在此处按路径标签列出,加上一个字符(我将在下面使用):
abbc: abb
bbc: bb
bc: bc[1]
c: c[1]
请注意,我已包含 [1],因为它是区分 [1] 和 [2] 的其他相同后缀的元字符。这在识别在多个字符串中重复的子字符串时很方便,但它对我们的问题没有用,因为如果我们删除 [1],我们最终会得到一个出现在 [2] 中的字符串,即它不是候选字符串。
现在,右边的标签都没有出现在任何其他字符串中,所以我们选择最短的不包括元字符的标签,即 bb。
同样,第二个字符串有这些候选:
abc: abc
bc: bc[2]
c: c[2]
只有一个结尾没有元字符,所以我们必须使用 abc。
我的最后一点是,每个字符串的最小查找不必一次发生一次;可以扫描 GST 一次以将节点标记为包含来自一个字符串 ([1],[2],..[n]) 或“混合”的叶子,然后是每个字符串的最小非共享字符串(我会将这些称为“区分中缀”)也可以一次性计算出来。
【讨论】:
这听起来像我想象的有趣的方法可能存在,但我仍然没有完全想象这会是什么样子。能否麻烦您添加一些类似伪代码或算法步骤的内容。如果我能把它变成 O(N),我肯定会把我的选择移到这个答案上。 这是同一算法的另一种解释:reddit.com/r/algorithms/comments/372egn/…【参考方案3】:说N
是字符串的数量,L
是字符串的最大长度。您最多可以进行N*L*L*N
迭代。
我只能通过用一次迭代换取额外内存来稍微改进它。对于每个可能的子字符串长度(L
迭代),
枚举每个名称 (N*L
) 中该长度的所有子字符串,并将其与名称的索引一起存储到哈希表 (1
) 中。如果这个子字符串已经有一个索引,你知道它不会工作,那么你用一些特殊的值替换索引,比如-1
。
遍历哈希表,获取索引不是-1
的子字符串——这是它们对应索引的答案,但只有在这些名称在之前的迭代中没有更短的答案时才使用它们
通过将引用存储回现有字符串而不是复制子字符串,可以大大减少内存使用量。
【讨论】:
由于似乎没有人真正提出与最初提供的蛮力完全不同的算法,我将接受这个答案作为更明确定义的改进建议。 不过,我有点不同意你的大 O 估计。由于 indexOf 是对L
的迭代操作,我相信原来的蛮力会更像O(N*L*L*N*L)
。因此,删除最后一个 N*L
并迭代原始数组所有元素的所有可能排列的哈希表似乎只是稍微好一点。不过,使用金丝雀数组,迭代数组可能会更小。【参考方案4】:
for(String s : strArr) //O(n)
//Assume the given string as shortest and override with shortest
result.put(s, s);
for(int i = 0; i < s.length(); i++) // O(m)
for (int j = i + 1; j <=s.length(); j++)
String subStr = s.substring(i, j);
boolean isValid = true;
for(String str2: strArr) // O(n)
if(str2.equals(s)) // Same string cannot be a substring
continue;
if(str2.contains(subStr))
isValid = false;
break;
if(isValid && subStr.length() < result.get(s).length())
result.put(s, subStr);
return result;
【讨论】:
以上是关于查找数组中每个字符串的最小唯一子字符串的主要内容,如果未能解决你的问题,请参考以下文章