Longest Increasing Subsequence
Дан список целых чисел nums. Подпоследовательность сохраняет некоторые элементы в исходном порядке, а остальные пропускает; сохранённые элементы не обязаны стоять рядом. Верните длину самой длинной подпоследовательности, значения которой строго возрастают слева направо. Два одинаковых значения подряд не считаются возрастающими.
Функция
- numsinteger-array
- список целых чисел, из которых нужно выбрать
- Возвращаетinteger
- длина наибольшей строго возрастающей подпоследовательности
Ограничения
1 ≤ nums.length ≤ 2500-104 ≤ nums[i] ≤ 104
Примеры
- Ввод
- nums = [3, 1, 8, 2, 5, 9, 4, 7]
- Вывод
- 4
- Пояснение
- Последовательность 1, 2, 5, 9 образует возрастающую подпоследовательность длины 4; то же верно для 1, 2, 5, 7 и 1, 2, 4, 7. Ни один выбор из пяти значений не образует возрастающую последовательность, поэтому ответ — 4.
- Ввод
- nums = [7, 7, 7, 7]
- Вывод
- 1
- Пояснение
- Значения должны строго возрастать, поэтому никакие два числа 7 не могут находиться в одной подпоследовательности. Один элемент сам по себе считается подпоследовательностью, поэтому ответ — 1.
- Ввод
- nums = [12, -4, 0, 25, -10, 3, 16, 5]
- Вывод
- 4
- Пояснение
- -4, 0, 3, 16 имеет длину 4 (у -4, 0, 3, 5 она тоже равна 4). Начиная с первого элемента, 12, можно получить только два значения, например 12, 25: наилучшая подпоследовательность не обязана начинаться с первого элемента.
+20 скрытых тестов при отправке
Дополнительный вопрос
Можешь вернуть саму одну из самых длинных возрастающих подпоследовательностей, а не только её длину, и при этом по-прежнему выполнить алгоритм за O(n log n)?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Лучшую подпоследовательность всего списка сложно описать напрямую. Задай более узкий вопрос для каждого индекса
i: какова самая длинная возрастающая подпоследовательность, которая заканчивается именно наnums[i]?Подпоследовательность, заканчивающаяся на
nums[i], состоит либо только изnums[i], либо продолжает лучшую подпоследовательность, заканчивающуюся на каком-либо более раннемnums[j] < nums[i]. Выбери такоеj, для которого значение максимально, и прибавь один. Ответ — наибольшее из этих значений независимо от того, где заканчивается подпоследовательность.Чтобы получить сложность ниже
O(n²), храните для каждой длины только наименьшее значение, которым может заканчиваться подпоследовательность этой длины. Эти значения остаются отсортированными, поэтому бинарный поиск подскажет, удлиняет ли новое число самую длинную подпоследовательность или заменяет последнее значение.
Решение
Подпоследовательность может пропускать любые элементы, поэтому для списка из n чисел существует 2^n таких подпоследовательностей — слишком много, чтобы проверить их все. Решение с помощью динамического программирования состоит в том, чтобы задать для каждого индекса более узкий вопрос: какова длина наилучшей возрастающей подпоследовательности, которая заканчивается именно здесь? Это даёт таблицу размера O(n²). Самый быстрый вариант хранит по одному числу для каждой длины — наименьшее значение, которым может заканчиваться подпоследовательность такой длины, — и размещает каждый новый элемент с помощью бинарного поиска.
Взять или пропустить каждый элемент
Верно, но не успевает на самых больших тестах
Идея
Пройдитесь по списку и примите по одному решению для каждого элемента: оставить его или пропустить. Вы можете оставить nums[i], только если он больше последнего оставленного значения. Рекурсивная функция longest(i, prev) отвечает на вопрос: если последний оставленный элемент находится по индексу prev (или -1, если пока ничего не оставлено), сколько ещё элементов можно добавить, начиная с индекса i?
Если пропустить элемент, получится longest(i+1, prev). Если оставить его, когда это разрешено, получится 1 + longest(i+1, i). Ответом будет большее из этих двух значений, а за концом списка уже ничего нельзя добавить, поэтому результат там равен 0. Каждая возрастающая подпоследовательность представляет собой один путь из решений оставить или пропустить, поэтому поиск не может упустить наилучшую последовательность.
Этот способ работает медленно, потому что обе ветви продолжаются, когда значения возрастают. Для списка вида 1, 2, 3, ..., n число вызовов удваивается с каждым элементом: 2 в степени 40 — это уже примерно 10^12 вызовов, а в больших тестах 2500 элементов. Однако longest(i, prev) зависит только от пары (i, prev), поэтому существует не более n² разных вопросов. Следующий подход — задать каждый из них только один раз.
Алгоритм
- Напиши
longest(i, prev), гдеprev— индекс последнего оставленного элемента или-1. - Если
iвыходит за конец, верни 0. - Пропусти
nums[i]:best = longest(i+1, prev). - Если
prevравен-1илиnums[i] > nums[prev], оставь его:best = max(best, 1 + longest(i+1, i)). - Верни
best. Ответ —longest(0, -1).
def lengthOfLIS(nums):
def longest(i, prev):
# The longest increasing subsequence of nums[i:] whose values all exceed nums[prev].
# prev is -1 while nothing has been taken.
if i == len(nums):
return 0
best = longest(i + 1, prev) # skip nums[i]
if prev == -1 or nums[i] > nums[prev]:
best = max(best, 1 + longest(i + 1, i)) # take nums[i]
return best
return longest(0, -1)Самая длинная подпоследовательность, заканчивающаяся на каждом индексе
Идея
Состояние. Пусть ending[i] — длина наибольшей возрастающей подпоследовательности, последний элемент которой — nums[i]. Фиксация последнего элемента позволяет легко разделить задачу: если известно, где заканчивается подпоследовательность, понятно, какие последующие значения могут идти после неё.
Рекуррентное соотношение. Если подпоследовательность, заканчивающаяся на nums[i], содержит больше одного элемента, то перед nums[i] стоит некоторый nums[j], где j < i и nums[j] < nums[i], а подпоследовательность до него должна быть как можно длиннее. Поэтому ending[i] = 1 + max(ending[j]) для таких j. Базовый случай: каждый элемент сам по себе является подпоследовательностью, поэтому начальное значение ending[i] равно 1. Порядок: для вычисления ending[i] используются только меньшие индексы, поэтому заполняй таблицу слева направо.
Для [3, 1, 8, 2, 5, 9, 4, 7] таблица имеет вид [1, 1, 2, 2, 3, 4, 3, 4]. Например, перед 5 могут идти 3, 1 или 2, и лучший из них — 2, для которого ending = 2, поэтому ending[4] = 3. Ответ — наибольшее значение, 4, а не последнее: лучшая подпоследовательность может заканчиваться где угодно.
Для каждого индекса проверяется каждый предшествующий индекс, поэтому выполняется n(n-1)/2 сравнений — около 3.1 × 10^6 при n = 2500.
Алгоритм
- Создай
ending, задав для каждой записи значение 1. - Для каждого
iслева направо просмотри всеj < i. - Если
nums[j] < nums[i], присвойending[i]значениеending[j] + 1, если оно больше. - Верни наибольшее значение в
ending.
def lengthOfLIS(nums):
n = len(nums)
# ending[i]: the longest increasing subsequence that ends with nums[i]
ending = [1] * n
for i in range(1, n):
for j in range(i):
if nums[j] < nums[i] and ending[j] + 1 > ending[i]:
ending[i] = ending[j] + 1
return max(ending)Наименьшие хвосты с бинарным поиском
Идея
Таблица выше хранит по одному значению длины для каждого индекса. Можно хранить меньше: для каждой длины — только наименьшее значение, которым может заканчиваться возрастающая подпоследовательность этой длины. Обозначим его tails[k] для длины k+1. Меньшее конечное значение всегда не хуже, потому что любое значение, которое может следовать за подпоследовательностью, заканчивающейся на 9, также может следовать за подпоследовательностью, заканчивающейся на 5.
tails всегда отсортирован по возрастанию: подпоследовательность длины k+2, заканчивающаяся на t, содержит подпоследовательность длины k+1, заканчивающуюся значением меньше t. Поэтому для каждого нового значения x выполни двоичный поиск первого хвостового значения, которое ≥ x. Если такого значения нет, x больше любого хвостового значения и увеличивает длину самой длинной подпоследовательности, поэтому добавь его. В противном случае замени это хвостовое значение на x: подпоследовательность на единицу короче заканчивается значением меньше x, поэтому добавление x даёт ту же длину с меньшим конечным значением.
Для [3, 1, 8, 2, 5, 9, 4, 7] массив tails принимает значения [3], [1], [1, 8], [1, 2], [1, 2, 5], [1, 2, 5, 9], [1, 2, 4, 9], [1, 2, 4, 7], а его длина 4 — это ответ. На шаге [1, 2, 4, 9] число 4 идёт во входных данных после числа 9, поэтому tails сам по себе не является подпоследовательностью; значение имеет только его длина. Этот метод также называют терпеливой сортировкой — по карточной игре, где каждое хвостовое значение является верхней картой в стопке.
Для каждого элемента выполняется один двоичный поиск среди не более чем n хвостовых значений: около 2500 × 12 = 30,000 шагов для входных данных максимального размера.
Алгоритм
- Начни с пустого списка
tails. - Для каждого
xвnumsвыполни бинарный поиск первого индексаk, для которогоtails[k] ≥ x. - Если нет элемента, который был бы
≥ x, добавьxв конец списка. - Иначе присвой
tails[k] = x. - Верни длину
tails.
from bisect import bisect_left
def lengthOfLIS(nums):
# tails[k]: the smallest last value of any increasing subsequence of length k + 1
tails = []
for x in nums:
k = bisect_left(tails, x) # the first tail that is >= x
if k == len(tails):
tails.append(x) # x extends the longest subsequence so far
else:
tails[k] = x # x is a smaller ending for length k + 1
return len(tails)
Ловушки и крайние случаи
Большинство неправильных ответов возникает из-за путаницы в том, что хранится в таблице, или из-за того, что равные значения считаются возрастающими.
- Возвращение
ending[n-1]вместо наибольшего элемента. Для[1, 2, 3, 0]последний элемент равен 1, а ответ — 3. - Сравнение с
≤вместо<. Для[7, 7, 7, 7]результат должен быть 1, а не 4. - В варианте с tails поиск первого хвоста
> xвместо≥ x. При наличии дубликатов это добавляет вторую 7 после первой и считает равные значения более длинной подпоследовательностью. - Рассмотрение
tailsкак самой подпоследовательности. Её значения могут принадлежать разным подпоследовательностям, поэтому выводите её, только если отдельно отслеживаете родителей. - Случайное решение задачи о непрерывном отрезке. В
[3, 1, 8, 2, 5, 9, 4, 7]самая длинная возрастающая последовательность соседних элементов — 2, 5, 9 (длина 3), а ответ — 4. - В Lua и R массивы начинаются с 1, поэтому маркер
prev = -1для индексации с 0 становится равен 0, а бинарный поиск выполняется по индексам от 1 до текущего размера.
Частые вопросы4
Какова временная сложность задачи о наибольшей возрастающей подпоследовательности?
Метод tails работает за время O(n log n) и требует O(n) памяти: один бинарный поиск на каждый элемент. Таблица динамического программирования для каждой пары индексов требует времени O(n²), а перебор каждой подпоследовательности — O(2ⁿ). Для n = 2500 это примерно 30 000, 3 миллиона и астрономическое число шагов.
Почему метод сортировки терпением дает правильную длину?
После каждого элемента в tails[k] хранится наименьшее значение, которым может заканчиваться любая возрастающая подпоследовательность длины k+1, найденная к этому моменту. Добавление происходит только тогда, когда x больше каждого из хвостов, а значит, теперь существует подпоследовательность на один элемент длиннее любой из найденных ранее. Замена никогда не меняет длину, она лишь уменьшает конечное значение, поэтому длина списка всегда равна длине наибольшей возрастающей подпоследовательности.
Как получить самую длинную возрастающую подпоследовательность, а не только её длину?
Запишите родителя для каждого элемента. В таблице O(n²) родитель элемента i — это j, который присвоил значение ending[i]. В методе с хвостами храните индекс элемента, стоящего за каждым хвостом, и при размещении элемента задавайте ему в качестве родителя индекс, сохранённый на одну позицию левее. Затем пройдите по родителям от конца самой длинной подпоследовательности и разверните результат.
Как вместо этого найти самую длинную неубывающую подпоследовательность?
Разрешите равных соседей. В таблице используйте nums[j] ≤ nums[i]. В методе хвостов ищите первый хвост, который строго больше x, а не больше или равен ему, чтобы равное значение расширяло список, а не заменяло хвост. Тогда [7, 7, 7, 7] вернёт 4.
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
Python
def lengthOfLIS(nums):
# Напишите код здесьСлучай 1
Случай 2
Случай 3
Ввод
nums = [3, 1, 8, 2, 5, 9, 4, 7]
Ожидается
4