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

(Я пишу это в контексте 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;
            }
        }
    }
}

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

изменить: Я считаю, что это форма, которую выбранный ответ будет принимать программно в 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;
    }
}

3 ответов


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

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

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

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

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


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

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

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

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


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

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

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

(edit -- I can't reply to comments yet)

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

пример:

строки: 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]) или" смешанные", а затем минимальные не разделяемые строки (Я бы назвал эти" отличительные инфиксы") могут быть вычислены за один проход.