Как решить задачу от яндекса "НОП с восстановлением ответа"?
Решаю задачки от яндекса и попалась вот такая:
НОП с восстановлением ответа Средняя Даны две последовательности, требуется найти и вывести их наибольшую общую подпоследовательность.
Напомним:
Последовательность x x называется подпоследовательностью последовательности y y, если x x получается из ? y удалением нескольких (возможно, нуля или всех) элементов.
Наибольшая общая подпоследовательность - последовательность наибольшей длины, которая является подпоследовательностью обеих последовательностей.
Формат ввода В первой строке входных данных содержится число N – длина первой последовательности (1 ≤ ≤ N ≤ ≤ 1000). Во второй строке заданы члены первой последовательности (через пробел) – целые числа, не превосходящие 10000 по модулю.
В третьей строке записано число M – длина второй последовательности (1 ≤ ≤ M ≤ ≤ 1000). В четвертой строке задаются члены второй последовательности (через пробел) – целые числа, не превосходящие 10000 по модулю.
Формат вывода Требуется вывести наибольшую общую подпоследовательность данных последовательностей, через пробел. Если таких подпоследовательностей несколько, то можно вывести любую.
Примечание В примере 2 существует сразу три наибольшие общие подпоследовательности.
1
2
3
Любая из них будет правильным ответом.
Сначала написал обычное решение, которое берёт все подпоследовательности из первой последовательности (начиная с самой длинной) и сравнивает с подпоследовательностями такой же длины во второй последовательности. Несколько первых тестов прошли успешно, но потом вчитался в условие и понял, что сложность заключается в том, что из последовательности можно произвольно выкидывать элементы.
То есть если даны 2 последовательности:
1 2 3 4 5
1 7 4 3 5
То ответ будет 1 4 5
Не могу придумать как к этой задаче подступиться.
Ответы (2 шт):
есть вот такая идея алгоритма (LCS через DP) пусть у нас есть две последовательности:
A[0...N-1]B[0...M-1]
Создаём двумерный массив dp[N+1][M+1], где:
dp[i][j]— длина наибольшей общей подпоследовательности междуA[0:i]иB[0:j].
Формула будет такой:
if A[i-1] == B[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
Если говорить о полном коде, могу привести пример на Python
def lcs(A, B):
# Получаем длины последовательностей A и B
N = len(A)
M = len(B)
# Создаем двумерный массив dp размером (N+1) x (M+1), и инициализируем его нулями
# dp[i][j] будет хранить длину наибольшей общей подпоследовательности для первых i элементов A и первых j элементов B
dp = [[0] * (M + 1) for _ in range(N + 1)]
# Заполняем таблицу dp с использованием динамического программирования
for i in range(1, N + 1): # Перебираем элементы последовательности A
for j in range(1, M + 1): # Перебираем элементы последовательности B
if A[i - 1] == B[j - 1]: # Если текущие элементы равны
# Значение dp[i][j] равно dp[i-1][j-1] + 1 (прибавляем этот общий элемент)
dp[i][j] = dp[i - 1][j - 1] + 1
else:
# Иначе выбираем максимальную длину из предыдущих вариантов
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
# Восстанавливаем саму наибольшую общую подпоследовательность
i, j = N, M # Начинаем с конца таблицы
result = [] # Список для хранения элементов LCS
while i > 0 and j > 0: # Пока не достигнем начала таблицы
if A[i - 1] == B[j - 1]: # Если элементы на текущих позициях равны
result.append(A[i - 1]) # Добавляем элемент в результат
i -= 1 # Переходим к предыдущему элементу в A
j -= 1 # Переходим к предыдущему элементу в B
elif dp[i - 1][j] >= dp[i][j - 1]: # Если длина LCS без элемента B[j-1] больше или равна
i -= 1 # Переходим к предыдущему элементу в A
else: # Если длина LCS без элемента A[i-1] больше
j -= 1 # Переходим к предыдущему элементу в B
# Поскольку мы восстанавливаем последовательность с конца, нужно перевернуть результат
return result[::-1]
# Ввод данных
N = int(input()) # Длина первой последовательности
A = list(map(int, input().split())) # Первая последовательность
M = int(input()) # Длина второй последовательности
B = list(map(int, input().split())) # Вторая последовательность
print(*lcs(A, B))
Обозначим s и t строки для которых ищется наибольшая общая подпоследовательность / longest common subsequence:
s состоит из символов si, где 1 ≤ i ≤ m, m – длина строки s.
t состоит из символов tj, где 1 ≤ j ≤ n, n – длина строки t.
Нотация s[1,i] обозначает префикс строки s длиной i. Аналогично вводится t[1,j] – префикс t.
s[1,0] и t[1,0] – пустые строки.
Даны два префикса s[1,i] и t[1,j]. Нас интересует длина наибольшей общей подпоследовательности, которую обозначим fi,j.
Какие есть свойства у f?
fi,0 = f0,j = 0 – у любой строки нет ничего общего с пустой строкой, поэтому нули.
fi+1,j ≥ fi,j, fi,j+1 ≥ fi,j – монотонность. Чем длиннее префикс тем длиннее (не короче) общая часть. От добавления новых символов решение укоротиться не может.
fi-1,j-1 ≥ fi,j - 1 – f растёт не слишком быстро. Если оба префикса укоротить на один символ, то из общей последовательности может удалиться один последний символ, не больше.
если si = tj то fi,j = fi-1,j-1 + 1. Если символы равны, то к длиннейшей подпоследовательности (fi-1,j-1) добавляется единица (символ равный si и tj). Двойка и больше добавиться не может из-за свойства 3.
если si ≠ tj то fi,j = fi-1,j или fi,j = fi,j-1. Если символы не равны, то длиннейшая последовательность не может заканчиваться одновременно на si и на tj.
Если она заканчивается не на si, то fi,j = fi-1,j, так как символ si можно отбросить без ущерба для длины решения.
Если она заканчивается не на tj, то fi,j = fi,j-1, так как символ tj можно отбросить без ущерба для длины решения.
Собирая вместе свойства 1, 4, 5 получим формулу:
fi,j = 0, если i = 0 или j = 0 (свойство 1);
fi,j = fi-1,j-1 + 1, если i, j > 0 и si = tj (свойство 4);
fi,j = max(fi-1,j, fi,j-1), если i, j > 0 и si ≠ tj (свойство 5).
Формула позволяет вычислить fi,j если известны значения fi-1,j, fi,j-1, fi-1,j-1. Сложность вычисления fi,j и по времени и по памяти O(ij). Сложность по памяти можно сократить до O(min(i, j)) если требуется вычислить только длину. Если кроме длины нужно предъявить общую последовательность, то придётся хранить все значения f.
Восстанавливается подпоследовательность от конца к началу.
Из позиции (i, j) надо перейти в позицию
- (i - 1, j - 1), если i, j > 0 и si = tj;
- (i - 1, j), если i, j > 0 и si ≠ tj и fi,j = fi-1,j;
- (i, j - 1), если i, j > 0 и si ≠ tj и fi,j = fi,j-1.
Если i = 0 или j = 0, поиск завершается. Каждый раз, когда на маршруте было равенство si = tj, соответствующий символ записывается в ответ. В конце ответ надо будет перевернуть задом наперёд. Или, если хотите избежать переворота, с самого начала стройте подпоследовательность из конца строк s и t в начало.
