В коде есть несколько ответов, но я нашел их немного сложными для понимания, поэтому здесь приводится объяснение общей идеи, оставляя без внимания все оптимизации.Позже я вернусь к оптимизации.
Мы будем использовать последовательность 2, 8, 4, 12, 3, 10, и, чтобы было легче следовать, нам потребуется, чтобы входная последовательность не была пустой ине включать один и тот же номер более одного раза.
Мы проходим последовательность по порядку.
При этом мы поддерживаем набор последовательностей, лучшие последовательности, которые мы до сих пор нашли для каждой длины.После того, как мы находим первую последовательность длины 1, которая является первым элементом входной последовательности, мы гарантированно получим набор последовательностей для каждой возможной длины от 1 до самой длинной, которую мы до сих пор нашли.Это очевидно, потому что если у нас есть последовательность длиной 3, то первые 2 элемента этой последовательности являются последовательностью длины 2.
Итак, мы начинаем с того, что первый элемент представляет собой последовательность длины 1, и нашset выглядит как
1: 2
Мы берем следующий элемент последовательности (8) и ищем самую длинную последовательность, к которой мы можем добавить его.Это последовательность 1, поэтому мы получаем
1: 2
2: 2 8
. Мы берем следующий элемент последовательности (4) и ищем самую длинную последовательность, к которой мы можем добавить его.Самая длинная последовательность, к которой мы можем добавить это длина 1 (которая просто 2
). Вот то, что я нашел для хитрой (или, по крайней мере, неочевидной) части. Поскольку мы не смогли добавить ее в конец последовательности длины 2 (2 8
), что означает это должен быть лучший выбор, чтобы закончить длину 2 кандидата .Если бы элемент был больше 8, он бы привязался к последовательности длины 2 и дал бы нам новую последовательность длины 3.Итак, мы знаем, что оно меньше 8, и поэтому заменим 8 на 4.
С точки зрения алгоритма, мы говорим, что независимо от того, какую самую длинную последовательность мы можем привязать к элементу, эта последовательность плюс этот элемент являетсялучший кандидат на последовательность результирующей длины. Обратите внимание, что каждый элемент, который мы обрабатываем, должен где-то принадлежать (потому что мы исключили повторяющиеся числа во входных данных).Если он меньше элемента длины 1, это новая длина 1, в противном случае он идет в конце некоторой существующей последовательности. Здесь последовательность длины 1 плюс элемент 4 становится новой последовательностью длины 2, и мыимеем:
1: 2
2: 2 4 (replaces 2 8)
Следующий элемент 12 дает нам последовательность длины 3, и мы имеем
1: 2
2: 2 4
3: 2 4 12
Следующий элемент 3 дает нам лучшую последовательность длины 2:
1: 2
2: 2 3 (replaces 2 4)
3: 2 4 12
Обратите внимание, что мы не можем изменить последовательность длины 3 (подставляя 3 вместо 4), потому что они не встречались в указанном порядке во входной последовательности.Следующий элемент, 10, заботится об этом.Поскольку лучшее, что мы можем сделать с 10, это добавить его к 2 3
, он становится новым списком длины 3:
1: 2
2: 2 3
3: 2 3 10 (replaces 2 4 12)
Обратите внимание, что с точки зрения алгоритма нам действительно все равно, что получитсяперед последним элементом в любой из наших последовательностей-кандидатов, но, конечно, мы должны отслеживать, чтобы в конце мы могли вывести полную последовательность.
Мы продолжаем обрабатывать входные элементы так: просто прикрепите каждый из них к самой длинной последовательности, которую мы можем, и сделайте эту новую последовательность-кандидат для полученной длины, потому что она гарантированно не будет хуже существующей последовательности этогодлина.В конце мы выводим самую длинную последовательность, которую мы нашли.
Оптимизации
Одна оптимизация заключается в том, что нам не нужно хранить всю последовательность каждой длины.Для этого потребовалось бы место O (n ^ 2).По большей части, мы можем просто сохранить последний элемент каждой последовательности, так как это все, с чем мы когда-либо сравнивали.(Я немного позже пойму, почему этого недостаточно). Посмотрим, сможете ли вы выяснить, почему, прежде чем я это сделаю.)
Допустим, мы будем хранить наш набор последовательностей в виде массива M
, где M[x]
содержит последний элемент последовательности длиной x
. Если вы подумаете об этом, вы поймете, что элементы M
сами в порядке возрастания: они отсортированы. Если бы M[x+1]
было меньше M[x]
, оно заменило бы M[x]
.
Поскольку M
отсортировано, следующая оптимизация идет к чему-то, что я полностью затмил выше: как нам найти последовательность, к которой можно добавить? Итак, поскольку M
отсортировано, мы можем просто выполнить бинарный поиск, чтобы найти самое большое M[x]
меньше, чем добавляемый элемент. Это последовательность, к которой мы добавляем.
Это здорово, если все, что мы хотим сделать, это найти длину самой длинной последовательности. Однако M
недостаточно для восстановления самой последовательности. Помните, однажды наш сет выглядел так:
1: 0
2: 0 2
3: 0 4 12
Мы не можем просто вывести M
в виде последовательности. Нам нужно больше информации, чтобы иметь возможность восстановить последовательность. Для этого мы делаем еще 2 изменения . Сначала , мы сохраняем входную последовательность в массиве seq
и вместо того, чтобы хранить значение элемента в M[x]
, мы сохраняем индекс элемента в seq
, поэтому значение равно seq[M[x]]
.
Мы делаем это так, чтобы мы могли вести запись всей последовательности, связывая подпоследовательности. Как вы видели в начале, каждая последовательность создается путем добавления одного элемента в конец уже существующей последовательности. Итак, секунда , мы сохраняем другой массив P
, в котором хранится индекс (в seq
) последнего элемента последовательности, к которой мы добавляем. Чтобы сделать его цепочечным, поскольку то, что мы храним в P
, является индексом seq
, мы должны сами индексировать P
по индексу seq
.
Это работает так, что при обработке элемента i
из seq
мы находим, к какой последовательности мы добавляем. Помните, мы собираемся прикрепить seq[i]
к последовательности длиной x
, чтобы создать новую последовательность длиной x+1
для некоторого x
, и мы храним i
, а не seq[i]
в M[x+1]
, Позже, когда мы обнаружим, что x+1
является самой большой возможной длиной, мы собираемся восстановить последовательность, но единственная отправная точка, которую мы имеем, это M[x+1]
.
Мы устанавливаем M[x+1] = i
и P[i] = M[x]
(что идентично P[M[x+1]] = M[x]
), то есть для каждого добавляемого нами элемента i
мы сохраняем i
как последний элемент в самая длинная цепочка, которую мы можем, и мы храним индекс последнего элемента цепочки, который мы расширяем, в P[i]
. Итак имеем:
last element: seq[M[x]]
before that: seq[P[M[x]]]
before that: seq[P[P[M[x]]]]
etc...
И теперь мы закончили. Если вы хотите сравнить это с реальным кодом, вы можете посмотреть другие примеры . Основное отличие состоит в том, что они используют j
вместо x
, могут хранить список длины j
в M[j-1]
вместо M[j]
, чтобы не тратить пространство на M[0]
, и могут вызывать входную последовательность X
вместо seq
.