E122. Нахождение наидлиннейшей возрастающей подпоследовательности

e-maxx algorithm original: C/C++ #algorithm #array #emaxx #sequence
El texto de la tarea se traduce del ruso para el idioma seleccionado. El código no cambia.

Источник: e-maxx.ru/algo, страница PDF 401.

Enunciado задачи следующее. Дан arreglo из

чисел:

. it is required find в

этой последовательности строго возрастающую subsequence наибольшей длины. Формально это выглядит следующим образом: it is required find такую последовательность индексов , что:

В данной статье рассматриваются различные Algoritmoы решения данной задачи, а также некоторые задачи, которые можно свести к данной задаче.

Solución за

: метод динамического программирования

dynamic programming — это весьма общая методика, позволяющая решать огромный класс задач. Здесь мы рассмотрим эту методику применительно к нашей конкретной задаче. Научимся сначала искать длину наидлиннейшей возрастающей подпоследовательности, а восстановлением самой подпоследовательности займёмся чуть позже.

dynamic programming для поиска длины ответа

Для этого давайте научимся считать arreglo

, где

— это длина наидлиннейшей

возрастающей подпоследовательности, оканчивающейся именно в elementе с индексом

. arreglo этот (он и есть —

сама динамика) будем считать постепенно: сначала

, затем

и т.д. В конце, когда этот arreglo будет

подсчитан нами, ответ на задачу будет равен максимуму в arregloе

.

Итак, пусть текущий индекс —

, т.е. мы хотим посчитать значение

, а все предыдущие значения

уже подсчитаны. Тогда заметим, что у нас есть два варианта:

● либо

, т.е. искомая subsequence состоит только из числа

.

● либо

. Тогда перед numberм

в искомой подпоследовательности стоит какое-то другое number.

Давайте переберём это number: это может быть любой element

, но такой, что

. Пусть мы рассматриваем какой-то текущий индекс

. Поскольку динамика

для него уже

подсчитана, получается, что это number

вместе с numberм

даёт ответ

. Таким образом,

можно считать по такой формуле: Объединяя эти два варианта в один, получаем окончательный Algoritmo для вычисления : Этот Algoritmo — и есть сама динамика.

Implementación

Приведём реализацию описанного выше Algoritmoа, которая находит и выводит длину наидлиннейшей возрастающей подпоследовательности:

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 мы вывести не можем, т. к. не сохраняем никакой дополнительной информации о том, где достигаются максимумы.

Чтобы суметь восстановить ответ, помимо динамики

надо также хранить вспомогательный

arreglo

— то, в каком месте достигся максимум для каждого значения

. Иными словами, индекс

будет обозначать тот самый индекс

, при котором получилось наибольшее значение

. (Этот arreglo

в динамическом программировании часто называют "arregloом предков".) Тогда, чтобы вывести ответ, надо просто идти от elementа с максимальным значением

по его предкам до тех

пор, пока мы не выведем всю subsequence, т.е. пока не дойдём до elementа со значением .

Implementación восстановления ответа

Итак, у нас изменится и код самой динамики, и добавится код, производящий вывод наидлиннейшей подпоследовательности (выводятся индексы 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] << ' ';

Альтернативный способ восстановления ответа

Впрочем, как почти всегда в случае динамического программирования, для восстановления ответа можно не

хранить дополнительный arreglo предков

, а просто заново пересчитывая текущий element динамики и ища, на каком же индексе был достигнут максимум. Этот способ при реализации приводит к чуть более длинному коду, однако взамен получаем экономию памяти и абсолютное совпадение логики программы в процессе подсчёта динамики и в процессе восстановления.

Solución за

: dynamic programming

с двоичным поиском

Чтобы получить более быстрое Solución задачи, построим другой вариант динамического программирования за

,

а затем поймём, как можно этот вариант ускорить до

.

Динамика теперь будет такой: пусть

— это 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

обновляет максимум одну ячейку

.

Таким образом, это означает, что обрабатывать очередное

мы можем за

, сделав двоичный поиск

по arregloу

. В самом деле, мы просто ищем в arregloе

первое number, которое строго больше

, и

пытаемся произвести обновление этого elementа аналогично приведённой выше реализации.

Implementación за

Воспользовавшись стандартным в языке C++ Algoritmoом двоичного поиска

(который

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];

}

Восстановление ответа

По такой динамике тоже можно восстановить ответ, для чего опять же помимо динамики

также надо хранить

arreglo "предков"

— то, на elementе с каким индексом оканчивается оптимальная subsequence длины

. Кроме того, для каждого elementа arregloа

надо будет хранить его "предка" — т.е. индекс того elementа,

который должен стоять перед

в оптимальной подпоследовательности. Поддерживая эти два arregloа по ходу вычисления динамики, в конце будет нетрудно восстановить искомую subsequence. (Интересно отметить, что применительно к данной динамике ответ можно восстанавливать только так, через arregloы предков — а без них восстановить ответ после вычисления динамики будет невозможно. Это один из редких случаев, когда к динамике неприменим альтернативный способ восстановления — без arregloов предков).

Solución за

: структуры данных

Если приведённый выше способ за

весьма красив, однако не совсем тривиален идейно, то есть и

другой путь: воспользоваться одной из известных простых структур данных. В самом деле, давайте вернёмся к самой первой динамике, где состоянием являлась просто текущая позиция.

Текущее значение динамики

вычисляется как максимум значений

среди всех таких elementов

,

что

.

Следовательно, если мы через

обозначим такой arreglo, в который будем записывать значения динамики от чисел: то получается, что всё, что нам надо уметь — это искать максимум на префиксе arregloа : . Tarea поиска максимума на префиксах arregloа (с учётом того, что arreglo может меняться) решается многими стандартными структурами данных, наEjemplo, árbolм отрезков или árbolм Фенвика. Воспользовавшись любой такой структурой данных, мы получим Solución за . У этого способа решения есть явные недостатки: по длине и сложности реализации этот путь будет в любом

случае хуже, чем описанная выше динамика за

. Кроме того, если Entradaные числа

могут

быть достаточно большими, то скорее всего их придётся сжимать (т.е. перенумеровывать от

до

) — без

этого многие стандартные структуры данных работать не смогут из-за высокого потребления памяти. С другой стороны, у данного пути есть и преимущества. Во-первых, при таком способе решения не придётся задумываться о хитрой динамике. Во-вторых, этот способ позволяет решать некоторые обобщения нашей задачи (о них см. ниже).

Смежные задачи

Приведём здесь несколько задач, тесно связанных с задачей поиска наидлиннейшей возрастающей подпоследовательности.

Наидлиннейшая неубывающая subsequence

Фактически, это та же самая Tarea, только теперь в искомой подпоследовательности допускаются одинаковые числа (т. е. мы должны find нестрого возрастающую subsequence). Solución этой задачи по сути ничем не отличается от нашей исходной задачи, просто при сравнениях изменятся знаки неравенств, а также надо будет немного изменить двоичный поиск.

Количество наидлиннейших возрастающих подпоследовательностей

Для решения этой задачи можно использовать самую первую динамику за

либо подход с помощью

структур данных для решения за

. И в том, и в том случае все изменения заключаются только в том,

что помимо значения динамики

надо также хранить, сколькими способами это значение могло быть получено.

По всей видимости, способ решения через динамику за

к данной задаче применить невозможно.

Наименьшее number невозрастающих

подпоследовательностей, покрывающих данную

Enunciado таково. Дан arreglo из

чисел

. it is required раскрасить его числа в наименьшее

number цветов так, чтобы по каждому цвету получалась бы невозрастающая subsequence. Solución. Утверждается, что минимальное количество необходимых цветов равно длине наидлиннейшей возрастающей подпоследовательности. Prueba. Фактически, нам надо доказать двойственность этой задачи и задачи поиска наидлиннейшей возрастающей подпоследовательности. Обозначим через

длину наидлиннейшей

возрастающей подпоследовательности, а через

— искомое наименьшее number

невозрастающих подпоследовательностей. Нам надо доказать, что

.

С одной стороны, понятно, почему не может быть

: ведь если у нас есть

строго возрастающих elementов,

то никакие два из них не могли попасть в одну невозрастающую subsequence, а, значит, .

Покажем теперь, что, наоборот,

не может быть

. Докажем это от противного: предположим, что

.

Тогда рассмотрим любой оптимальный набор из

невозрастающих подпоследовательностей. Преобразуем этот

набор таким образом: пока есть две таких подпоследовательности, что первая начинается раньше второй, но при этом первая начинается с числа, больше либо равного чем начало второй — отцепим это стартовое number от первой подпоследовательности и прицепим в начало второй. Таким образом, через какое-то конечное number шагов у

нас останется

подпоследовательностей, причём их стартовые числа будут образовывать

возрастающую subsequence длины

. Но

, т.е. мы пришли к противоречию (ведь не может

быть возрастающих подпоследовательностей длиннее

).

Таким образом, в самом деле,

, что и требовалось доказать. Восстановление ответа. Утверждается, что само искомое разбиение на подпоследовательности можно искать жадно, т.е. идя слева направо и относя текущее number в ту subsequence, которая сейчас заканчивается на минимальное number, больше либо равное текущему.

C# solución

borrador automático, revisar antes de enviar
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++ solución

coincidente/original
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 solución

borrador automático, revisar antes de enviar
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];
    }
}

Материал разбит как Algoritmoическая Tarea: изучить постановку, понять асимптотику и реализовать Algoritmo на выбранном языке.

Vacantes para esta tarea

Se muestran vacantes activas con etiquetas coincidentes.

Todas las vacantes
Todavía no hay vacantes activas.