Алгоритм для проверки, если строка из списка подстрок

дана строка и массив строк. Как быстро проверить, может ли эта строка быть построена путем объединения некоторых строк в массиве?

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

редактировать Читая какой-то ответ, я заметил, что это, вероятно, NP-полная проблема. Даже найдя подмножество строк, которые вместе будут иметь одинаковые длина, поскольку данная строка является классической проблемой суммы подмножеств.

поэтому я думаю, что на это нет простого ответа.

редактировать

теперь кажется, что это не NP-полная проблема в конце концов. Вот так круче: -)

редактировать

Я придумал решение, которое проходит некоторые тесты:

def can_build_from_substrings(string, substrings):
    prefixes = [True] + [False] * (len(string) - 1)
    while True:
        old = list(prefixes)
        for s in substrings:
            for index, is_set in enumerate(prefixes):
                if is_set and string[index:].startswith(s):
                    if string[index:] == s:
                        return True
                    prefixes[index + len(s)] = True
        if old == prefixes: # nothing has changed in this iteration
            return False

Я считаю, что время O(n * m^3), где n - длина substrings и m - длина string. А ты как думаешь?

10 ответов


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

это проблема динамического программирования. (И отличный вопрос!)

давайте composable(S, W) быть true, если строка S можно написать используя список подстроки W.

S composable если и только если:

  1. S начинается с подстроки w на W.
  2. остаток S после w также composable.

давайте напишем какой-нибудь псевдокод:

COMPOSABLE(S, W):
  return TRUE if S = "" # Base case
  return memo[S] if memo[S]

  memo[S] = false

  for w in W:
    length <- LENGTH(w)
    start  <- S[1..length]
    rest   <- S[length+1..-1]
    if start = w AND COMPOSABLE(rest, W) :
      memo[S] = true # Memoize

  return memo[S]

этот алгоритм имеет o(m*n) время выполнения, предполагая, что длина подстрок не является линейной w/r/t к самой строке, и в этом случае время выполнения будет O (m*n^2) (где m размер списка подстрок и n - это длина рассматриваемой строки). Он использует o (n) пространство для запоминания.

(N. B. Как написано, псевдокод использует o (n^2) пространство, но хэширование ключей мемуаризации облегчит это.)

редактировать

вот рабочая реализация Ruby:

def composable(str, words)
  composable_aux(str, words, {})
end

def composable_aux(str, words, memo)
  return true if str == ""                # The base case
  return memo[str] unless memo[str].nil?  # Return the answer if we already know it

  memo[str] = false              # Assume the answer is `false`

  words.each do |word|           # For each word in the list:
    length = word.length
    start  = str[0..length-1]
    rest   = str[length..-1]

    # If the test string starts with this word,
    # and the remaining part of the test string
    # is also composable, the answer is true.
    if start == word and composable_aux(rest, words, memo)
      memo[str] = true           # Mark the answer as true
    end
  end

  memo[str]                      # Return the answer
end

Это определенно не быстро, но вы вот идея:

  • повторите все строки, проверяя, "начинается" ли целевая строка с любой из них
  • возьмите самую длинную строку, с которой начинается целевая строка, удалите ее из списка и обрезайте из основной строки
  • смыть, повторить

Stop, когда вы остались с целевой строкой длины 0.

Как я уже говорил, это определенно не быстро, но должно дать вам baseline ("это не должно быть намного хуже, чем это").

редактировать

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

  • когда вы обнаружите, что строка является главой цели, нажмите ее в список. После построения списка вы, естественно, попробуете самую большую "голову" цели
  • когда вы обнаружите, что голова вы пробовал не подходит с тем, что осталось, попробуйте следующий лучший head

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


вот как я бы это сделал.

  1. определите длину целевой строки.
  2. определите длину каждой строки в массиве подстрок
  3. определите, какая комбинация подстрок даст строку с той же длиной, что и целевая строка (если есть, если вы не закончили)
  4. создайте все перестановки комбинаций подстрок, определенных на Шаге 3. Проверьте, совпадают ли они с целью строка.

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


Мне кажется, что проблема может быть решена простым линейным пересечением массива и сравнения. Однако может быть несколько проходов. Вы можете разработать стратегию минимизации проходов. Например, построение суб-массива всех подстрок исходной строки в первом проходе. Затем попробуйте различные варианты линейно.


вдохновленный @cnicutars ответ:

  • функции Possible(array A, string s)
    • если s пусто, возвращает true.
    • вычислить массив P всех строк A это префикс s.
    • если P пусто, возвращает false.
    • для каждой строки p на P:
      • если Possible(A with p removed, s with prefix p removed) возвращает true
    • возвращает false

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

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

основная проблема в алгоритме: что, если некоторые из подстрок на самом деле являются подстроками к другим подстрокам ... то есть, что, если у нас есть подстроки: "ab", "abc", "abcd",... , В этом случае порядок проверки подстрок изменит сложность. Для этого у нас есть LR-Парсеры. Думаю, они лучше всех решают такие задачи.

Я найду вам точное решение скоро.


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

1) грубая сила: сделайте это так, как вы бы генератор паролей, то есть word1 + word1+word1 > word1+word1+word2 > word1 + word1 + word3 и т. д. и т. д.

трюк есть длина, поэтому вы должны попробовать все комбинации из 2 или более слов, и вы не знаете, где установить предел. Очень много времени.

2) взять строку и запустить найти в нем для каждого слова у вас есть 1 в время. возможно, проверьте длину, и если ее больше 0, сделайте это снова. продолжайте делать это, пока не достигнете нуля, он не может найти больше результатов. если вы нажмете 0, это победа, если не проигрыш. Я думаю, что этот метод будет намного лучше, чем первый, но я думаю, что у кого-то будет лучшее предложение.


вот примерная идея, которая должна работать.

  1. скопировать исходную строку в новую строку
  2. В то время как строка копирования все еще имеет данные и все еще есть подстроки a. Возьмите подстроку, если копировать.содержит(подстрока) копия.удалить(подстрока)
  3. если копия теперь пуста, то да, вы можете построить строку
  4. если копия не пуста, выбросьте первый substr, который был удален из строки и повторите.
  5. Если все подстроки ушел, и копия все еще не пуста, тогда нет, вы не можете ее построить.

изменить: Способ улучшить это - сначала перебрать все подстроки и выбросить все, что не содержится в основной строке. Затем выполните описанные выше шаги.


позвольте мне предложить использовать деревья суффиксов (используя онлайн-алгоритм Укконена для его построения), который кажется подходящим с точки зрения поиска общих подстрок в двух текстах. Вы можете найти дополнительную информацию в Википедии / специальных источниках. Задача

Find all z occurrences of the patterns P1..Pn of total length m
enter code hereas substrings in O(m + z) time.

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


Если каждая подстрока должна использоваться только один раз, но не все из них должны использоваться...

для каждой перестановки размера N из подстрок, равных по размеру исходной строке, проверьте ее, если нет, сделайте перестановку из N+1 элементов, закончите так далее, пока не исчерпаете все перестановки.

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

чтобы объяснить, почему решения, в которых удаление подстрок из исходной строки никогда не будет работать:

есть строка "1234123" и массив "12","34","123". Если вы удалите " 123 " с самого начала, у вас будет ложное отрицание. Аналогичный пример, когда удаление из конца будет: "1234123" : "23,"41","123".

с обратным отслеживанием с жадностью: (длина строки M 7, N элементов num 3) - возьмите самый длинный: 123 - удалить его из первого появления O (3) - попробуйте другие два с остальными: no go + O ((n-1)*(m-3)) - отката за O(1) - снять со второго: O(m-3) - попробуйте другие два O ((n-1)*m-3) = O (30)

перестановки 1 + 2 + 3 = O (3) + O(4) + O(6) = O(13). Таким образом, для небольших поднаборов перестановки длины на самом деле быстрее, чем обратный путь. Это изменится, если вы попросите найти много подстрок (в большинстве случаев, но не все).

вы можете удалить только несуществующие подстроки из массива, чтобы уменьшить количество перестановок от n^n до n^(n-1) для каждой удаленной несуществующей подстроки.