Найти наименьшую уникальную подстроку для каждой строки в массиве

(Я пишу это в контексте JavaScript, но соглашусь с алгоритмически правильным ответом на любом языке)

Как вы найдете кратчайшую подстроку каждого элемента в массиве строк, где подстрока НЕ ​​содержится внутри любого из других элементов, игнорируя случай?

Предположим, что у меня есть входной массив, например:

var names = ["Anne", "Anthony", "LouAnn", "Kant", "Louise", "ark"];

Результат должен выглядеть примерно так:

var uniqueNames = ["ne", "h", "ua", "ka", "i", "r"];

В моих целях вы можете с уверенностью предположить, что ни один элемент не будет полностью содержаться внутри другого элемента.

Мои мысли:
Кажется, что, возможно, это было бы грубой силой, по строкам:

var names = ["Anne", "Anthony", "LouAnn", "Kant", "Louise", "ark"];
var uniqueNames = [], nameInd, windowSize, substrInd, substr, otherNameInd, foundMatch;
// For each name
for (nameInd = 0; nameInd < names.length; nameInd++)
{
    var name = names[nameInd];
    // For each possible substring length
    windowLoop:
    for (windowSize = 1; windowSize <= name.length; windowSize++)
    {
        // For each starting index of a substring
        for (substrInd = 0; substrInd <= name.length-windowSize; substrInd++)
        {
            substr = name.substring(substrInd,substrInd+windowSize).toLowerCase();
            foundMatch = false;
            // For each other name
            for (otherNameInd = 0; otherNameInd < names.length; otherNameInd++)
            {
                if (nameInd != otherNameInd && names[otherNameInd].toLowerCase().indexOf(substr) > -1)
                {
                    foundMatch = true;
                    break;
                }
            }

            if (!foundMatch)
            {
                // This substr works!
                uniqueNames[nameInd] = substr;
                break windowLoop;
            }
        }
    }
}

Но я должен представить себе более элегантное решение, использующее файлы try/prefix, массивы суффиксов или что-то интересное.

Edit: Я считаю, что это форма, которую выбранный ответ будет программно использоваться в JavaScript:

var names = ["Anne", "Anthony", "LouAnn", "Kant", "Louise", "ark"];
var uniqueNames = [], permutations = {}, permutation, nameInd, windowSize, substrInd, substr;

// For each name
for (nameInd = 0; nameInd < names.length; nameInd++)
{
    var name = names[nameInd];
    // For each possible substring length
    windowLoop:
    for (windowSize = 1; windowSize <= name.length; windowSize++)
    {
        // For each starting index of a substring
        for (substrInd = 0; substrInd <= name.length-windowSize; substrInd++)
        {
            substr = name.substring(substrInd,substrInd+windowSize).toLowerCase();
            permutations[substr] = (typeof permutations[substr] === "undefined")?nameInd:-1;
        }
    }
}

for (substr in permutations)
{
    permutation = permutations[substr];
    if (permutation !== -1 && ((typeof uniqueNames[permutation] === "string" && substr.length < uniqueNames[permutation].length) || typeof uniqueNames[permutation] === "undefined"))
    {
        uniqueNames[permutation] = substr;
    }
}

Ответ 1

Скажите N - количество строк, а L - максимальная длина строки. Вы выполняете итерации N*L*L*N.

Я могу только немного улучшить его, торгуя одной итерацией для дополнительной памяти. Для каждой возможной длины подстроки (L итераций),

  • перечислить все подстроки этой длины в каждом имени (N*L) и сохранить их среди индекса имени в хэш-таблицу (1). Если для этой подстроки уже есть индекс, вы знаете, что это не сработает, тогда вы замените индекс специальным значением, например -1.

  • Пройдите хэш-таблицу, подбирая подстроки, для которых индекс не является -1 - это ответы для соответствующих индексов, но используйте их только в том случае, если эти имена еще не имеют более короткого ответа от предыдущей итерации

Использование памяти может быть значительно уменьшено путем хранения ссылки обратно в существующую строку вместо копирования подстрок.

Ответ 2

Эта проблема может быть решена в сложности O (N * L * L * L). В этом подходе будут использоваться попытки суффикса. Каждый node of the trie также будет хранить счетчик префикса, который будет ссылаться на количество раз, когда сформированная подстрока при перемещении к этому node из корня появилась во всех суффиксах, вставленных до сих пор.

Мы будем создавать попытки N + 1. Первое три будет глобальным, и мы будем вставлять в него все суффиксы всей строки N. Следующие N попытки будут локальными для каждой строки N, содержащей соответствующие суффиксы.

Этот шаг предварительной обработки построения попыток будет выполнен в O (N * L * L).

Теперь, как только попытки были построены, для каждой строки мы можем начать запрашивать количество раз, когда подстрока (начиная с минимальной длины) имела место в глобальном trie и trie, соответствующем этой строке. Если это то же самое в обоих, то это означает, что он не включен ни в какие другие строки, кроме самого себя. Этого можно достичь в O (N * L * L * L). Сложность может быть объяснена как N для каждой строки L * L для рассмотрения каждой подстроки и L для выполнения запроса в trie.

Ответ 3

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

Для каждой строки S найдите самую мелкую (по глубине родительской метки) node N, которая содержит только листья из S, а метка кромки - по крайней мере один символ. Метка пути от корня до родителя N плюс один символ из метки кромки, ведущей к N, является кратчайшим инфишированием S, не найденным в других строках.

Я считаю, что маркировка узлов, содержащих только листья из одной строки, может быть выполнена во время построения или путем O (N) сканирования GST; то просто проверить сканирование последнего дерева и сохранить минимальный уровень для каждой строки. Так что все O (N).

(изменить - я еще не могу ответить на комментарии)

Чтобы уточнить, каждый суффикс в дереве суффиксов имеет node, где он отделяется от других суффиксов; целью здесь является найти суффикс /a для каждой строки, которая отходит от суффиксов всех остальных строк на минимальной глубине, как измеряется меткой пути к этому значению node. Все, что нам нужно, это один дополнительный символ после этой точки, чтобы иметь подстроку, которая не отображается ни в какой другой строке.

Пример:

Строки: abbc, abc

Используя алгоритм Уконена, после первой строки мы имеем дерево суффикса только суффиксов из этой строки; Я назову их [1] здесь:

abbc[1]
b
 bc[1]
 c[1]
c[1]

Затем мы вставляем суффиксы строки 2:

ab
  bc[1]
  c[2]
b
 bc[1]
 c
  [1]
  [2]
c
 [1]
 [2]

Теперь мы хотим найти кратчайшую строку, которая ведет к ветке с только [1] под ней; мы можем сделать это, просмотрев все [1] и посмотрев на их непосредственных родителей, которые я перечислю здесь по метке пути, плюс один символ (который я буду использовать ниже):

abbc:  abb
bbc: bb
bc: bc[1]
c: c[1]

Обратите внимание, что я включил [1], поскольку это метасимвол, который отличает идентичные суффиксы [1] и [2]. Это удобно при определении подстрок, которые повторяются в нескольких строках, но это не полезно для нашей проблемы, так как если мы удалим [1], мы получим строку, которая также встречается в [2], т.е. Это не кандидат.

Теперь ни одна из ярлыков справа не встречается в любой другой строке, поэтому мы выбираем самую короткую, не включая метасимвол, который является bb.

Аналогично, вторая строка имеет следующие кандидаты:

abc: abc
bc: bc[2]
c: c[2]

Только один не имеет метасимвола в конце, поэтому нам нужно перейти с помощью abc.

Моим последним моментом является то, что этот минимальный поиск по строке не должен происходить один раз в раз; GST можно отсканировать один раз, чтобы обозначить узлы как содержащие листы из одной строки ([1], [2],.. [n]) или "смешанные", а затем минимальные неразрешенные строки в строке (я бы называть эти "отличительные инфикс" ) также можно рассчитать за один проход.