很抱歉长标题:)
在此问题中,我们的长度为S
的字符串n
和长度为T
的字符串m
。我们可以在时间复杂度O(n + m)中检查S
是否为subsequence字符串T
。这很简单。
我很好奇:如果我们最多可以删除K
个连续字符怎么办?例如,如果K = 2
,我们可以从"ab"
开始"accb"
,但不能从"abcccb"
开始。我想检查一下是否可能非常快。
我只能找到明显的O(nm)
:检查字符串S
和字符串T
中的每个后缀对是否可能。我认为也许贪婪的算法是可能的,但如果K = 2
,案例S = "abc"
和T = "ababbc"
是一个反例。
有没有解决这个问题的快速解决方案?
答案 0 :(得分:4)
(更新:我重写了这个答案的开头,包括对复杂性的讨论以及讨论一些替代方法和潜在风险。)
(简短回答,我能想到的唯一真正改进的O(nm)方法是观察我们通常不需要计算所有 n 次表中的m 条目。我们只能计算出我们需要的那些单元格。但实际上它可能非常好,具体取决于数据集。)
澄清问题:我们有一个长度为S
的字符串n
,以及一个长度为T
的字符串m
。允许的最大间隙为k
- 此差距也应在字符串的开头和结尾处强制执行。间隙是两个匹配字符之间不匹配字符的数量 - 即如果字母相邻,则为0
,而不是1
。
想象一下包含n+1
行和m+1
列的表格。
0 1 2 3 4 ... m
--------------------
0 | ? ? ? ? ? ?
1 | ? ? ? ? ? ?
2 | ? ? ? ? ? ?
3 | ? ? ? ? ? ?
... |
n | ? ? ? ? ? ?
首先,我们可以定义行r
和列c
中的条目是一个二进制标记,告诉我们r
的第一个S
字符是否为k
是c
的第一个T
个字符的有效T
- 子序列。 (不要担心如何计算这些值,或者这些值是否有用,我们只需要定义它们第一。)
但是,这个二进制标志表并不是非常有用。作为附近细胞的函数,不可能容易地计算一个细胞。相反,我们需要每个单元格存储更多信息。除了记录相关字符串是否是有效的子序列之外,我们还需要记录c
子字符串末尾的连续不匹配字符数(带有r=2
字符的子字符串)。例如,如果S
的前"ab"
个字符为c=3
且T
的前"abb"
个字符为b
,则有两个这里可能的匹配:第一个字符显然彼此匹配,但b
可以与后者b
中的任何一个匹配。因此,我们可以选择在最后留下一个或零不匹配的S,T
。我们在表中记录了哪一个?
答案是,如果一个单元格有多个有效值,那么我们采用最小的一个。合乎逻辑的是,我们希望在匹配字符串的其余部分时尽可能简化自己的生活,因此最后的差距越小越好。警惕其他不正确的优化 - 我们不希望匹配尽可能多的字符或少数字符。这可能会适得其反。但对于给定的字符串S
,找到匹配(如果有任何有效匹配)最小化最后的差距是合乎逻辑的。
另一个观察结果是,如果字符串T
比k
短得多,那么它就无法匹配。这显然取决于S
。 rk
可以涵盖的最大长度为c
,如果小于(r,c)
,则我们可以轻松地将-1
标记为?
。
(可以做出的任何其他优化陈述?)
我们不需要计算此表中的所有值。不同可能状态的数量是k + 3。他们从一个未定义的'开始。州(-
)。如果对(子)字符串对不可能匹配,则状态为f(r,c)
。如果匹配是可能的,那么单元格中的分数将是介于0和k之间的数字,在末尾记录最小可能数量的不匹配连续字符。这给了我们总共k + 3个状态。
我们只对表格右下角的条目感兴趣。如果f(n,m)
是计算特定单元格的函数,那么我们只对r
感兴趣。可以将特定单元的值计算为附近值的函数。我们可以构建一个递归算法,将c
和f(r,c)
作为输入,并根据附近的值执行相关的计算和查找。如果此函数查找?
并找到f(r,c)
,它将继续进行计算,然后存储答案。
存储答案非常重要,因为算法可能会多次查询同一个单元格。但是,一些细胞永远不会被计算出来。我们开始尝试计算一个单元格(右下角),并根据需要进行查找和计算并存储。
这是显而易见的" O(nm)方法。这里唯一的优化是观察我们不需要计算所有单元,因此这应该使复杂度低于O(nm)。当然,对于非常讨厌的数据集,您最终可能会计算几乎所有的单元格!因此,很难对此进行官方复杂性估计。
最后,我应该说明如何计算特定的单元格r==0
:
c <= k
和f(r,c) = 0
,则k
。 空字符串可以匹配其中包含最多r==0
个字符的任何字符串。 c > k
和f(r,c) = -1
,则S[r]==T[c]
。 比赛太长了。 f(r-1,c-1) != -1
和f(r,c) = 0
,则f(r,c-1) != -1
。 这是最好的情况 - 没有尾随差距的匹配。 f(r,c) < k
和f(r,c) = f(r,c-1)+1
,那么f(r,c) = -1
。S
。这个答案的其余部分是我最初的基于Haskell的方法。它的一个优点是它能够理解&#39;它不需要计算每个单元,只需要计算单元。但它可能会导致多次计算一个细胞的效率低下。
*另请注意,Haskell方法有效地解决了镜像中的问题 - 它试图从T
和K
的最终子串构建匹配,其中最小前导一堆无与伦比的人物。我没有时间用它的镜像重写它#39;形成!
递归方法应该有效。我们想要一个带有三个参数的函数,int S
,String T
和String helloworld
l r d
。但是,我们不仅仅想要一个布尔答案来判断S是否是T的有效k子序列。
对于这种递归方法,如果S是一个有效的k子序列,我们还想通过返回从T的开头可以删除多少个字符来了解可能的最佳子序列。我们希望找到最好的&#39;子。如果S和T不可能有k子序列,那么我们返回-1,但如果可能,那么我们想要返回从T中拉出的最小数量的字符,同时保留k子序列属性。
lowo
这是一个有效的4子序列,但最大的差距(最多)有四个字符(he
)。这是最好的子序列,因为它在开始时只留下两个字符的间隙( helloworld
l r d
)。或者,这是另一个具有相同字符串的有效k子序列,但它不是很好,因为它在开始时留下了三个间隙:
best :: Int -> String -> String -> Int
-- K S T return
-- where len(S) <= len(T)
best k [] t_string -- empty S is a subsequence of anything!
| length(t_string) <= k = length(t_string)
| length(t_string) > k = -1
best k sss@(s:ss) [] = (-1) -- if T is empty, and S is non-empty, then no subsequence is possible
best k sss@(s:ss) tts@(t:ts) -- both are non-empty. Various possibilities:
| s == t && best k ss ts /= -1 = 0 -- if s==t, and if best k ss ts != -1, then we have the best outcome
| best k sss ts /= -1
&& best k sss ts < k = 1+ (best k sss ts) -- this is the only other possibility for a valid k-subsequence
| otherwise = -1 -- no more options left, return -1 for failure.
这是用Haskell编写的,但它应该很容易用其他任何语言重写。我将在下面详细介绍它。
--
逐行分析:
(Haskell中的注释以best :: Int -> String -> String -> Int
)
best k [] t -- empty S is a subsequence of anything!
| length(t) <= k = length(t)
| length(t) > k = -1
一个函数,它接受一个Int和两个字符串,并返回一个Int。如果k子序列不可能,则返回值为-1。否则它将返回0到K(包括)之间的整数,告诉我们在T开始时可能存在的最小间隙。
我们只是按顺序处理案件。
[]
上面,我们处理S为空(best k sss@(s:ss) [] = (-1)
-- if T is empty, and S is non-empty, then no subsequence is possible
)的情况。这很简单,因为空字符串始终是有效的子序列。但是要测试它是否是有效的 k-subsequence ,我们必须计算T的长度。
best k sss@(s:ss) tts@(t:ts) -- both are non-empty. Various possibilities:
| s == t && best k ss ts /= -1 = 0 -- if s==t, and if best k ss ts != -1, then we have the best outcome
| best k sss ts /= -1
&& best k sss ts < k = 1+ (best k sss ts) -- this is the only other possibility for a valid k-subsequence
| otherwise = -1 -- no more options left, return -1 for failure.
该评论解释了这一点。这让我们看到两个字符串都非空的情况:
tts@(t:ts)
tts
匹配非空字符串。字符串的名称是t
。但是在Haskell中还有一个方便的技巧,允许您为字符串(ts
)中的第一个字母和字符串的其余部分(ts
)指定名称。此处t
应该大声朗读,因为s
的复数形式 - t
后缀在这里表示“复数”。我们说有一个ts
和一些sss
,它们一起构成完整的(非空的)字符串。
最后一段代码处理两个字符串都非空的情况。这两个字符串称为tts
和head sss
。但为了省去编写tail sss
和@(s:ss)
以访问字符串的第一个字母和字符串重新存储器的麻烦,我们只需使用s
告诉编译器存储这些字符串数量为变量ss
和char s = sss[0];
。例如,如果这是C ++,那么s==t
与函数的第一行相同,效果相同。
最好的情况是第一个字符匹配best k sss ts /= -1
,其余字符串是有效的k子序列sss
。这允许我们返回0。
如果当前完整字符串(ts
)是另一个字符串({{1}})的其余部分的有效k子序列,则唯一的另一种成功可能性。我们在此添加1并返回,但如果差距过大则会例外。
非常重要的是不要改变最后五行的顺序。它们的排序依次为“好”的顺序。得分是。我们想先测试并返回最佳可能性。
答案 1 :(得分:2)
朴素的递归解决方案。 Bonus:=返回值是字符串可以匹配的方式数。
#include <stdio.h>
#include <string.h>
unsigned skipneedle(char *haystack, char *needle, unsigned skipmax)
{
unsigned found,skipped;
// fprintf(stderr, "skipneedle(%s,%s,%u)\n", haystack, needle, skipmax);
if ( !*needle) return strlen(haystack) <= skipmax ? 1 : 0 ;
found = 0;
for (skipped=0; skipped <= skipmax ; haystack++,skipped++ ) {
if ( !*haystack ) break;
if ( *haystack == *needle) {
found += skipneedle(haystack+1, needle+1, skipmax);
}
}
return found;
}
int main(void)
{
char *ab = "ab";
char *test[] = {"ab" , "accb" , "abcccb" , "abcb", NULL}
, **cpp;
for (cpp = test; *cpp; cpp++ ) {
printf( "[%s,%s,%u]=%u \n"
, *cpp, ab, 2
, skipneedle(*cpp, ab, 2) );
}
return 0;
}
答案 2 :(得分:1)
O(p * n)解,其中p = T中S的子序列数。
扫描字符串T并保留可能具有的S的可能子序列列表
1.找到的最后一个字符的索引和
2.找到要删除的字符数
继续在T的每个字符处更新此列表。
答案 3 :(得分:0)
不确定这是否是您要求的,但您可以从每个String创建一个字符列表,并在另一个列表中搜索一个列表的实例,然后if(list2.length-K&gt; list1.length) )返回假。
答案 4 :(得分:0)
以下是提出的算法: - O(| T | * k)平均情况
1&GT;扫描T并在哈希表中存储字符索引: -
例如。 S =“abc”T =“ababbc”
符号表条目: -
a = 1 3
b = 2 4 5
c = 6
2&GT;我们知道isValidSub(S,T)= isValidSub(S(0,j),T)&amp;&amp; (isValidSub(S(j + 1,N),T)|| .... isValidSub(S(j + K,T),T))
A&GT。我们将使用自下而上的方法来解决上述问题
B个。我们将维护一个有效的数组有效(len(S)),其中每个记录指向一个哈希表(解释为我们继续解决)
c取代。从S的最后一个元素开始,查找与符号表
中的字符对应存储的索引例如。在上面的例子中S [last] =“c”
符号表中的c = 6
现在我们将(5,6),(4,6),....(6-k-1,6)等记录放入有效(最后)的哈希表中
说明: - 因为s(6,len(S))是有效的子序列,因此s(0,6-i)++ s(6,len(S))(其中i在范围内(1,k +) 1))也是有效的子序列,前提是s(0,6-i)是有效的子序列。
3&GT;从最后一个元素开始填充有效数组: -
A&GT。从对应于S [j]的哈希表条目中获取指示,其中j是我们正在分析的有效数组的当前指标。
B个。检查indice是否在Valid(j + 1)中,如果少于add(indice-i,indice),其中i在范围(1,k + 1)中有效(j)哈希表
例如: -
S =“abc”T =“ababbc”
迭代1:
j = len(S)= 3
S [3] ='c'
符号表:c = 6
在(j)
中添加(5,6),(4,6),(3,6)为K = 2有效(3)= {(5,6),(4,6),(3,6)}
j = 2
迭代2:
S [j] ='b'
符号表:b = 2 4 5
在Valid(3)=&gt;中查找2找不到=&gt;跳过
在Valid(3)=&gt;中查找4 found =&gt;添加有效(2)= {(3,4),(2,4),(1,4)}
在Valid(3)=&gt;中查找5 found =&gt;添加有效(2)= {(3,4),(2,4),(1,4),(4,5)}
j = 1
迭代3:
S [j] =“a”
符号表:a = 1 3
在Valid(2)=&gt;中查找1找不到
在有效(2)=&gt;中查找3 found =&gt;停止,因为它是最后一次迭代
END
在Valid(2)中找到3,表示存在从T开始的有效子序列
开始= 3
4.&GT;重建在Valid Array中向下移动的解决方案: -
示例:
开始= 3
在有效(2)=&gt;中查找3发现(3,4)
在Valid(3)=&gt;中查找4发现(4,6)
结束
重建的解决方案(3,4,6)确实是有效的子序列
如果我们在该迭代中添加了(3,5)而不是(3,4),那么记住(3,5,6)也可以是一个解决方案时间复杂度分析&amp;空间复杂性: -
时间复杂度:
步骤1:扫描T = O(| T |)
步骤2:使用HashTable查找填充所有有效条目O(| T | * k)是aprox O(1)
步骤3:重建溶液O(| S |)
总体平均情况时间:O(| T | * k)
空间复杂性:
符号表= O(| T | + | S |)
有效表= O(| T | * k)可以通过优化来改进
总空间= O(| T | * k)
Java实施: -
public class Subsequence {
private ArrayList[] SymbolTable = null;
private HashMap[] Valid = null;
private String S;
private String T;
public ArrayList<Integer> getSubsequence(String S,String T,int K) {
this.S = S;
this.T = T;
if(S.length()>T.length())
return(null);
S = S.toLowerCase();
T = T.toLowerCase();
SymbolTable = new ArrayList[26];
for(int i=0;i<26;i++)
SymbolTable[i] = new ArrayList<Integer>();
char[] s1 = T.toCharArray();
char[] s2 = S.toCharArray();
//Calculate Symbol table
for(int i=0;i<T.length();i++) {
SymbolTable[s1[i]-'a'].add(i);
}
/* for(int j=0;j<26;j++) {
System.out.println(SymbolTable[j]);
}
*/
Valid = new HashMap[S.length()];
for(int i=0;i<S.length();i++)
Valid[i] = new HashMap<Integer,Integer >();
int Start = -1;
for(int j = S.length()-1;j>=0;j--) {
int index = s2[j] - 'a';
//System.out.println(index);
for(int m = 0;m<SymbolTable[index].size();m++) {
if(j==S.length()-1||Valid[j+1].containsKey(SymbolTable[index].get(m))) {
int value = (Integer)SymbolTable[index].get(m);
if(j==0) {
Start = value;
break;
}
for(int t=1;t<=K+1;t++) {
Valid[j].put(value-t, value);
}
}
}
}
/* for(int j=0;j<S.length();j++) {
System.out.println(Valid[j]);
}
*/
if(Start != -1) { //Solution exists
ArrayList subseq = new ArrayList<Integer>();
subseq.add(Start);
int prev = Start;
int next;
// Reconstruct solution
for(int i=1;i<S.length();i++) {
next = (Integer)Valid[i].get(prev);
subseq.add(next);
prev = next;
}
return(subseq);
}
return(null);
}
public static void main(String[] args) {
Subsequence sq = new Subsequence();
System.out.println(sq.getSubsequence("abc","ababbc", 2));
}
}
答案 5 :(得分:0)
考虑一种递归方法:让int f(int i, int j)
表示S [i ... n]匹配T [j ... m]时开头的最小可能间隙。如果此类匹配不存在,则f
会返回-1
。以下是f
:
int f(int i, int j){
if(j == m){
if(i == n)
return 0;
else
return -1;
}
if(i == n){
return m - j;
}
if(S[i] == T[j]){
int tmp = f(i + 1, j + 1);
if(tmp >= 0 && tmp <= k)
return 0;
}
return f(i, j + 1) + 1;
}
如果我们将这种递归方法转换为动态编程方法,那么我们的时间复杂度可能为O(nm)
。
答案 6 :(得分:0)
这是通常 * 在O(N)中运行并且占用O(m)空间的实现,其中m是长度(S)。
它使用了测量员链的概念:
想象一下由长度为k的链条连接的一系列极点。
Achor在弦乐开头的第一个极点。
现在,在找到一个角色匹配之前,向前推进下一个极点。
放置那根杆子。如果有松弛,继续前进到下一个角色;
否则前一个杆子被拖了,你需要回去
并将其移至下一个最接近的比赛。
重复直到你到达终点或用完为止。
typedef struct chain_t{
int slack;
int pole;
} chainlink;
int subsequence_k_impl(char* t, char* s, int k, chainlink* link, int len)
{
char* match=s;
int extra = k; //total slack in the chain
//for all chars to match, including final null
while (match<=s+len){
//advance until we find spot for this post or run out of chain
while (t[link->pole] && t[link->pole]!=*match ){
link->pole++; link->slack--;
if (--extra<0) return 0; //no more slack, can't do it.
}
//if we ran out of ground, it's no good
if (t[link->pole] != *match) return 0;
//if this link has slack, go to next pole
if (link->slack>=0) {
link++; match++;
//if next pole was already placed,
while (link[-1].pole < link->pole) {
//recalc slack and advance again
extra += link->slack = k-(link->pole-link[-1].pole-1);
link++; match++;
}
//if not done
if (match<=s+len){
//currrent pole is out of order (or unplaced), move it next to prev one
link->pole = link[-1].pole+1;
extra+= link->slack = k;
}
}
//else drag the previous pole forward to the limit of the chain.
else if (match>=s) {
int drag = (link->pole - link[-1].pole -1)- k;
link--;match--;
link->pole+=drag;
link->slack-=drag;
}
}
//all poles planted. good match
return 1;
}
int subsequence_k(char* t, char* s, int k)
{
int l = strlen(s);
if (strlen(t)>(l+1)*(k+1))
return -1; //easy exit
else {
chainlink* chain = calloc(sizeof(chainlink),l+2);
chain[0].pole=-1; //first pole is anchored before the string
chain[0].slack=0;
chain[1].pole=0; //start searching at first char
chain[1].slack=k;
l = subsequence_k_impl(t,s,k,chain+1,l);
l=l?chain[1].pole:-1; //pos of first match or -1;
free(chain);
}
return l;
}
*我不确定那个大O.我最初认为它类似O(km + N)。在测试中,对于良好匹配,平均值小于2N,对于失败的匹配,平均小于N.
......但是......有一个奇怪的退化案例。对于从大小为A
的字母表中选择的随机字符串,k = 2A+1
时速度会慢得多。即使在这种情况下,它也优于O(Nm),并且当k稍微增加或减少时,性能返回到O(N)。 Gist Here如果有人好奇的话。