如何确定字符串S可以通过删除一些字符来从字符串T中进行,但最多可以是K个连续字符

时间:2013-11-07 00:30:08

标签: string algorithm

很抱歉长标题:)

在此问题中,我们的长度为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"是一个反例。

有没有解决这个问题的快速解决方案?

7 个答案:

答案 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字符是否为kc的第一个T个字符的有效T - 子序列。 (不要担心如何计算这些值,或者这些值是否有用,我们只需要定义它们第一。)

但是,这个二进制标志表并不是非常有用。作为附近细胞的函数,不可能容易地计算一个细胞。相反,我们需要每个单元格存储更多信息。除了记录相关字符串是否是有效的子序列之外,我们还需要记录c子字符串末尾的连续不匹配字符数(带有r=2字符的子字符串)。例如,如果S的前"ab"个字符为c=3T的前"abb"个字符为b,则有两个这里可能的匹配:第一个字符显然彼此匹配,但b可以与后者b中的任何一个匹配。因此,我们可以选择在最后留下一个不匹配的S,T。我们在表中记录了哪一个?

答案是,如果一个单元格有多个有效值,那么我们采用最小的一个。合乎逻辑的是,我们希望在匹配字符串的其余部分时尽可能简化自己的生活,因此最后的差距越小越好。警惕其他不正确的优化 - 我们不希望匹配尽可能多的字符或少数字符。这可能会适得其反。但对于给定的字符串S,找到匹配(如果有任何有效匹配)最小化最后的差距是合乎逻辑的。

另一个观察结果是,如果字符串Tk短得多,那么它就无法匹配。这显然取决于Srk可以涵盖的最大长度为c,如果小于(r,c),则我们可以轻松地将-1标记为?

(可以做出的任何其他优化陈述?)

我们不需要计算此表中的所有值。不同可能状态的数量是k + 3。他们从一个未定义的'开始。州(-)。如果对(子)字符串对不可能匹配,则状态为f(r,c)。如果匹配是可能的,那么单元格中的分数将是介于0和k之间的数字,在末尾记录最小可能数量的不匹配连续字符。这给了我们总共k + 3个状态。

我们只对表格右下角的条目感兴趣。如果f(n,m)是计算特定单元格的函数,那么我们只对r感兴趣。可以将特定单元的值计算为附近值的函数。我们可以构建一个递归算法,将cf(r,c)作为输入,并根据附近的值执行相关的计算和查找。如果此函数查找?并找到f(r,c),它将继续进行计算,然后存储答案。

存储答案非常重要,因为算法可能会多次查询同一个单元格。但是,一些细胞永远不会被计算出来。我们开始尝试计算一个单元格(右下角),并根据需要进行查找和计算并存储。

这是显而易见的" O(nm)方法。这里唯一的优化是观察我们不需要计算所有单元,因此这应该使复杂度低于O(nm)。当然,对于非常讨厌的数据集,您最终可能会计算几乎所有的单元格!因此,很难对此进行官方复杂性估计。

最后,我应该说明如何计算特定的单元格r==0

  1. 如果c <= kf(r,c) = 0,则k空字符串可以匹配其中包含最多r==0个字符的任何字符串。
  2. 如果c > kf(r,c) = -1,则S[r]==T[c]比赛太长了。
  3. 细胞只有两种其他方式可以获得成功的状态。我们先试试:
    • 如果f(r-1,c-1) != -1f(r,c) = 0,则f(r,c-1) != -1这是最好的情况 - 没有尾随差距的匹配。
    • 如果那不起作用,我们会尝试下一个最好的事情。如果是f(r,c) < kf(r,c) = f(r,c-1)+1,那么f(r,c) = -1
    • 如果这些都不起作用,那么S

  4. 这个答案的其余部分是我最初的基于Haskell的方法。它的一个优点是它能够理解&#39;它不需要计算每个单元,只需要计算单元。但它可能会导致多次计算一个细胞的效率低下。

    *另请注意,Haskell方法有效地解决了镜像中的问题 - 它试图从TK的最终子串构建匹配,其中最小前导一堆无与伦比的人物。我没有时间用它的镜像重写它#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
    

    一个函数,它接受一个I​​nt和两个字符串,并返回一个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,它们一起构成完整的(非空的)字符串。

    最后一段代码处理两个字符串都非空的情况。这两个字符串称为ttshead sss。但为了省去编写tail sss@(s:ss)以访问字符串的第一个字母和字符串重新存储器的麻烦,我们只需使用s告诉编译器存储这些字符串数量为变量sschar 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如果有人好奇的话。