Предисловие
Привет, Хабр! Наткнулся на достаточно интересную задачу по динамическому программированию с нетривиальным восстановлением ответа. Поэтому пишу эту статью, с целью помочь интересующимся.
Условие задачи
Даны две последовательности, требуется найти и вывести их НОВП (наибольшую общую возрастающую подпоследовательность).
Формат ввода
Во входном файле записаны две последовательности. Каждая последовательность описывается двумя строками следующим образом: в первой строке идет длина последовательности , во второй идут
целых чисел
- члены последовательности.
Формат вывода
В первой строке выходного файла выведите длину наибольшей общей возрастающей подпоследовательности. Во второй строке выходного файла выведите саму подпоследовательность.
Решение
Найдем простое, но неэффективное решение задачи, а потом попробуем оптимизировать его временную сложность.
К сожалению, нельзя просто найти наибольшую общую подпоследовательность, а потом в ней найти наибольшую возрастающую.
Контрпример
Последовательности (1, 5, 3, 2, 1) и (5, 3, 2, 1, 5).
Наибольшей общей является подпоследовательность (5, 3, 2, 1), но длина наибольшей возрастающей в ней равна 1.
При этом существует возрастающая общая подпоследовательность (1, 5), длина которой равна 2.
Пусть на вход поданы последовательность first_array длины first_size и последовательность second_array длины second_size.
Рассмотрим следующую динамику: создадим двумерную таблицу dp размера first_size x second_size. Элемент на i-ой строке и j-ом столбце будет хранить длину НОВП для первых i символов последовательности first_array, и первых j символов последовательности second_array. При этом НОВП должна оканчиваться на a[i] (a[i] и b[j] должны быть равны). Если first_array[i] и second_array[j] различны, то будем хранить 0. Таблицу заполняем слева направо, сверху вниз.
Предположим, что в текущий момент необходимо найти значение элемента dp[i][j]. В случае, когда first_array[i] и second_array[j] совпадают, искомая НОВП будет оканчиваться на first_array[i], а вся остальная её часть будет являться НОВП для какой-либо другой пары индексов k и l (k < i, l < j). Тогда, нужно найти максимум из всех элементов dp[k][l] с подходящими k и l, после чего прибавить единицу - получим значение dp[i][j]. Не забываем проверять, что подпоследовательность действительно возрастающая (first_array[k] < first_array[i]).

Поиск длины НОВП
for (size_t i = 0; i < first_size; ++i) {
for (size_t j = 0; j < second_size; ++j) {
if (first_array[i] != second_array[j]) {
// dp[i][j] уже 0, идем дальше
continue;
}
dp[i][j] = 1; // при равенстве элементов есть хотя бы одна НОВП длины 1
// перебираем НОВП на подмассивах
for (size_t k = 0; k < i; ++k) {
for (size_t l = 0; l < j; ++l) {
if (first_array[k] < first_array[i] && dp[k][l] + 1 > dp[i][j]) {
dp[i][j] = dp[k][l] + 1;
parents[i][j] = std::make_pair(k, l);
}
}
}
}
}Для восстановления ответа создадим двумерный массив parents, который будет хранить пары из индексов, где первый элемент - индекс в первом массиве, второй - индекс во втором. parents[i][j] хранит (k, l), если dp[k][l] это максимум из всех доступных элементов таблицы dp при вычислении dp[i][j].
Весь код
#include <iostream>
#include <vector>
#include <algorithm>
#include <tuple>
std::vector<int> read_array() {
size_t size = 0;
std::cin >> size;
std::vector<int> array(size, 0);
for (size_t i = 0; i < size; ++i) {
std::cin >> array[i];
}
return array;
}
std::vector<int> find_lcis(const std::vector<int>& first_array, const std::vector<int>& second_array) {
size_t first_size = first_array.size();
size_t second_size = second_array.size();
std::vector<std::vector<int>> dp(first_size, std::vector<int>(second_size, 0));
std::vector<std::vector<std::pair<size_t, size_t>>> parents(
first_size,
std::vector<std::pair<size_t, size_t>>(second_size, std::make_pair(0, 0))
);
// заполнение dp
for (size_t i = 0; i < first_size; ++i) {
for (size_t j = 0; j < second_size; ++j) {
if (first_array[i] != second_array[j]) {
// dp[i][j] уже 0, идем дальше
continue;
}
dp[i][j] = 1; // при равенстве элементов есть хотя бы одна НОВП длины 1
// перебираем НОВП на подмассивах
for (size_t k = 0; k < i; ++k) {
for (size_t l = 0; l < j; ++l) {
if (first_array[k] < first_array[i] && dp[k][l] + 1 > dp[i][j]) {
dp[i][j] = dp[k][l] + 1;
parents[i][j] = std::make_pair(k, l);
}
}
}
}
}
// восстановление ответа
size_t best_first = 0;
size_t best_second = 0;
for (size_t i = 0; i < first_size; ++i) {
for (size_t j = 0; j < second_size; ++j) {
if (dp[i][j] > dp[best_first][best_second]) {
best_first = i;
best_second = j;
}
}
}
// теперь мы готовы восстановить ответ по матрице parents
std::vector<int> answer(dp[best_first][best_second], 0);
size_t current_first = best_first;
size_t current_second = best_second;
for (size_t i = 0; i < answer.size(); ++i) {
answer[i] = first_array[current_first];
// <-> x,y = parents[x][y].first, parents[x][y].second
std::tie(current_first, current_second) = parents[current_first][current_second];
}
std::reverse(answer.begin(), answer.end());
return answer;
}
int main() {
std::vector<int> first_array = read_array();
std::vector<int> second_array = read_array();
std::vector<int> answer = find_lcis(first_array, second_array);
std::cout << answer.size() << '\n';
for (size_t i = 0; i < answer.size(); ++i) {
std::cout << answer[i] << ' ';
}
std::cout << '\n';
}Полученное решение имеет временную сложность (
- длина первого массива,
- длина второго).
Оптимизация 1.
Получили решение с большой временной сложностью. Как оптимизировать?
Сделаем условия для элементов dp более мягкими. Теперь dp[i][j] будет хранить длину НОВП для первых i символов последовательности first_array, и первых j символов последовательности second_array, но теперь first_array[i] не обязательно является концом НОВП (теперь элементы first_array[i] и second_array[j] могут быть различны).
Заполнять таблицу dp будем следующим образом:
Если
i == 0, то приfirst_array[i] == second_array[j], очевидно,dp[i][j] = 1, иначеdp[i][j] = 0.Если
first_array[i] != second_array[j], то мы не можем добавить к какой-либо последовательности элементfirst_array[i]в конец. Тогда вdp[i][j]положим максимальное значение из всех уже посчитанных сsecond_array[j]на конце. Т.е. максимум изdp[k][j], гдеk < iЕсли
first_array[i] == second_array[j], то найдём НОВП, в конец которой можно добавитьsecond_array[j]. Для этого, аналогично первому решению, переберем все парыkиl(гдеk < i, l < j) и найдем максимум средиdp[k][l].
Заметим, что согласно алгоритму заполнения таблицы dp, элементы в ней не убывают при увеличении i (т.е. для любого значения j последовательность dp[0][j], dp[1][j], ..., dp[i - 1][j] нестрого возрастает).
Тогда, в случае неравенства first_array[i] и second_array[j] пропадает необходимость в переборе, т.к. мы сразу знаем что dp[i - 1][j] максимальный элемент и dp[i][j] = dp[i - 1][j].
В случае равенства можем заменим два цикла на один:
Оптимизация заполнения dp
// было
for (size_t i = 0; i < first_size; ++i) {
for (size_t j = 0; j < second_size; ++j) {
if (i == 0) {
if (first_array[i] == second_array[j]) {
dp[i][j] = 1;
}
continue;
}
if (first_array[i] != second_array[j]) {
for (size_t k = 0; k < i; ++k) {
if (dp[k][j] > dp[i][j]) {
dp[i][j] = dp[k][j];
}
}
} else {
dp[i][j] = 1;
for (size_t k = 0; k < i; ++k) {
for (size_t l = 0; l < j; ++l) {
if (dp[k][l] + 1 > dp[i][j] && second_array[l] < second_array[j]) {
dp[i][j] = dp[k][l] + 1;
}
}
}
}
}
}
// стало
for (size_t i = 0; i < first_size; ++i) {
for (size_t j = 0; j < second_size; ++j) {
if (i == 0) {
if (first_array[i] == second_array[j]) {
dp[i][j] = 1;
}
continue;
}
if (first_array[i] != second_array[j]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = 1;
for (size_t l = 0; l < j; ++l) {
if (dp[i - 1][l] + 1 > dp[i][j] && second_array[l] < second_array[j]) {
dp[i][j] = dp[i - 1][l] + 1;
}
}
}
}
}Теперь, при заполнении родителей в случае first_array[i] != second_array[j] будем "ссылаться" на родителя лучшего варианта, т.е. dp[i - 1][j]:parents[i][j] = parents[i - 1][j]
При first_array[i] == second_array[j] заполняем по-прежнему парой индексов с лучшей длиной НОВП.
Важно обратить внимание: теперь индексы из первого массива, которые хранятся в парах таблицы parents, не обязательно указывают на элементы искомой НОВП (мы ослабили условие). Следовательно, восстанавливать НОВП нужно через индексы второго массива.
Элементы dp растут при увеличении первого индекса, поэтому для поиска наибольшего элемента таблицы в конце достаточно пройтись по самому правому столбцу.
Весь код
#include <iostream>
#include <vector>
#include <algorithm>
#include <tuple>
std::vector<int> read_array() {
size_t size = 0;
std::cin >> size;
std::vector<int> array(size, 0);
for (size_t i = 0; i < size; ++i) {
std::cin >> array[i];
}
return array;
}
std::vector<int> find_lcis(const std::vector<int>& first_array, const std::vector<int>& second_array) {
size_t first_size = first_array.size();
size_t second_size = second_array.size();
std::vector<std::vector<int>> dp(first_size, std::vector<int>(second_size, 0));
std::vector<std::vector<std::pair<size_t, size_t>>> parents(
first_size,
std::vector<std::pair<size_t, size_t>>(second_size, std::make_pair(0, 0))
);
// заполнение dp
for (size_t i = 0; i < first_size; ++i) {
for (size_t j = 0; j < second_size; ++j) {
if (i == 0) {
if (first_array[i] == second_array[j]) {
dp[i][j] = 1;
}
continue;
}
if (first_array[i] != second_array[j]) {
dp[i][j] = dp[i - 1][j];
parents[i][j] = parents[i - 1][j];
} else {
dp[i][j] = 1;
for (size_t l = 0; l < j; ++l) {
if (dp[i - 1][l] + 1 > dp[i][j] && second_array[l] < second_array[j]) {
dp[i][j] = dp[i - 1][l] + 1;
parents[i][j] = std::make_pair(i - 1, l);
}
}
}
}
}
// восстановление ответа
size_t best_first = first_size - 1;
size_t best_second = 0;
for (size_t j = 0; j < second_size; ++j) {
if (dp[best_first][j] > dp[best_first][best_second]) {
best_second = j;
}
}
// теперь мы готовы восстановить ответ по матрице parents
std::vector<int> answer(dp[best_first][best_second], 0);
size_t current_first = best_first;
size_t current_second = best_second;
for (size_t i = 0; i < answer.size(); ++i) {
answer[i] = second_array[current_second];
// <-> x,y = parents[x][y].first, parents[x][y].second
std::tie(current_first, current_second) = parents[current_first][current_second];
}
std::reverse(answer.begin(), answer.end());
return answer;
}
int main() {
std::vector<int> first_array = read_array();
std::vector<int> second_array = read_array();
std::vector<int> answer = find_lcis(first_array, second_array);
std::cout << answer.size() << '\n';
for (size_t i = 0; i < answer.size(); ++i) {
std::cout << answer[i] << ' ';
}
std::cout << '\n';
}Это решение имеет временную сложность .
Оптимизация 2.
Подробнее рассмотрим нахождение dp[i][j] при равенстве first_array[i] == second_array[j].
Мы заполняем таблицу слева направо, сверху вниз. Поэтому внутри каждой строки (i = const) можно считать элемент первого массива first_array[i] фиксированным. Именно с ним мы будем сравнивать элементы второго массива от second_array[0] до second_array[second_size - 1].
Рассмотрим заполнение каждой строки таблицы dp.
Перебираем по возрастанию индексы
jвторого массиваВ случае неравенства
first_array[i] != second_array[j]тратимвремени, заполняя либо
dp[i][j] = 1, либоdp[i][j] = dp[i - 1][j]В случае равенства
first_array[i] == second_array[j]пробегаем по всем индексамlот0доj - 1и находим элемент второго массива, который меньшеsecond_array[j], чтобы при этом величинаdp[i - 1][l]была максимальна.
Получаем времени на каждую строку.
Т.к. first_array[i] в каждой строке постоянный, то получается, что при равенстве мы ищем максимум dp[i - 1][k], где k лежит от 0 до j - 1 и при это second_array[k] < first_array[i]. Попробуем оптимизировать до
:
Теперь мы будем хранить этот максимум в отдельной переменной. Будем использовать переменные best_len - само значение максимума и best_index - соответствующий максимуму индекс во втором массиве.
Тогда в процессе мы будем обновлять значения этих переменных, и в случае равенства first_array[i] == second_array[j] сможем быстро определить значение dp[i][j]:dp[i][j] = best_len + 1
Обновлять значения этих переменных будем в случае, когда мы нашли элемент second_array[k] меньший first_array[i], при этом best_len < dp[k][i]. Тогда best_len = dp[k][i] и best_index = k.
Поиск длины НОВП
for (size_t i = 0; i < first_size; ++i) {
size_t best_index = 0;
int best_len = 0;
for (size_t j = 0; j < second_size; ++j) {
if (i == 0) {
if (first_array[i] == second_array[j]) {
dp[i][j] = 1;
}
continue;
}
if (first_array[i] != second_array[j]) {
dp[i][j] = dp[i - 1][j];
parents[i][j] = parents[i - 1][j];
// проверяем можно ли обновить best_len
if (first_array[i] > second_array[j] && dp[i - 1][j] > best_len) {
best_len = dp[i - 1][j];
best_index = j;
}
} else {
if (best_len + 1 > dp[i][j]) {
dp[i][j] = best_len + 1;
parents[i][j] = std::make_pair(i - 1, best_index);
}
}
}
}Этим улучшением оптимизируем временную сложность алгоритма до .
Весь код
#include <iostream>
#include <vector>
#include <algorithm>
#include <tuple>
std::vector<int> read_array() {
size_t size = 0;
std::cin >> size;
std::vector<int> array(size, 0);
for (size_t i = 0; i < size; ++i) {
std::cin >> array[i];
}
return array;
}
std::vector<int> find_lcis(const std::vector<int>& first_array, const std::vector<int>& second_array) {
size_t first_size = first_array.size();
size_t second_size = second_array.size();
std::vector<std::vector<int>> dp(first_size, std::vector<int>(second_size, 0));
std::vector<std::vector<std::pair<size_t, size_t>>> parents(
first_size,
std::vector<std::pair<size_t, size_t>>(second_size, std::make_pair(0, 0))
);
// заполнение dp
for (size_t i = 0; i < first_size; ++i) {
size_t best_index = 0;
int best_len = 0;
for (size_t j = 0; j < second_size; ++j) {
if (i == 0) {
if (first_array[i] == second_array[j]) {
dp[i][j] = 1;
}
continue;
}
if (first_array[i] != second_array[j]) {
dp[i][j] = dp[i - 1][j];
parents[i][j] = parents[i - 1][j];
// проверяем можно ли обновить best_len
if (first_array[i] > second_array[j] && dp[i - 1][j] > best_len) {
best_len = dp[i - 1][j];
best_index = j;
}
} else {
if (best_len + 1 > dp[i][j]) {
dp[i][j] = best_len + 1;
parents[i][j] = std::make_pair(i - 1, best_index);
}
}
}
}
// восстановление ответа
size_t best_first = first_size - 1;
size_t best_second = 0;
for (size_t j = 0; j < second_size; ++j) {
if (dp[best_first][j] > dp[best_first][best_second]) {
best_second = j;
}
}
// теперь мы готовы восстановить ответ по матрице parents
std::vector<int> answer(dp[best_first][best_second], 0);
size_t current_first = best_first;
size_t current_second = best_second;
for (size_t i = 0; i < answer.size(); ++i) {
answer[i] = second_array[current_second];
// <-> x,y = parents[x][y].first, parents[x][y].second
std::tie(current_first, current_second) = parents[current_first][current_second];
}
std::reverse(answer.begin(), answer.end());
return answer;
}
int main() {
std::vector<int> first_array = read_array();
std::vector<int> second_array = read_array();
std::vector<int> answer = find_lcis(first_array, second_array);
std::cout << answer.size() << '\n';
for (size_t i = 0; i < answer.size(); ++i) {
std::cout << answer[i] << ' ';
}
std::cout << '\n';
}Надеюсь, эта статья помогла вам лучше понять задачу и её решение. Желаю удачи в программировании!
wataru
Хорошая статья. В отличии от многих статей про алгоритмы на хабре тут действитеньно объясняется "как" и "почему" а не "что", и статья не является лишь текстовой копией кода в стиле:
Единественное замечаение: стоило бы заострить внимание, почему в последней версии сравниваются second_array[k] и first_array[i], ведь до этого сравнивались 2 значения в second_array.
ConsigliereNumberTwo Автор
Благодарю за комментарий! Вы правы, этот момент я несколько замял под ковёр.
В предпоследней версии сравнение
second_array[l] < second_array[j]используется для поддержания возрастания подпоследовательности, однако в этом случае можно сравнивать иsecond_array[l] < first_array[i], т.к. внутри этого блока элементы первого и второго массивов равны.В последней версии при заполнении строки
iлюбой элементsecond_arrayбудет сравниваться сfirst_array[i]на равенство. То есть при равенствеfirst_array[i] == second_array[j]неравенстваfirst_array[i] > second_array[k]иsecond_array[j] > second_array[k]будут эквивалентны. Поэтому, достаточно сравнения сfirst_array[i], потому что при встрече равного элемента из второго массива мы будем уверенны в возрастании подпоследовательности.Надеюсь, что смог подробнее объяснить этот момент!