【问题标题】:How can I detect common substrings in a list of strings如何检测字符串列表中的常见子字符串
【发布时间】:2009-09-11 13:19:35
【问题描述】:

给定一组字符串,例如:

EFgreen
EFgrey
EntireS1
EntireS2
J27RedP1
J27GreenP1
J27RedP2
J27GreenP2
JournalP1Black
JournalP1Blue
JournalP1Green
JournalP1Red
JournalP2Black
JournalP2Blue
JournalP2Green

我希望能够检测到这是三组文件:

  • 整个[1,2]
  • J27[红、绿]P[1,2]
  • JournalP[1,2][红、绿、蓝]

是否有任何已知的方法来解决这个问题 - 我可以阅读任何已发表的论文?

我正在考虑的方法是为每个字符串查看所有其他字符串并找到常见字符以及不同字符的位置,尝试找到最常见的字符串集,但我担心这不是很有效并可能给出误报。

请注意,这与'How do I detect groups of common strings in filenames' 不同,因为它假定字符串后面总是有一系列数字。

【问题讨论】:

  • 判断[J27Red,Journal]P27[Red,Green]不是集合的规则是什么?您是否优先考虑字符串中较早开始的匹配项?
  • 请更具体地说明您希望如何定义您的集合。例如,除了先前的评论之外,是什么决定了 "J27[Red,Green]P[1,2]" 是一个集合并且 [A-Z][A-Z][A-Z][A-Z][A-Z][A-Z][0- 9] 或类似的不是。
  • 除了定义模式匹配标准的规则缺乏特异性之外,这是一个很好的问题。这是一个孩子可以毫不费力地解决问题的例子,但它可能需要 SO 的集体大脑来勾勒出一个可行的算法 [具有实际的大 O 复杂度]
  • @djna @DVK 使用 J27[Red,Green]P[1,2] 和 JournalP[1,2][Red,Green,Blue] 代替提供更多的常用字符。可能存在寻找最常见字符会产生错误结果的理论示例,但我认为我的数据集不会发生这种情况。

标签: algorithm pattern-matching


【解决方案1】:

我会从这里开始:http://en.wikipedia.org/wiki/Longest_common_substring_problem

外部链接中有补充信息的链接,包括文章中解释的两种算法的 Perl 实现。

编辑添加:

根据讨论,我仍然认为最长公共子串可能是这个问题的核心。即使在您在评论中引用的 Journal 示例中,该集合的定义特征也是子字符串“Journal”。

我会首先考虑将集合定义为与其他集合分开的因素。这为您提供了划分数据的分区,然后问题在于衡量一个集合中存在多少共性。如果定义特征是一个公共子串,那么最长公共子串将是一个逻辑起点。

为了使集合检测过程自动化,一般来说,您需要一个成对的共性度量,您可以使用它来衡量所有可能对之间的“差异”。然后,您需要一种算法来计算导致总体差异最小的分区。如果差异度量不是最长公共子串,那很好,但是您需要确定它将是什么。显然,它需要是可以衡量的具体内容。

还要记住,差异测量的属性将影响可用于进行分区的算法。例如,假设 diff(X,Y) 给出 X 和 Y 之间差异的度量。那么,如果您的距离度量是 diff(A,C)

在考虑这个问题时,我也开始怀疑我们是否可以将“差异”设想为任意两个字符串之间的距离,并且通过对距离的严格定义,我们是否可以尝试某种cluster analysis在输入字符串上。只是一个想法。

【讨论】:

  • 这很有帮助,但我认为我更接近最长常见子序列问题,因为我可能有多个子字符串(例如 Journal 示例)
  • 是的,可能。当我第一次阅读您的问题时,我认为“集合”将被限制为具有各种附属物的单个子字符串根。但我现在看到您正在寻找更多相关的东西。
  • 我将尝试看看 LCS 如何处理多个常见的子字符串。您关于距离和聚类的新观点看起来也是一个很好的尝试方法 - 谢谢。
【解决方案2】:

好问题!解决方案的步骤是:

  1. tokenizing输入
  2. 使用令牌构建适当的data structureDAWG 是理想的,但 Trie 更简单,也是一个不错的起点。
  3. 可选的数据结构后处理,以简化或将子树聚类成单独的输出
  4. serialization 的数据结构为 regular expression 或类似语法

我在regroup.py 中实现了这种方法。这是一个例子:

$ cat | ./regroup.py --cluster-prefix-len=2
EFgreen
EFgrey
EntireS1
EntireS2
J27RedP1
J27GreenP1
J27RedP2
J27GreenP2
JournalP1Black
JournalP1Blue
JournalP1Green
JournalP1Red
JournalP2Black
JournalP2Blue
JournalP2Green
^D
EFgre(en|y)
EntireS[12]
J27(Green|Red)P[12]
JournalP[12](Bl(ack|ue)|(Green|Red))

【讨论】:

    【解决方案3】:

    这样的事情可能会奏效。

    1. 构建一个代表所有字符串的 trie。

    在您给出的示例中,从根开始会有两条边:“E”和“J”。然后“J”分支将拆分为“Jo”和“J2”。

    1. 分叉的单股,例如E-n-t-i-r-e-S-(forks to 1, 2) 表示选择,所以这将是 EntireS[1,2]

    2. 如果链相对于前叉“太短”,例如B-A-(forks to N-A-N-A and H-A-M-A-S),我们列出两个词(“banana, bahamas”)而不是一个选择(“ba[nana,hamas]”)。 “太短”可能就像“如果分叉之后的部分比之前的部分长”一样简单,或者可能由具有给定前缀的单词数加权。

    3. 如果两个子树“足够相似”,则可以合并它们,这样您就可以得到一个通用图,而不是树。例如,如果您有 ABRed,ABBlue,ABGreen,CDRed,CDBlue,CDGreen,您可能会发现以“AB”为根的子树与以“CD”为根的子树相同,因此您将它们合并。在您的输出中,这将如下所示:[左分支,右分支][子树],因此:[AB,CD][Red,Blue,Green]。如何处理接近但不完全相同的子树?可能没有绝对的答案,但这里有人可能有一个好主意。

    我正在标记这个答案社区维基。请随意扩展它,以便我们一起对这个问题有一个合理的答案。

    【讨论】:

    • 我喜欢这种方法,但我认为它会与例如J-27-(RedP,GreenP)-(1,2)。在这种情况下,我希望能够识别红色和绿色,因此似乎需要更多处理才能找到公共子字符串。
    • 我不知道你为什么认为它会挣扎:在这种情况下,J27RedP 和 J27GreenP 下的子树是相同的:(1,2)。所以第 3 步将合并它们。显示这个合并的树将直接返回 J27-(RedP,GreenP)-(1,2) 因为 -(a,b) 是你写的方式(fork to a and b)。
    【解决方案4】:

    试试 "frak" 。它从一组字符串创建正则表达式。也许对其进行一些修改会对您有所帮助。 https://github.com/noprompt/frak

    希望对你有帮助。

    【讨论】:

      【解决方案5】:

      字符串相似性有很多方法。我建议看看这个开源库,它实现了许多指标,比如 Levenshtein 距离。

      http://sourceforge.net/projects/simmetrics/

      【讨论】:

      • 这很有帮助 - 看起来像 Levenshtein 距离、Needleman-Wunch 距离或类似的距离函数可能在 jbourque 建议的技术中用作距离函数。
      • SimMetrics 链接已失效,但包在 SourceForge 上:sourceforge.net/projects/simmetrics
      【解决方案6】:

      您应该能够使用通用后缀树来实现这一点:在后缀树中查找来自多个源字符串的长路径。

      【讨论】:

        【解决方案7】:

        提出了许多解决方案来解决寻找公共子字符串的一般情况。但是,这里的问题更专业。您正在寻找常见的前缀,而不仅仅是子字符串。这使它更简单一些。 找到最长公共前缀的一个很好的解释可以在 http://www.geeksforgeeks.org/longest-common-prefix-set-1-word-by-word-matching/

        所以我提出的“pythonese”伪代码是这样的(请参阅链接以获取find_lcp的实现:

        def count_groups(items):
          sorted_list = sorted(items)
        
          prefix = sorted_list[0]
          ctr = 0
          groups = {}
          saved_common_prefix = ""
          for i in range(1, sorted_list):
            common_prefix = find_lcp(prefix, sorted_list[i])
            if len(common_prefix) > 0: #we are still in the same group of items
              ctr += 1
              saved_common_prefix = common_prefix
              prefix = common_prefix
            else: # we must have switched to a new group
              groups[saved_common_prefix] = ctr
              ctr = 0
              saved_common_prefix = ""
              prefix = sorted_list[i]
          return groups
        

        【讨论】:

          【解决方案8】:

          对于这个特殊的字符串示例,为了使其极其简单,请考虑使用简单的单词/数字分隔符。

          一个非数字序列显然可以以大写字母 (Entire) 开头。将所有字符串分解为序列组后,类似于

          [Entire][S][1]
          [Entire][S][2]
          [J][27][Red][P][1]
          [J][27][Green][P][1]
          [J][27][Red][P][2]
          ....
          [Journal][P][1][Blue]
          [Journal][P][1][Green]
          

          然后开始按组分组,您很快就会看到前缀“Entire”对于某些组来说是常见的,并且所有子组都以 S 作为头组,因此这些组的唯一变量是 1,2。 对于 J27 案例,您可以看到 J27 只是叶子,但它随后在红色和绿色处分支。

          所以某种 List> -结构(如果我没记错的话是复合模式)。

          【讨论】:

          • 好主意,但实际的字符串不会有这样的简单规则 - 我只是在示例中使用了大写字母以使其更易于阅读。
          【解决方案9】:
          import java.util.*;
          class StringProblem
          {
              public List<String> subString(String name)
              {
                  List<String> list=new ArrayList<String>(); 
                  for(int i=0;i<=name.length();i++)
                  {
                     for(int j=i+1;j<=name.length();j++)
                     {
                         String s=name.substring(i,j);
                         list.add(s);
                     }
                  }
                  return list;
              }
              public String commonString(List<String> list1,List<String> list2,List<String> list3)
              {
                  list2.retainAll(list1);
                  list3.retainAll(list2);
          
                  Iterator it=list3.iterator();
                  String s="";
                  int length=0;
                  System.out.println(list3);   // 1 1 2 3 1 2 1
                  while(it.hasNext())    
                  {
                     if((s=it.next().toString()).length()>length)
                     {
                        length=s.length();
                     }
                  }
                  return s;
              }
              public static void main(String args[])
              {
                  Scanner sc=new Scanner(System.in);
                  System.out.println("Enter the String1:");
                  String name1=sc.nextLine();
                  System.out.println("Enter the String2:");
                  String name2=sc.nextLine();
                  System.out.println("Enter the String3:");
                  String name3=sc.nextLine();
                //  String name1="salman";
                //  String name2="manmohan";
                //  String name3="rahman";
          
                  StringProblem  sp=new StringProblem();
          
                  List<String> list1=new ArrayList<String>();
                  list1=sp.subString(name1);
          
                  List<String> list2=new ArrayList<String>();
                  list2=sp.subString(name2);
          
          
                  List<String> list3=new ArrayList<String>();
                  list3=sp.subString(name3);
          
                  sp.commonString(list1,list2,list3);
                  System.out.println(" "+sp.commonString(list1,list2,list3));
              }
          }
          

          【讨论】:

            猜你喜欢
            • 2019-09-09
            • 2020-02-23
            • 1970-01-01
            • 2022-01-25
            • 1970-01-01
            • 2023-03-28
            • 2011-06-18
            • 2023-04-04
            相关资源
            最近更新 更多