Алгоритм проверки того, была ли построена строка из списка подстрок
Вам предоставляется строка и массив строк. Как быстро проверить, может ли эта строка быть построена путем конкатенации некоторых строк в массиве?
Это теоретический вопрос, мне он не нужен по практическим соображениям. Но я хотел бы знать, если для этого есть хороший алгоритм.
ИЗМЕНИТЬ
Читая какой-то ответ, я заметил, что это, вероятно, проблема NP-Complete. Даже найти подмножество строк, которые будут иметь одинаковую длину, так как заданная строка является классической проблемой суммы подмножества.
Итак, я думаю, что на этот вопрос нет простого ответа.
ИЗМЕНИТЬ
Теперь кажется, что это не проблема NP-Complete. Таким образом, кулер: -)
ИЗМЕНИТЬ
Я придумал решение, которое проходит некоторые тесты:
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
. Как вы думаете?
Ответы
Ответ 1
Примечание. Я предполагаю, что вы можете использовать каждую подстроку более одного раза. Вы можете обобщить решение, чтобы включить это ограничение, изменив, как мы определяем подзадачи. Это будет иметь негативное влияние на пространство, а также на ожидаемое время выполнения, но проблема остается полиномиальной.
Это проблема динамического программирования. (И большой вопрос!)
Пусть define composable(S, W)
будет true, если строка S
может быть записана с использованием списка подстрок W
.
S
является композиционным тогда и только тогда, когда:
-
S
начинается с подстроки W
в W
.
- Остальная часть
S
после W
также является составной.
Пусть напишите некоторый псевдокод:
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) пространство для memoization.
(N.B., как записано, псевдокод использует O (n ^ 2) пространство, но хэширование ключей memoization облегчит это.)
ИЗМЕНИТЬ
Вот работающая реализация 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
Ответ 2
Это определенно не быстро, но у вас есть идея:
- Итерации по всем строкам, проверка того, что целевая строка "начинается" с любым из них
- Возьмите самую длинную строку, с которой начинается целевая строка, удалите ее из списка и обрезайте ее из основной строки
- Промыть, повторить
Остановитесь, когда вы остаетесь с целевой длиной 0 длины.
Как я уже говорил, это определенно не быстро, но должно дать вам базовый уровень ( "он не должен сильно ухудшаться" ).
ИЗМЕНИТЬ
Как указано в комментариях, это не сработает. Вам придется хранить частичные совпадения и отступать от них, когда вы обнаружите, что пути дальше нет.
- Когда вы обнаружите, что строка является головкой цели, вставьте ее в список. После создания списка вы, естественно, попробуете самую большую "головку" цели.
- Когда вы обнаружите, что голова, которую вы пробовали, не соответствует тому, что осталось, попробуйте следующую лучшую голову.
Таким образом, в конечном итоге вы изучите все пространство решений. Для каждого кандидата в голову вы будете стараться каждый возможный хвост.
Ответ 3
Вот как я это сделаю.
- Определите длину целевой строки.
- Определить длину каждой строки в массиве подстроки
- Определите, какая комбинация подстрок даст строку с той же длиной, что и целевая строка (если есть, если не все)
- Сгенерировать все перестановки комбинаций подстроки, определенные на шаге 3. Проверьте, соответствует ли какая-либо из них целевая строка.
Генерация всех перестановок является тяжелой задачей процессора, поэтому, если вы можете сократить свой "n" (размер ввода), вы получите некоторую значительную эффективность.
Ответ 4
Мне кажется, проблема может быть решена простым линейным перемещением массива и сравнением. Однако может быть многократный проход. Вы можете разработать стратегию для минимизации проходов. Например, построим вспомогательный массив всех подстрок исходной строки в первый проход. Затем попробуйте различные варианты линейно.
Ответ 5
Вдохновленный @cnicutars отвечает:
- функция
Possible(array A, string s)
- Если
s
пусто, верните true.
- вычислить массив
P
всех строк в A
, которые являются префиксом s
.
- Если
P
пусто, верните false.
- для каждой строки
P
в P
:
- if
Possible(A with p removed, s with prefix p removed)
return true
- return false
Ответ 6
То, что вы ищете, - это синтаксический анализатор. Парсер проверяет, принадлежит ли определенное слово определенному языку. Я не уверен в точной вычислительной сложности вашей проблемы. Некоторые из вышеперечисленных, кажется, правильны (вообще нет необходимости для исчерпывающего поиска). Одно точно, это не NP-Complete.
Азбукой вашего языка будет все мелкие подстроки.
Слово, которое вы ищете, это строка, которую вы используете.
Регулярное выражение может быть простой звездой Клини или просто простой контекстной свободной грамматикой, которая не что иное, как Ор.
Основная проблема в алгоритме: что, если некоторые подстроки на самом деле являются подстроками для других подстрок... то есть, если у нас есть подстроки: "ab", "abc", "abcd",... В этом случае порядок проверки подстрок изменит сложность. Для этого у нас есть LR-парсеры. Я думаю, они лучше всего подходят для решения таких проблем.
Я скоро найду точное решение.
Ответ 7
два варианта спринта, но ни один из них не кажется очень элегантным.
1) грубая сила: сделайте это, как будто бы вы создали генератор паролей, то есть word1 + word1 + word1 > word1 + word1 + word2 > word1 + word1 + word3 и т.д. и т.д. и т.д.
трюк - это длина, поэтому вам нужно попробовать все комбинации из двух или более слов, и вы не знаете, где установить предел. Очень много времени.
2) возьмите строку, о которой идет речь, и запустите поиск на ней для каждого слова, которое у вас есть по одному за раз. возможно, проверьте длину, и если ее больше 0, сделайте это снова. продолжайте делать это, пока вы не достигнете нуля, он не сможет найти больше результатов. если вы нажмете 0, то выиграйте, если не проиграете. Я думаю, что этот метод будет намного лучше первого, но я думаю, у кого-то будет лучшее предложение.
Ответ 8
Вот приблизительная идея, которая должна работать.
- Скопировать исходную строку в новую строку
- В то время как в копировальной строке все еще есть данные, а еще есть подстроки
а. Возьмите подстроку, если copy.contains(substr) copy.remove(substr)
- Если копия теперь пуста, то да, вы можете построить строку
- Если копия не пуста, выкиньте первый субстрат, который был удален из строки, и повторите.
- Если все подстроки ушли, а копия все еще не пуста, то нет, вы не можете ее создать.
Изменить:
Способ улучшить это будет состоять в том, чтобы сначала перебрать все подстроки и выбросить любые, которые не содержатся в основной строке. Затем выполните описанные выше шаги.
Ответ 9
Если каждая подстрока должна использоваться только один раз, но не все из них должны использоваться...
Для каждой перестановки размера N из подстрок, равных по размеру исходной строке, проверьте его, если нет, выполните перестановку элементов N + 1, завершите так, пока не исчерпаете все перестановки.
Конечно, NP полный, медленный, как черт, но я думаю, что никаких нормальных решений не существует.
Чтобы объяснить, почему решения, в которых удаление подстрок из исходной строки никогда не будут работать:
Введите строку "1234123" и массив "12", "34", "123" . Если вы удалите "123" с самого начала, у вас есть ложный минус. Аналогичный пример, где удаление с конца будет: "1234123": "23", "41", "123" .
С возвратом с жадным: (длина строки m 7, n num elements 3)
- возьмите самое длинное: 123
- удалить его из первого появления O (3)
- попробуйте другие два с остальными: no go + O ((n-1) * (m-3))
- backtrack O (1)
- удалить со второго: O (м-3)
- попробуйте другие два O ((n-1) * m-3)
= O (30)
Пермутации 1 + 2 + 3 = O (3) + O (4) + O (6) = O (13).
Таким образом, для небольших подмножеств длина перестановок на самом деле быстрее, чем откат. Это изменится, если вы попросите найти множество подстрок (в большинстве случаев, но не для всех).
Вы можете удалить только несуществующие подстроки из массива, чтобы уменьшить число перестановок от n ^ n до n ^ (n-1) для каждой удаленной несуществующей подстроки.
Ответ 10
Позвольте мне предложить использовать деревья суффикса (используя онлайн-алгоритм Ukkonen для его создания), который, по-видимому, подходит для поиска общих подстрок в двух текстах.
Вы можете найти дополнительную информацию в википедии/специальных источниках.
Задача
Find all z occurrences of the patterns P1..Pn of total length m
enter code hereas substrings in O(m + z) time.
поэтому вы видите, что существует очень крутое решение. Надеюсь, это сработает для вас.
Это больше подходит для повторного сканирования, а не для одного сканирования.