在程序中,我需要有效地回答以下形式的查询:
给定一组字符串
A
和查询字符串q
,返回所有s ∈ A
,使q为s
的{{3}}
例如,应该返回A = {"abcdef", "aaaaaa", "ddca"}
和q = "acd"
"abcdef"
。
以下是我到目前为止所考虑的内容:
对于每个可能的字符,请为其显示的所有字符串/位置创建一个排序列表。对于查询交错所涉及字符的列表,并扫描它以查找字符串边界内的匹配。
对于单词而不是字符,这可能更有效,因为有限数量的不同字符会使返回列表非常密集。
对于每个n前缀q
可能有的,存储所有匹配字符串的列表。 n
可能实际上接近3.对于长于此的查询字符串,我们强制使用初始列表。
这可能会加快速度,但可以很容易地想象A
中所有字符串附近都存在一些n子序列,这意味着最坏的情况与强制整个集合的粗暴相同。
您是否知道任何数据结构,算法或预处理技巧可能有助于大型A
s有效执行上述任务? (我的s
将大约100个字符)
更新:有人建议使用LCS检查q
是s
的子序列。我只是想提醒一下,这可以使用一个简单的函数来完成,例如:
def isSub(q,s):
i, j = 0, 0
while i != len(q) and j != len(s):
if q[i] == s[j]:
i += 1
j += 1
else:
j += 1
return i == len(q)
更新2:我被要求提供有关q
,A
及其元素性质的更多详情。虽然我更喜欢尽可能普遍运行的东西,但我认为A
的长度大约为10 ^ 6,并且需要支持插入。元素s
将更短,平均长度为64.查询q
将只有1到20个字符并用于实时搜索,因此查询“ab”将在之前发送查询“abc”。同样,我更倾向于使用上述解决方案尽可能少。
更新3:我发现,具有O(n^{1-epsilon})
查询的数据结构将允许您解决OVP /反驳SETH猜想。这可能是我们遭受苦难的原因。然后,唯一的选择是反驳猜想,使用近似或利用数据集。我想象quadlets和try会在不同的设置中完成最后一次。
答案 0 :(得分:8)
可以通过构建automaton
来完成。你可以从NFA
开始(非确定性有限自动机就像一个不确定的有向图),它允许标有epsilon
字符的边,这意味着在处理过程中你可以从一个节点跳到另一个节点而不消耗任何一个节点。字符。我会尝试减少你的A
。假设您A
是:
A = {'ab, 'bc'}
如果您为NFA
字符串构建ab
,则应该得到以下内容:
+--(1)--+
e | a| |e
(S)--+--(2)--+--(F)
| b| |
+--(3)--+
上图不是最好看的自动机。但有几点需要考虑:
S
状态是起始状态,F
是结束状态。F
状态,则表示您的字符串符合后续条件。e
(epsilon)来向前跳跃,因此你可以在每个时间点处于多于一个状态。这称为e
闭包。现在,如果给定b
,则从州S
开始,我可以跳过一个epsilon
,到达2
,然后消费b
并到达3
}。现在给定end
字符串我消耗epsilon
并到达F
,因此b
符合sub-sequence
ab
的条件。 a
或ab
您也可以尝试使用上述自动机。
关于NFA
的好处是他们有一个开始状态和一个最终状态。可以使用NFA
轻松连接两个epsilons
。有各种算法可以帮助您将NFA
转换为DFA
。 DFA
是一个有向图,它可以遵循给定角色的精确路径 - 特别是,它在任何时间点始终处于完全一个状态。 (对于任何NFA,都有相应的DFA,其状态对应于NFA中的状态集。)
因此,对于A = {'ab, 'bc'}
,我们需要为NFA
构建ab
,然后为NFA
构建bc
,然后加入两个NFAs
和构建整个大DFA
的{{1}}。
NFA
的子序列的NFA为abc
,因此您可以将NFA构建为:
现在,考虑输入a?b?c?
。要查询acd
是ab
的子序列,您可以使用此NFA:{'abc', 'acd'}
。拥有NFA后,您可以将其转换为DFA,其中每个州都将包含(a?b?c?)|(a?c?d)
或abc
或两者的子序列。
我使用下面的链接从正则表达式创建NFA图形:
http://hackingoff.com/images/re2nfa/2013-08-04_21-56-03_-0700-nfa.svg
你是对的!如果您在acd
中有10,000个唯一字符。通过唯一我的意思是A是这样的:A
即A的每个元素的交集是空集。然后你的DFA在状态方面是最糟糕的情况,即{'abc', 'def'}
。但是我不确定什么时候会有可能,因为永远不会有2^10000
个独特的字符。即使你在A中有10,000个字符仍然会有重复,这可能会减少很多状态,因为e-closure最终可能会合并。我无法估计它可能会减少多少。但即使拥有1000万个州,你也只能消耗不到10mb的空间来构建DFA。您甚至可以在运行时使用NFA并查找电子闭包,但这会增加运行时的复杂性。您可以搜索有关将大规模正则表达式转换为DFA的不同文章。
正则表达式10,000
如果您将上述NFA转换为DFA,则会获得:
实际上NFA的状态要少得多。
参考: http://hackingoff.com/compilers/regular-expression-to-nfa-dfa
更多地摆弄那个网站之后。我发现最坏的情况就是这样的A = {'aaaa','bbbbb','cccc'......}。但即便在这种情况下,国家也不如NFA国家。
答案 1 :(得分:7)
此主题中有四个主要提案:
Shivam Kalra建议根据A
中的所有字符串创建自动机。这种方法在文献中略有尝试,通常名称为“有向无环子序列图”(DASG)。
J Random Hacker建议将我的'前缀列表'想法扩展到查询字符串中的所有'n选择3'三元组,并使用堆合并它们。
在“数据库中的高效子序列搜索”中,Rohit Jain,Mukesh K. Mohania和Sunil Prabhakar建议使用Trie结构进行一些优化,并递归搜索树以查询查询。他们也有类似三元组理念的建议。
最后还有'天真'的方法,wanghq建议通过为A
的每个元素存储一个索引进行优化。
为了更好地了解值得继续努力的内容,我已经在Python中实现了上述四种方法,并对两组数据进行了基准测试。通过在C或Java中完善的实现,可以更快地实现几个实现;而且我没有包括针对'trie'和'naive'版本建议的优化。
A
由我文件系统中的随机路径组成。 q
是100个平均长度为7的随机[a-z]
字符串。由于字母表很大(Python很慢),我只能使用duplet作为方法3。
作为A
大小函数的施工时间(以秒为单位):
以A
大小为单位的查询时间(以秒为单位):
A
由随机抽样的[a-b]
字符串组成,长度为20. q
是100个随机[a-b]
字符串,平均长度为7.由于字母表很小,我们可以使用quadlets方法3。
作为A
大小函数的施工时间(以秒为单位):
以A
大小为单位的查询时间(以秒为单位):
双对数图有点难以阅读,但从数据中我们可以得出以下结论:
自动机在查询时非常快(恒定时间),但是无法为|A| >= 256
创建和存储它们。有可能进行更密切的分析可以产生更好的时间/记忆平衡,或者适用于其余方法的一些技巧。
dup- / trip- / quadlet方法的速度是我的trie实现速度的两倍,是'naive'实现速度的四倍。我只使用了线性数量的合并列表,而不是j_random_hacker建议的n^3
。有可能更好地调整方法,但总的来说这是令人失望的。
我的trie实现始终比天真的方法做得好一两倍左右。通过结合更多的预处理(例如“这个子树中下一个'c'在哪里”)或者可能将它与三元组方法合并,这似乎是今天的赢家。
如果你的性能可以降低,那么天真的方法相对来说只需很少的费用就可以了。
答案 2 :(得分:3)
正如您所指出的,可能是A中的所有字符串都包含q作为子序列,在这种情况下,您不能希望比O(| A |)做得更好。 (也就是说,对于A中的每个字符串i,你可能仍然可以比在{q,A [i])上运行LCS所做的更好,但我不会在这里关注它。)
TTBOMK没有神奇,快速的方法来回答这个问题(后缀树的方式是神奇,快速的方式来回答涉及子串而不是子序列的相应问题)。然而,如果您希望大多数查询的答案集平均较小,那么值得研究加速这些查询的方法(产生小规模答案的方法)。
我建议基于你的启发式(2)的推广进行过滤:如果某个数据库序列A [i]包含q作为子序列,那么它还必须包含q的每个子序列。 (不幸的是,反方向不正确!)因此,对于一些小的k,例如3如你所知,你可以通过构建一个列表数组来预处理,告诉你,对于每个长度为k的字符串s,包含s作为子序列的数据库序列列表。即c [s]将包含包含s作为子序列的数据库序列的ID号列表。保持每个列表的数字顺序,以便以后启用快速交叉。
现在,每个查询q的基本思想(我们将在一瞬间改进)是:查找q的所有k大小的子序列,在列表c []的数组中查找每个子序列,并将它们相交列表以查找A中可能包含q作为子序列的序列集。然后对于这个(希望很小的)交叉点中的每个可能的序列A [i],用q执行O(n ^ 2)LCS计算以查看它是否确实包含q。
一些观察结果:
答案 3 :(得分:2)
答案 4 :(得分:2)
首先让我确保我的理解/抽象是正确的。应满足以下两个要求:
请注意,A中的char可能会在B中出现多次。
要解决1),可以使用地图/集合。键是字符串B中的字符,值无关紧要。 要解决2),我们需要保持每个角色的位置。由于角色可能不止一次出现,因此该位置应该是一个集合。
所以结构就像:
Map<Character, List<Integer>)
e.g.
abcdefab
a: [0, 6]
b: [1, 7]
c: [2]
d: [3]
e: [4]
f: [5]
一旦我们有了结构,如何知道字符是否在字符串A
中的顺序正确?如果B
为acd
,我们应该检查位置0(但不是6)的a
,位置2的c
和位置3的d
。< / p>
此处的策略是选择之前和之前所选位置的位置。 TreeSet是此操作的理想选择。
public E higher(E e)
Returns the least element in this set strictly greater than the given element, or null if there is no such element.
运行时复杂度为O(s *(n1 + n2)* log(m)))。
a
。下面是一些测试数据的实现。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
public class SubsequenceStr {
public static void main(String[] args) {
String[] testSet = new String[] {
"abcdefgh", //right one
"adcefgh", //has all chars, but not the right order
"bcdefh", //missing one char
"", //empty
"acdh",//exact match
"acd",
"acdehacdeh"
};
List<String> subseqenceStrs = subsequenceStrs(testSet, "acdh");
for (String str : subseqenceStrs) {
System.out.println(str);
}
//duplicates in query
subseqenceStrs = subsequenceStrs(testSet, "aa");
for (String str : subseqenceStrs) {
System.out.println(str);
}
subseqenceStrs = subsequenceStrs(testSet, "aaa");
for (String str : subseqenceStrs) {
System.out.println(str);
}
}
public static List<String> subsequenceStrs(String[] strSet, String q) {
System.out.println("find strings whose subsequence string is " + q);
List<String> results = new ArrayList<String>();
for (String str : strSet) {
char[] chars = str.toCharArray();
Map<Character, TreeSet<Integer>> charPositions = new HashMap<Character, TreeSet<Integer>>();
for (int i = 0; i < chars.length; i++) {
TreeSet<Integer> positions = charPositions.get(chars[i]);
if (positions == null) {
positions = new TreeSet<Integer>();
charPositions.put(chars[i], positions);
}
positions.add(i);
}
char[] qChars = q.toCharArray();
int lowestPosition = -1;
boolean isSubsequence = false;
for (int i = 0; i < qChars.length; i++) {
TreeSet<Integer> positions = charPositions.get(qChars[i]);
if (positions == null || positions.size() == 0) {
break;
} else {
Integer position = positions.higher(lowestPosition);
if (position == null) {
break;
} else {
lowestPosition = position;
if (i == qChars.length - 1) {
isSubsequence = true;
}
}
}
}
if (isSubsequence) {
results.add(str);
}
}
return results;
}
}
输出:
find strings whose subsequence string is acdh
abcdefgh
acdh
acdehacdeh
find strings whose subsequence string is aa
acdehacdeh
find strings whose subsequence string is aaa
一如既往,我可能完全错了:)
答案 5 :(得分:1)
您可能希望了解Dan Gusfield的字符串和序列的书籍算法。事实证明,它的一部分可以在互联网上找到。您可能还想阅读Gusfield的Introduction to Suffix Trees。事实证明,这本书涵盖了许多方法,为您提出了一些问题。它被认为是该领域的标准出版物之一。
s ∈ A
length(LCS(s,q)) == length(q)
醇>