Застрял в поиске самого глубокого пути в общем обходе дерева, пытаясь найти самую большую общую подстроку

Я пытаюсь решить проблему наибольшей общей подстроки между двумя строками. Я сведу свою проблему к следующему: я создал общее дерево суффиксов и, насколько я понимаю, самая большая общая подстрока — это самый глубокий путь, состоящий из узлов, принадлежащих обеим цепочкам.

Мой тестовый ввод:

String1 = xabc
String2 = abc

введите здесь описание изображения

Кажется, что дерево, которое я строю, правильное, но моя проблема заключается в следующем методе (сначала я передаю корень дерева):

private void getCommonSubstring(SuffixNode node) {  
   if(node == null)  
    return;  
   if(node.from == ComesFrom.Both){  
    current.add(node);          
   }  
   else{  
    if(max == null || current.size() > max.size()){  
        max = current;              
    }  
    current = new ArrayList<SuffixNode>();   
   }  
   for(SuffixNode n:node.children){  
    getCommonSubstring(n);  
   }  
}       

Что я стремился сделать, так это найти самый глубокий путь с узлами, принадлежащими обеим строкам, я должен пройти по дереву (предварительный порядок) и добавить узлы, принадлежащие обеим строкам, в список (current). Как только я нахожусь в узле, который не является частью обоих, я обновляю список max, если current больше.

Но код ошибочный. И я не понимаю, как это реализовать, так как я давно не писал код для общих (небинарных) деревьев.

Не могли бы вы помочь мне разобраться в этом?

Обновление:
изменено в соответствии с @templatetypedef. Не удалось сделать эту работу либо.

private void getCommonSubstring(SuffixNode node, List<SuffixNode> nodes) {  
   if(node == null)  
    return;  
   if(node.from == ComesFrom.Both){  
    nodes.add(node);              
   }  
   else{  
       if(max == null || current.size() > max.size()){  
       max = nodes;               
    }  
    nodes = new ArrayList<SuffixNode>();  
   }  
   for(SuffixNode n:node.children){  
    List<SuffixNode> tmp = new ArrayList<SuffixNode>(nodes);  
    getCommonSubstring(n, tmp);  
   }  
}  


public class SuffixNode {
    Character character;  
    Collection<SuffixNode> children;  
    ComesFrom from;  
    Character endMarker;  
}  

person Cratylus    schedule 03.01.2013    source источник


Ответы (3)


Одна проблема, которую я здесь вижу, заключается в том, что глубина узла в дереве суффиксов не совпадает с длиной строки на этом пути. Каждое ребро в суффиксном дереве аннотируется диапазоном символов, поэтому строка, закодированная серией узлов глубины пять, может иметь меньшую длину, чем строка, закодированная глубиной два. Вам, вероятно, потребуется настроить свой алгоритм, чтобы справиться с этим, отслеживая эффективную длину строки, которую вы создали до сих пор, а не количество узлов в пути, который вы проследили до этого момента.

Вторая проблема, которую я только что заметил, заключается в том, что у вас, похоже, есть только одна переменная current, которая используется во всех разных ветвях рекурсии. Вероятно, это портит ваше состояние при рекурсивных вызовах. Например, предположим, что вы находитесь в узле и имеете путь длины три, и что есть два потомка, первый из которых заканчивается только суффиксом первой строки, а второй заканчивается суффиксом обе струны. В этом случае, если вы сделаете рекурсивный вызов первой строки, вы в конечном итоге выполните строку

current = new ArrayList<SuffixNode>();  

в рекурсивном вызове. Это затем очистит всю вашу историю, поэтому, когда вы вернетесь из этого вызова обратно к исходному узлу и спуститесь ко второму дочернему узлу, вы будете действовать так, как если бы до сих пор не было созданного списка узлов, вместо того, чтобы продолжать с три узла, которые вы нашли до сих пор.

Чтобы исправить это, я предлагаю сделать current параметром функции, а затем при необходимости создать новый ArrayList, а не стирать существующий ArrayList. Некоторая другая логика, возможно, также должна измениться, чтобы учесть это.

Учитывая это, я бы предложил написать функцию в псевдокоде следующим образом (поскольку я не знаю подробностей реализации вашего дерева суффиксов):

  • Если текущий узел нулевой, вернуть 0.
  • Если текущий узел не получен из обеих строк, вернуть 0.
  • Otherwise:
    • Set maxLen = 0.
    • For each child node:
      • Compute the length of the longest common substring rooted at that node.
      • Добавьте к этой длине количество символов вдоль края до этого дочернего элемента.
      • Обновите maxLen, если оно превышает старое значение.
    • Вернуть макслен.

Надеюсь это поможет!

person templatetypedef    schedule 03.01.2013
comment
Я обновил OP с помощью дерева суффиксов для примера. Результат должен быть {a1,2},{b1,2},{c1,2}. Верно? Поэтому я немного запутался в вашем различии между глубиной узла и длиной строки. - person Cratylus; 04.01.2013
comment
То, что вы разместили выше, не является деревом суффиксов - это суффикс trie (обратите внимание, что в строках нет маркеров конца и что есть узлы только с одним дочерним элементом). В этом случае нет разницы между глубиной узла и длиной строки. Это, вероятно, означает, что проблема может заключаться в другой ошибке, о которой я упоминал. Вы исследовали это? - person templatetypedef; 04.01.2013
comment
Но разве дерево суффиксов не просто дерево со всеми суффиксами? Оно должно быть сжато? Я проверю вашу ошибку с параметром и дам вам знать как можно скорее - person Cratylus; 04.01.2013
comment
@Cratylus- О, я только что понял, что есть еще одна ошибка - вы должны обновлять самое длинное совпадение, которое вы нашли до сих пор, на каждой итерации, а не только тогда, когда вы находите узел, который не принадлежит обеим строкам. Посмотрите на приведенный выше пример - в вашем случае самая длинная работающая подстрока заканчивается конечным узлом. В этом случае ваша рекурсия не будет пытаться обновить максимальную подстроку. Попробуйте переместить код обновления в начало рекурсии, чуть ниже нулевой проверки. - person templatetypedef; 04.01.2013
comment
@Cratylus- Дерево суффиксов - это сжатое дерево суффиксов, в котором любой узел только с одним дочерним элементом объединяется с этим дочерним элементом. Также к строке добавляются специальные конечные маркеры, чтобы указать, где заканчивается строка. Они намного более эффективны с точки зрения пространства, чем тройник, но их значительно сложнее построить. - person templatetypedef; 04.01.2013
comment
Я обновил OP модифицированным методом из вашего ответа. Но я все испортил. - person Cratylus; 04.01.2013
comment
В вашем псевдоалго, почему вас интересует maxLen? Мне не нужна длина. Я хочу найти фактическую строку - person Cratylus; 04.01.2013
comment
давайте продолжим это обсуждение в чате - person templatetypedef; 04.01.2013

Хотя это и не ответ, я бы решил это, используя стандартные коллекции с поиском O (n log n).

static String findLongestCommonSubstring(String s1, String s2) {
    if (s1.length() > s2.length()) return findLongestCommonSubstring(s2, s1);

    NavigableSet<String> substrings = new TreeSet<>();
    for (int i = 0; i < s1.length(); i++)
        substrings.add(s1.substring(i));
    String longest = "";
    for (int i = 0; i < s2.length(); i++) {
        String sub2 = s2.substring(i);
        String floor = match(substrings.floor(sub2), sub2);
        String ceiling = match(substrings.ceiling(sub2), sub2);
        if (floor.length() > longest.length())
            longest = floor;
        if (ceiling.length() > longest.length())
            longest = ceiling;
    }
    return longest;
}

private static String match(String s1, String s2) {
    if (s1 == null || s2 == null) return "";
    for (int i = 0; i < s1.length() && i < s2.length(); i++)
        if (s1.charAt(i) != s2.charAt(i))
            return s1.substring(0, i);
    return s1.substring(0, Math.min(s1.length(), s2.length()));
}

public static void main(String... args) {
    System.out.println(findLongestCommonSubstring("sdlkjfsdkljfkljsdlfkjaeakjf", "kjashdkasjdlkjasdlfkjaesdlk"));
}

отпечатки

sdlfkjae
person Peter Lawrey    schedule 03.01.2013

Вы ДОЛЖНЫ идти по пути суффиксного дерева? Если нет, то почему вы не могли:

public String findCommonSubString(string str1, string str2) {
   string mainStr;
   string otherStr;
   string commonStr = "";
   string foundCommonStr = "";
   boolean strGrowing = false;

   If (str1.length() > str2.length()) {
      mainStr = str1;
      otherStr = str2;
   } else {
      mainStr = str2;
      otherStr = str1;
   }

   int strCount = 0;

   for(int x = 0; x < mainStr.length();x++) {
      strCount = 1;
      strGrowing = true;

      while(strGrowing) {
         if (otherStr.IndexOf(mainStr.substring(x, strCount) >= 0) {
            //Found a match now add a character to it.
            strCount++;

            foundCommonStr = mainStr.substring(x, strCount);

            if (foundCommonStr.length() > commonStr.length()) {
               commonStr = foundCommonStr;
            }
         } else {
            strGrowing = false;
         }
      }

   }

return commonStr;

}

Я не запускал это ... но я буду. По сути, это начнется с наименьшей из двух строк и попытается найти общую строку между двумя строками, используя IndexOf и подстроку. затем, если это произойдет, он снова проверит, но на этот раз проверит, добавив еще один символ из меньшей строки к проверке. Он сохранит строку в переменной commonStr только в том случае, если найденная строка (foundCommonStr) больше, чем commonStr. Если он не находит совпадения, то он уже сохранил наибольшую общую строку для возврата.

Я считаю, что идея верна, но я не запускал ее в компиляторе.

person user1901482    schedule 03.01.2013
comment
Суффиксное дерево позволяет найти самую длинную повторяющуюся подстроку за время O(n + m), где n и m — длины двух строк. Ваш подход выполняется за время O(mn), что очень медленно для достаточно длинных строк. - person templatetypedef; 04.01.2013
comment
Понял. Имеет смысл. К тому же то, что у меня там было, в любом случае не работало бы. Я только что попытался запустить его. :-) - person user1901482; 04.01.2013