E122. Нахождение наидлиннейшей возрастающей подпоследовательности
Источник: e-maxx.ru/algo, страница PDF 401.
题意 задачи следующее. Дан 数组 из
чисел:
. it is required find в
этой последовательности строго возрастающую subsequence наибольшей длины. Формально это выглядит следующим образом: it is required find такую последовательность индексов , что:
В данной статье рассматриваются различные 算法ы решения данной задачи, а также некоторые задачи, которые можно свести к данной задаче.
解法 за
: метод динамического программирования
dynamic programming — это весьма общая методика, позволяющая решать огромный класс задач. Здесь мы рассмотрим эту методику применительно к нашей конкретной задаче. Научимся сначала искать длину наидлиннейшей возрастающей подпоследовательности, а восстановлением самой подпоследовательности займёмся чуть позже.
dynamic programming для поиска длины ответа
Для этого давайте научимся считать 数组
, где
— это длина наидлиннейшей
возрастающей подпоследовательности, оканчивающейся именно в elementе с индексом
. 数组 этот (он и есть —
сама динамика) будем считать постепенно: сначала
, затем
и т.д. В конце, когда этот 数组 будет
подсчитан нами, ответ на задачу будет равен максимуму в 数组е
.
Итак, пусть текущий индекс —
, т.е. мы хотим посчитать значение
, а все предыдущие значения
уже подсчитаны. Тогда заметим, что у нас есть два варианта:
● либо
, т.е. искомая subsequence состоит только из числа
.
● либо
. Тогда перед numberм
в искомой подпоследовательности стоит какое-то другое number.
Давайте переберём это number: это может быть любой element
, но такой, что
. Пусть мы рассматриваем какой-то текущий индекс
. Поскольку динамика
для него уже
подсчитана, получается, что это number
вместе с numberм
даёт ответ
. Таким образом,
можно считать по такой формуле: Объединяя эти два варианта в один, получаем окончательный 算法 для вычисления : Этот 算法 — и есть сама динамика.
实现
Приведём реализацию описанного выше 算法а, которая находит и выводит длину наидлиннейшей возрастающей подпоследовательности:
int d[MAXN]; // константа MAXN равна наибольшему возможному значению n
for (int i=0; i<n; ++i) {
d[i] = 1;
for (int j=0; j<i; ++j)
if (a[j] < a[i])
d[i] = max (d[i], 1 + d[j]);
}
int ans = d[0];
for (int i=0; i<n; ++i)
ans = max (ans, d[i]);
cout << ans << endl;
Восстановление ответа
Пока мы лишь научились искать длину ответа, но саму наидлиннейшую subsequence мы вывести не можем, т. к. не сохраняем никакой дополнительной информации о том, где достигаются максимумы.
Чтобы суметь восстановить ответ, помимо динамики
надо также хранить вспомогательный
数组
— то, в каком месте достигся максимум для каждого значения
. Иными словами, индекс
будет обозначать тот самый индекс
, при котором получилось наибольшее значение
. (Этот 数组
в динамическом программировании часто называют "数组ом предков".) Тогда, чтобы вывести ответ, надо просто идти от elementа с максимальным значением
по его предкам до тех
пор, пока мы не выведем всю subsequence, т.е. пока не дойдём до elementа со значением .
实现 восстановления ответа
Итак, у нас изменится и код самой динамики, и добавится код, производящий вывод наидлиннейшей подпоследовательности (выводятся индексы elementов подпоследовательности, в 0-индексации).
Для удобства мы изначально положили индексы
: для elementов, у которых динамика получилась
равной единице, это значение предка так и останется минус единицей, что чуть-чуть удобнее при восстановлении ответа.
int d[MAXN], p[MAXN]; // константа MAXN равна наибольшему возможному значению n
for (int i=0; i<n; ++i) {
d[i] = 1;
p[i] = -1;
for (int j=0; j<i; ++j)
if (a[j] < a[i])
if (1 + d[j] > d[i]) {
d[i] = 1 + d[j];
p[i] = j;
} }
int ans = d[0], pos = 0;
for (int i=0; i<n; ++i)
if (d[i] > ans) {
ans = d[i];
pos = i;
}
cout << ans << endl;
vector<int> path;
while (pos != -1) {
path.push_back (pos);
pos = p[pos];
}
reverse (path.begin(), path.end());
for (int i=0; i<(int)path.size(); ++i)
cout << path[i] << ' ';
Альтернативный способ восстановления ответа
Впрочем, как почти всегда в случае динамического программирования, для восстановления ответа можно не
хранить дополнительный 数组 предков
, а просто заново пересчитывая текущий element динамики и ища, на каком же индексе был достигнут максимум. Этот способ при реализации приводит к чуть более длинному коду, однако взамен получаем экономию памяти и абсолютное совпадение логики программы в процессе подсчёта динамики и в процессе восстановления.
解法 за
: dynamic programming
с двоичным поиском
Чтобы получить более быстрое 解法 задачи, построим другой вариант динамического программирования за
,
а затем поймём, как можно этот вариант ускорить до
.
Динамика теперь будет такой: пусть
— это number, на которое оканчивается
возрастающая subsequence длины
(а если таких чисел несколько — то наименьшее из них).
Изначально мы полагаем
, а все остальные elementы
.
Считать эту динамику мы будем постепенно, обработав number
, затем
, и т.д.
Приведём реализацию этой динамики за
:
int d[MAXN];
d[0] = -INF;
for (int i=1; i<=n; ++i)
d[i] = INF;
for (int i=0; i<n; i++)
for (int j=1; j<=n; j++)
if (d[j-1] < a[i] && a[i] < d[j])
d[j] = a[i];
Заметим теперь, что у этой динамики есть одно очень важное свойство:
для
всех
. Другое свойство — что каждый element
обновляет максимум одну ячейку
.
Таким образом, это означает, что обрабатывать очередное
мы можем за
, сделав двоичный поиск
по 数组у
. В самом деле, мы просто ищем в 数组е
первое number, которое строго больше
, и
пытаемся произвести обновление этого elementа аналогично приведённой выше реализации.
实现 за
Воспользовавшись стандартным в языке C++ 算法ом двоичного поиска
(который
returns позицию первого elementа, строго большего данного), получаем такую простую реализацию:
int d[MAXN];
d[0] = -INF;
for (int i=1; i<=n; ++i)
d[i] = INF;
for (int i=0; i<n; i++) {
int j = int (upper_bound (d.begin(), d.end(), a[i]) - d.begin());
if (d[j-1] < a[i] && a[i] < d[j])
d[j] = a[i];
}
Восстановление ответа
По такой динамике тоже можно восстановить ответ, для чего опять же помимо динамики
также надо хранить
数组 "предков"
— то, на elementе с каким индексом оканчивается оптимальная subsequence длины
. Кроме того, для каждого elementа 数组а
надо будет хранить его "предка" — т.е. индекс того elementа,
который должен стоять перед
в оптимальной подпоследовательности. Поддерживая эти два 数组а по ходу вычисления динамики, в конце будет нетрудно восстановить искомую subsequence. (Интересно отметить, что применительно к данной динамике ответ можно восстанавливать только так, через 数组ы предков — а без них восстановить ответ после вычисления динамики будет невозможно. Это один из редких случаев, когда к динамике неприменим альтернативный способ восстановления — без 数组ов предков).
解法 за
: структуры данных
Если приведённый выше способ за
весьма красив, однако не совсем тривиален идейно, то есть и
другой путь: воспользоваться одной из известных простых структур данных. В самом деле, давайте вернёмся к самой первой динамике, где состоянием являлась просто текущая позиция.
Текущее значение динамики
вычисляется как максимум значений
среди всех таких elementов
,
что
.
Следовательно, если мы через
обозначим такой 数组, в который будем записывать значения динамики от чисел: то получается, что всё, что нам надо уметь — это искать максимум на префиксе 数组а : . 题目 поиска максимума на префиксах 数组а (с учётом того, что 数组 может меняться) решается многими стандартными структурами данных, на示例, 树м отрезков или 树м Фенвика. Воспользовавшись любой такой структурой данных, мы получим 解法 за . У этого способа решения есть явные недостатки: по длине и сложности реализации этот путь будет в любом
случае хуже, чем описанная выше динамика за
. Кроме того, если 输入ные числа
могут
быть достаточно большими, то скорее всего их придётся сжимать (т.е. перенумеровывать от
до
) — без
этого многие стандартные структуры данных работать не смогут из-за высокого потребления памяти. С другой стороны, у данного пути есть и преимущества. Во-первых, при таком способе решения не придётся задумываться о хитрой динамике. Во-вторых, этот способ позволяет решать некоторые обобщения нашей задачи (о них см. ниже).
Смежные задачи
Приведём здесь несколько задач, тесно связанных с задачей поиска наидлиннейшей возрастающей подпоследовательности.
Наидлиннейшая неубывающая subsequence
Фактически, это та же самая 题目, только теперь в искомой подпоследовательности допускаются одинаковые числа (т. е. мы должны find нестрого возрастающую subsequence). 解法 этой задачи по сути ничем не отличается от нашей исходной задачи, просто при сравнениях изменятся знаки неравенств, а также надо будет немного изменить двоичный поиск.
Количество наидлиннейших возрастающих подпоследовательностей
Для решения этой задачи можно использовать самую первую динамику за
либо подход с помощью
структур данных для решения за
. И в том, и в том случае все изменения заключаются только в том,
что помимо значения динамики
надо также хранить, сколькими способами это значение могло быть получено.
По всей видимости, способ решения через динамику за
к данной задаче применить невозможно.
Наименьшее number невозрастающих
подпоследовательностей, покрывающих данную
题意 таково. Дан 数组 из
чисел
. it is required раскрасить его числа в наименьшее
number цветов так, чтобы по каждому цвету получалась бы невозрастающая subsequence. 解法. Утверждается, что минимальное количество необходимых цветов равно длине наидлиннейшей возрастающей подпоследовательности. 证明. Фактически, нам надо доказать двойственность этой задачи и задачи поиска наидлиннейшей возрастающей подпоследовательности. Обозначим через
длину наидлиннейшей
возрастающей подпоследовательности, а через
— искомое наименьшее number
невозрастающих подпоследовательностей. Нам надо доказать, что
.
С одной стороны, понятно, почему не может быть
: ведь если у нас есть
строго возрастающих elementов,
то никакие два из них не могли попасть в одну невозрастающую subsequence, а, значит, .
Покажем теперь, что, наоборот,
не может быть
. Докажем это от противного: предположим, что
.
Тогда рассмотрим любой оптимальный набор из
невозрастающих подпоследовательностей. Преобразуем этот
набор таким образом: пока есть две таких подпоследовательности, что первая начинается раньше второй, но при этом первая начинается с числа, больше либо равного чем начало второй — отцепим это стартовое number от первой подпоследовательности и прицепим в начало второй. Таким образом, через какое-то конечное number шагов у
нас останется
подпоследовательностей, причём их стартовые числа будут образовывать
возрастающую subsequence длины
. Но
, т.е. мы пришли к противоречию (ведь не может
быть возрастающих подпоследовательностей длиннее
).
Таким образом, в самом деле,
, что и требовалось доказать. Восстановление ответа. Утверждается, что само искомое разбиение на подпоследовательности можно искать жадно, т.е. идя слева направо и относя текущее number в ту subsequence, которая сейчас заканчивается на минимальное number, больше либо равное текущему.
C# 解法
自动草稿,提交前请检查using System;
using System.Collections.Generic;
using System.Linq;
public static class AlgorithmDraft
{
// Auto-generated C# draft from the original e-maxx C/C++ listing. Review before production use.
int d[MAXN]; // константа MAXN равна наибольшему возможному значению n
for (int i=0; i<n; ++i) {
d[i] = 1;
for (int j=0; j<i; ++j)
if (a[j] < a[i])
d[i] = max (d[i], 1 + d[j]);
}
int ans = d[0];
for (int i=0; i<n; ++i)
ans = max (ans, d[i]);
Console.WriteLine( ans << endl;
int d[MAXN], p[MAXN]; // константа MAXN равна наибольшему возможному значению n
for (int i=0; i<n; ++i) {
d[i] = 1;
p[i] = -1;
for (int j=0; j<i; ++j)
if (a[j] < a[i])
if (1 + d[j] > d[i]) {
d[i] = 1 + d[j];
p[i] = j;
}
}
int ans = d[0], pos = 0;
for (int i=0; i<n; ++i)
if (d[i] > ans) {
ans = d[i];
pos = i;
}
Console.WriteLine( ans << endl;
List<int> path;
while (pos != -1) {
path.push_back (pos);
pos = p[pos];
}
reverse (path.begin(), path.end());
for (int i=0; i<(int)path.size(); ++i)
Console.WriteLine( path[i] << ' ';
int d[MAXN];
d[0] = -INF;
for (int i=1; i<=n; ++i)
d[i] = INF;
for (int i=0; i<n; i++)
for (int j=1; j<=n; j++)
if (d[j-1] < a[i] && a[i] < d[j])
d[j] = a[i];
int d[MAXN];
d[0] = -INF;
for (int i=1; i<=n; ++i)
d[i] = INF;
for (int i=0; i<n; i++) {
int j = int (upper_bound (d.begin(), d.end(), a[i]) - d.begin());
if (d[j-1] < a[i] && a[i] < d[j])
d[j] = a[i];
}
}
C++ 解法
匹配/原始int d[MAXN]; // константа MAXN равна наибольшему возможному значению n
for (int i=0; i<n; ++i) {
d[i] = 1;
for (int j=0; j<i; ++j)
if (a[j] < a[i])
d[i] = max (d[i], 1 + d[j]);
}
int ans = d[0];
for (int i=0; i<n; ++i)
ans = max (ans, d[i]);
cout << ans << endl;
int d[MAXN], p[MAXN]; // константа MAXN равна наибольшему возможному значению n
for (int i=0; i<n; ++i) {
d[i] = 1;
p[i] = -1;
for (int j=0; j<i; ++j)
if (a[j] < a[i])
if (1 + d[j] > d[i]) {
d[i] = 1 + d[j];
p[i] = j;
}
}
int ans = d[0], pos = 0;
for (int i=0; i<n; ++i)
if (d[i] > ans) {
ans = d[i];
pos = i;
}
cout << ans << endl;
vector<int> path;
while (pos != -1) {
path.push_back (pos);
pos = p[pos];
}
reverse (path.begin(), path.end());
for (int i=0; i<(int)path.size(); ++i)
cout << path[i] << ' ';
int d[MAXN];
d[0] = -INF;
for (int i=1; i<=n; ++i)
d[i] = INF;
for (int i=0; i<n; i++)
for (int j=1; j<=n; j++)
if (d[j-1] < a[i] && a[i] < d[j])
d[j] = a[i];
int d[MAXN];
d[0] = -INF;
for (int i=1; i<=n; ++i)
d[i] = INF;
for (int i=0; i<n; i++) {
int j = int (upper_bound (d.begin(), d.end(), a[i]) - d.begin());
if (d[j-1] < a[i] && a[i] < d[j])
d[j] = a[i];
}
Java 解法
自动草稿,提交前请检查import java.util.*;
import java.math.*;
public class AlgorithmDraft {
// Auto-generated Java draft from the original e-maxx C/C++ listing. Review before production use.
int d[MAXN]; // константа MAXN равна наибольшему возможному значению n
for (int i=0; i<n; ++i) {
d[i] = 1;
for (int j=0; j<i; ++j)
if (a[j] < a[i])
d[i] = max (d[i], 1 + d[j]);
}
int ans = d[0];
for (int i=0; i<n; ++i)
ans = max (ans, d[i]);
System.out.println( ans << endl;
int d[MAXN], p[MAXN]; // константа MAXN равна наибольшему возможному значению n
for (int i=0; i<n; ++i) {
d[i] = 1;
p[i] = -1;
for (int j=0; j<i; ++j)
if (a[j] < a[i])
if (1 + d[j] > d[i]) {
d[i] = 1 + d[j];
p[i] = j;
}
}
int ans = d[0], pos = 0;
for (int i=0; i<n; ++i)
if (d[i] > ans) {
ans = d[i];
pos = i;
}
System.out.println( ans << endl;
ArrayList<Integer> path;
while (pos != -1) {
path.push_back (pos);
pos = p[pos];
}
reverse (path.begin(), path.end());
for (int i=0; i<(int)path.size(); ++i)
System.out.println( path[i] << ' ';
int d[MAXN];
d[0] = -INF;
for (int i=1; i<=n; ++i)
d[i] = INF;
for (int i=0; i<n; i++)
for (int j=1; j<=n; j++)
if (d[j-1] < a[i] && a[i] < d[j])
d[j] = a[i];
int d[MAXN];
d[0] = -INF;
for (int i=1; i<=n; ++i)
d[i] = INF;
for (int i=0; i<n; i++) {
int j = int (upper_bound (d.begin(), d.end(), a[i]) - d.begin());
if (d[j-1] < a[i] && a[i] < d[j])
d[j] = a[i];
}
}
Материал разбит как 算法ическая 题目: изучить постановку, понять асимптотику и реализовать 算法 на выбранном языке.
Vacancies for this task
活跃职位 with overlapping task tags are 已显示.