Ответ 1
(Обновление: я переписал открытие этого ответа, чтобы включить обсуждение сложности и обсудить некоторые альтернативные методы и потенциальные риски.)
(Короткий ответ, единственное реальное улучшение выше подхода O (nm), о котором я могу думать, - это наблюдать, что нам обычно не нужно вычислять все n раз m записей в таблице. Мы можем вычислить только те ячейки нам нужно, но на практике это может быть очень хорошо, в зависимости от набора данных.)
Уточнить проблему: у нас есть строка S
длины n
и строка T
длины m
. Максимально допустимый зазор составляет k
- этот зазор должен применяться и в начале и в конце строки. Разрыв - это количество непревзойденных символов между двумя совпадающими символами - т.е. Если буквы смежны, это пробел 0
, а не 1
.
Представьте таблицу с n+1
строками и столбцами m+1
.
0 1 2 3 4 ... m
--------------------
0 | ? ? ? ? ? ?
1 | ? ? ? ? ? ?
2 | ? ? ? ? ? ?
3 | ? ? ? ? ? ?
... |
n | ? ? ? ? ? ?
Сначала мы могли бы определить, что запись в строке r
и столбце c
является двоичным флагом, который сообщает нам, являются ли первые r
символы S
допустимой k
-последовательностью из первых c
символов T
. (Не волнуйтесь, как вычислить эти значения, или даже если эти значения полезны, нам просто нужно определить их в первую очередь.)
Однако эта таблица двоичного флага не очень полезна. Невозможно легко вычислить одну ячейку как функцию близлежащих клеток. Вместо этого нам нужно, чтобы каждая ячейка хранила немного больше информации. Также, как и запись того, соответствуют ли соответствующие строки правильной подпоследовательности, нам нужно записать количество последовательных непревзойденных символов в конце нашей подстроки T
(подстрока с символами c
). Например, если первые r=2
символы S
являются "ab"
, а первые c=3
символы T
являются "abb"
, тогда возможны два возможных совпадения: первые символы, очевидно, совпадают друг с другом, но b
может совпадать с любым из последних b
. Поэтому у нас есть выбор оставить один или ноль непревзойденного b
в конце. Какой из них мы записываем в таблице?
Ответ заключается в том, что если ячейка имеет несколько допустимых значений, то мы берем наименьшую из них. Логично, что мы хотим сделать жизнь максимально легкой для себя, сопоставляя остальную часть строки, и, следовательно, чем меньше разрыв в конце, тем лучше. Будьте осторожны с другими неправильными выборами - мы не хотим сопоставлять столько символов, сколько возможно, или как несколько символов. Это может иметь неприятные последствия. Но для данной пары строк S,T
логично найти совпадение (если есть какие-либо допустимые соответствия), который минимизирует разрыв в конце.
Еще одно замечание состоит в том, что если строка S
намного короче T
, то она не может совпадать. Это также зависит от k
. Максимальная длина, которую может покрывать S
, составляет rk
, если она меньше c
, тогда мы можем легко пометить (r,c)
как -1
.
(Любые другие утверждения оптимизации, которые могут быть сделаны?)
Нам не нужно вычислять все значения в этой таблице. Число различных возможных состояний k + 3. Они начинаются в состоянии undefined '(?
). Если совпадение невозможно для пары (под) строк, состояние -
. Если совпадение возможно, то оценка в ячейке будет числом от 0 до k включительно, записывая наименьшее возможное количество непревзойденных последовательных символов в конце. Это дает нам общее число k + 3.
Нам интересна только запись в правом нижнем углу таблицы. Если f(r,c)
- это функция, которая вычисляет конкретную ячейку, то нас интересует только f(n,m)
. Значение для конкретной ячейки может быть вычислено как функция близких значений. Мы можем построить рекурсивный алгоритм, который принимает в качестве входных данных r
и c
и выполняет соответствующие вычисления и поиски в терминах близких значений. Если эта функция просматривает f(r,c)
и находит ?
, она будет продолжать и вычислять ее, а затем сохранять ответ.
Важно сохранить ответ, поскольку алгоритм может запрашивать одну и ту же ячейку много раз. Но также, некоторые клетки никогда не будут вычислены. Мы просто начинаем пытаться вычислить одну ячейку (внизу справа) и просто искать и вычислять и хранить по мере необходимости.
Это "очевидный" подход O (nm). Единственная оптимизация здесь - это наблюдение, что нам не нужно вычислять все ячейки, поэтому это должно привести к сложности ниже O (nm). Конечно, с действительно неприятными наборами данных, вы можете в конечном итоге вычислить почти все ячейки! Поэтому сложно поставить официальную оценку сложности.
Наконец, я должен сказать, как вычислить определенную ячейку f(r,c)
:
- Если
r==0
иc <= k
, тогдаf(r,c) = 0
. Пустая строка может соответствовать любой строке с символамиk
. - Если
r==0
иc > k
, тогдаf(r,c) = -1
. Слишком долго для матча. - Есть только два других способа, которыми ячейка может иметь успешное состояние. Сначала мы попробуем:
-
- Если
S[r]==T[c]
иf(r-1,c-1) != -1
, тогдаf(r,c) = 0
. Это лучший случай - матч без отставания.
- Если
-
- Если это не сработало, мы попробуем следующее лучшее. Если
f(r,c-1) != -1
иf(r,c) < k
, тогдаf(r,c) = f(r,c-1)+1
.
- Если это не сработало, мы попробуем следующее лучшее. Если
-
- Если ни один из них не работает, тогда
f(r,c) = -1
.
- Если ни один из них не работает, тогда
Остальная часть этого ответа - мой первоначальный подход на основе Haskell. Одно из преимуществ этого заключается в том, что он "понимает", что ему не нужно вычислять каждую ячейку, а только вычислить ячейки там, где это необходимо. Но это может привести к неэффективности вычисления одной ячейки много раз.
* Также обратите внимание, что подход Haskell эффективно подходит к проблеме в зеркальном изображении - он пытается построить совпадения от концевых подстрок S
и T
, где минимальная ведущая пучка непревзойденных символов. У меня нет времени переписать его в форме "зеркального изображения"!
Рекурсивный подход должен работать. Нам нужна функция, которая будет принимать три аргумента: int k
, String S
и String T
. Однако нам не просто нужен логический ответ о том, является ли S действительной k-подпоследовательностью T.
Для этого рекурсивного подхода, если S - действительная k-подпоследовательность, мы также хотим знать о лучшей возможной подпоследовательности, возвращая, как можно отбросить несколько символов с начала T. Мы хотим найти "лучшую" подпоследовательность. Если k-подпоследовательность невозможна для S и T, то мы возвращаем -1, но если это возможно, мы хотим вернуть наименьшее количество символов, которые мы можем вытащить из T, сохраняя свойство k-подпоследовательности.
helloworld
l r d
Это действительная 4-подпоследовательность, но наибольший разрыв имеет (самое большее) четыре символа (lowo
). Это лучшая подпоследовательность, потому что она оставляет пробел всего двух символов в начале (he
). В качестве альтернативы, здесь есть еще одна действительная k-подпоследовательность с теми же строками, но она не так хороша, потому что она оставляет зазор в три раза:
helloworld
l r d
Это написано в Haskell, но должно быть достаточно легко переписать на любой другой язык. Я разберу его более подробно ниже.
best :: Int -> String -> String -> Int
-- K S T return
-- where len(S) <= len(T)
best k [] t_string -- empty S is a subsequence of anything!
| length(t_string) <= k = length(t_string)
| length(t_string) > k = -1
best k [email protected](s:ss) [] = (-1) -- if T is empty, and S is non-empty, then no subsequence is possible
best k [email protected](s:ss) [email protected](t:ts) -- both are non-empty. Various possibilities:
| s == t && best k ss ts /= -1 = 0 -- if s==t, and if best k ss ts != -1, then we have the best outcome
| best k sss ts /= -1
&& best k sss ts < k = 1+ (best k sss ts) -- this is the only other possibility for a valid k-subsequence
| otherwise = -1 -- no more options left, return -1 for failure.
Поэтапный анализ:
(Комментарий в Haskell начинается с --
)
best :: Int -> String -> String -> Int
Функция, которая принимает Int и две строки и возвращает Int. Возвращаемое значение должно быть -1, если k-подпоследовательность невозможна. В противном случае он вернет целое число от 0 до K (включительно), сообщив нам наименьший возможный пробел в начале T.
Мы просто разбираем дела по порядку.
best k [] t -- empty S is a subsequence of anything!
| length(t) <= k = length(t)
| length(t) > k = -1
Выше мы обрабатываем случай, когда S пусто ([]
). Это просто, так как пустая строка всегда является допустимой подпоследовательностью. Но чтобы проверить, является ли это допустимой k-подпоследовательностью, мы должны вычислить длину T.
best k [email protected](s:ss) [] = (-1)
-- if T is empty, and S is non-empty, then no subsequence is possible
Этот комментарий объясняет это. Это оставляет нас в ситуациях, когда обе строки непусты:
best k [email protected](s:ss) [email protected](t:ts) -- both are non-empty. Various possibilities:
| s == t && best k ss ts /= -1 = 0 -- if s==t, and if best k ss ts != -1, then we have the best outcome
| best k sss ts /= -1
&& best k sss ts < k = 1+ (best k sss ts) -- this is the only other possibility for a valid k-subsequence
| otherwise = -1 -- no more options left, return -1 for failure.
[email protected](t:ts)
соответствует непустой строке. Имя строки tts
. Но в Haskell есть также удобный трюк, позволяющий вам давать имена первой букве в строке (T
) и остаток строки (ts
). Здесь ts
следует читать вслух, поскольку множественное число T
- суффикс S
здесь означает "множественное число". Мы говорим, что имеем T
и некоторые ts
, и вместе они создают полную (непустую) строку.
Этот последний блок кода относится к случаю, когда обе строки не являются пустыми. Две строки называются sss
и tts
. Но чтобы избавить нас от необходимости писать head sss
и tail sss
для доступа к первой букве и остатку строки строки, мы просто используем @(s:ss)
, чтобы сообщить компилятору сохранить эти величины в переменных S
и ss
. Если это был С++, например, вы бы получили тот же эффект с char s = sss[0];
как первая строка вашей функции.
Лучшая ситуация заключается в том, что первые символы соответствуют s==t
, а оставшаяся часть строк является допустимой k-подпоследовательностью best k sss ts /= -1
. Это позволяет нам вернуть 0.
Единственная другая возможность для успеха, если текущая полная строка (sss
) является допустимой k-подпоследовательностью остальной части другой строки (ts
). Мы добавляем 1 к этому и возвращаемся, но делаем исключение, если разрыв будет слишком большим.
Очень важно не менять порядок этих последних пяти строк. Они - порядок в порядке убывания того, насколько хороша оценка. Мы хотим протестировать и сначала вернуть наилучшие возможности.