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

e-maxx algorithm original: C/C++ #algorithm #array #data-structures #emaxx
선택한 UI 언어에 맞게 문제 텍스트를 러시아어에서 번역합니다. 코드는 변경하지 않습니다.

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

문제 설명 задачи следующее. Дан 배열 из

чисел:

. 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, больше либо равное текущему.

Задачи в online judges

Список задач, которые можно решить по данной тематике:

● MCCME #1793 "Наибольшая возрастающая subsequence за O(n*log(n))"

[Complexity: низкая]

● TopCoder SRM 278 "500 IntegerSequence" [Complexity: низкая]

● TopCoder SRM 233 "DIV2 1000 AutoMarket" [Complexity: низкая]

● Всеукраинская олимпиада школьников по информатике — 문제 F "Турист" [Complexity: средняя]

● Codeforces Beta Round #10 — 문제 D "НОВП" [Complexity: средняя]

● ACM.TJU.EDU.CN 2707 "Greatest Common Increasing Subsequence" [Complexity: средняя]

● SPOJ #57 "SUPPER. Supernumbers in a permutation" [Complexity: средняя]

● ACM.SGU.RU #521 "North-East" [Complexity: высокая]

● TopCoder Open 2004 — Round 4 — "1000. BridgeArrangement" [Complexity: высокая]

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 표시됨.

전체 채용
아직 활성 채용이 없습니다.