Longest Increasing Path in a Matrix
Дан matrix — сетка целых чисел с m строками и n столбцами, представленная в виде списка строк. Путь проходит вверх, вниз, влево или вправо (по диагонали ходить нельзя, переходить через края нельзя), и на каждом шаге значение в следующей ячейке должно быть строго больше. Верните количество ячеек в самом длинном таком пути. Путь, состоящий из одной ячейки, имеет длину 1.
Функция
- matrixinteger-2d-array
- сетка значений в виде списка строк одинаковой длины
- Возвращаетinteger
- количество ячеек в самом длинном строго возрастающем пути
Ограничения
1 ≤ m, n ≤ 100, гдеm = matrix.lengthиn = matrix[i].length- Каждая строка имеет одинаковую длину
n. 0 ≤ matrix[i][j] ≤ 231-1
Примеры
- Ввод
- matrix = [[9, 8, 3], [2, 7, 4], [1, 6, 5]]
- Вывод
- 7
- Пояснение
- Путь 3, 4, 5, 6, 7, 8, 9 проходит вниз по правому столбцу, вдоль нижней строки и вверх по среднему столбцу до 9: 7 ячеек. От наименьшего значения, 1, лучшие пути содержат 5 ячеек.
- Ввод
- matrix = [[2, 2, 2], [2, 5, 2]]
- Вывод
- 2
- Пояснение
- Равные значения не являются шагом вверх, поэтому ни один путь не проходит по 2. Лучший вариант — шаг от одной из трёх двоек вокруг 5 к 5: 2 клетки.
- Ввод
- matrix = [[4, 4], [4, 4], [4, 4]]
- Вывод
- 1
- Пояснение
- Каждое значение равно 4, поэтому ни один шаг не разрешен. Каждая ячейка сама по себе представляет собой путь из 1 ячейки, и 1 — это ответ.
Дополнительный вопрос
Можешь также вернуть ячейки одного из самых длинных путей вместе с его длиной?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Может ли путь когда-нибудь вернуться в ячейку, которую он уже посетил? Следите за тем, как меняются значения на пути.
Значения только увеличиваются, поэтому путь никогда не проходит через одну и ту же ячейку дважды, а длина самого длинного пути, начинающегося в ячейке, не зависит от того, как вы туда попали. Она равна 1 плюс длина самого длинного пути из наилучшего из её соседей с большими значениями.
Вычислите это число один раз для каждой ячейки и сохраните его. Либо заполните его с помощью поиска в глубину по более крупным соседям, управляя собственным стеком, либо снимайте слои с сетки, начиная с её пиков, и подсчитывайте слои.
Решение
Нарисуй стрелку от каждой ячейки к каждому большему соседу. Значения возрастают вдоль каждой стрелки, поэтому цепочка стрелок не может образовать цикл: сетка представляет собой ориентированный ациклический граф, а ответ — это его самый длинный путь. При отсутствии циклов самый длинный путь из ячейки зависит только от этой ячейки, поэтому вычисли его один раз для каждой ячейки: O(m × n).
Следуй по каждому возрастающему пути
Верно, но не успевает на самых больших тестах
Идея
Начните обход в каждой ячейке и следуйте по каждой ветви к соседним ячейкам с большими значениями, пока такие ячейки не закончатся; самый длинный обход и будет ответом. На каждом шаге значения увеличиваются, поэтому обход никогда не возвращается в ячейку, и множество посещённых ячеек не нужно.
Обход из ячейки повторяется каждый раз, когда через неё проходит другой обход. На сетке 100 × 100, где значение каждой ячейки равно сумме её строки и столбца, каждый шаг вправо или вниз ведёт к большему значению, и из верхнего левого угла начинаются более чем 10^58 обходов.
Алгоритм
- Для каждой ячейки поместите в стек пару (эта ячейка, 1).
- Извлеките запись (ячейка, длина) и обновите ответ, используя длину.
- Поместите в стек пару (соседняя ячейка, длина + 1) для каждого соседа внутри сетки, значение которого строго больше.
- Повторяйте, пока стек не опустеет, затем переходите к следующей начальной ячейке.
- Верните наибольшую встреченную длину.
def longestIncreasingPath(matrix):
rows, cols = len(matrix), len(matrix[0])
dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))
answer = 0
for sr in range(rows):
for sc in range(cols):
# Each entry is one path in progress: the cell it ends on and
# how many cells it has.
stack = [(sr, sc, 1)]
while stack:
r, c, length = stack.pop()
answer = max(answer, length)
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c]:
stack.append((nr, nc, length + 1))
return answerПоиск в глубину с мемоизацией и собственным стеком
Идея
Пусть best[cell] — количество ячеек в самом длинном возрастающем пути, который начинается в этой ячейке: 1 + max(best[nb]) по большим соседям nb или 1, если таких нет. Сохранять это значение безопасно, потому что ячейки перед cell на пути меньше неё и никогда не идут после неё. В первом примере для 9 получается 1, для 8 — 2, для 7 — 3, для 6 и 2 — 4, для 5 и 1 — 5, для 4 — 6, а для 3 — 7.
Путь может проходить через все 10,000 ячеек — это слишком глубоко для рекурсии, поэтому используй собственный стек. Верхняя ячейка пробует следующее направление и помещает в стек большего соседа, для которого вычисления ещё не завершены. Когда проверены все четыре направления, извлеки её из стека и задай ей значение best. Пометка «в процессе» не нужна: каждая ячейка в стеке больше ячейки под ней.
Алгоритм
- Заполните
bestзначением 0 (пока неизвестно), а счётчик направлений для каждой ячейки — значением 0. - Для каждой ячейки, у которой
bestравен 0, поместите её в стек. - Для верхней ячейки увеличьте её счётчик и поместите в стек её соседа, если он находится внутри сетки, больше неё и ещё не обработан.
- После четырёх направлений извлеките ячейку из стека и установите
bestравным 1 плюс наибольшее значениеbestсреди её больших соседей или 1. - Верните наибольшее значение
best.
def longestIncreasingPath(matrix):
rows, cols = len(matrix), len(matrix[0])
dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))
# best[r][c]: cells on the longest increasing path that starts at (r, c).
# 0 means not known yet.
best = [[0] * cols for _ in range(rows)]
# step[r][c]: how many of the 4 directions the search has tried from (r, c).
step = [[0] * cols for _ in range(rows)]
answer = 0
for sr in range(rows):
for sc in range(cols):
if best[sr][sc]:
continue
# Our own stack instead of recursion: a path can be thousands of
# cells long, past Python's limit of 1,000 nested calls.
stack = [(sr, sc)]
while stack:
r, c = stack[-1]
d = step[r][c]
if d < 4:
step[r][c] = d + 1
nr, nc = r + dirs[d][0], c + dirs[d][1]
# The stack only climbs, so a larger neighbour is never on it.
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c] and best[nr][nc] == 0:
stack.append((nr, nc))
continue
# Every larger neighbour is finished: build on the best of them.
stack.pop()
length = 1
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c]:
length = max(length, best[nr][nc] + 1)
best[r][c] = length
answer = max(answer, length)
return answerСнимите сетку с её пиков
Идея
Постройте ту же таблицу сверху вниз, начиная с наибольших значений, как при топологической сортировке Кана. Пик — ячейка, у которой нет соседей с бо́льшим значением, — начинает путь из 1 ячейки: слой 1. Удалите все пики; ячейки, потерявшие последнего соседа с бо́льшим значением, образуют следующий слой. Повторяйте, пока сетка не опустеет. Ячейка попадает в слой через один раунд после того, как удалён её последний сосед с бо́льшим значением, поэтому её слой равен best, а количество слоёв и есть ответ. В первом примере слои такие: 9; 8; 7; 2 и 6; 1 и 5; 4; 3.
Для каждой ячейки подсчитывайте, сколько соседей с бо́льшим значением осталось. Удаление ячейки уменьшает счётчик каждого строго меньшего соседа, а счётчик, равный 0, означает, что ячейка попадает в следующий слой. Каждая ячейка удаляется один раз: O(m × n), без стека.
Алгоритм
- Для каждой ячейки посчитай соседей со значением больше.
- Помести в текущий слой все ячейки, счёт которых равен 0.
- Пока слой не пуст, увеличивай счётчик слоёв на 1. Для каждой ячейки в нём уменьши счётчик каждого строго меньшего соседа и помести соседа, счёт которого достиг 0, в следующий слой.
- Сделай следующий слой текущим и повтори.
- Верни счётчик слоёв.
def longestIncreasingPath(matrix):
rows, cols = len(matrix), len(matrix[0])
dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))
# higher[r][c]: neighbours of (r, c) with a larger value, not peeled yet.
higher = [[0] * cols for _ in range(rows)]
layer = []
for r in range(rows):
for c in range(cols):
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c]:
higher[r][c] += 1
# A peak: no neighbour is larger, so a path from it has one cell.
if higher[r][c] == 0:
layer.append((r, c))
layers = 0
while layer:
layers += 1
next_layer = []
for r, c in layer:
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] < matrix[r][c]:
higher[nr][nc] -= 1
# Its last larger neighbour is peeled: it is a peak now.
if higher[nr][nc] == 0:
next_layer.append((nr, nc))
layer = next_layer
# Each layer is one step further from a peak; the count is the longest path.
return layers
Ловушки и крайние случаи
- Сравнение с
>=вместо>. При наличии двух соседних четвёрок алгоритм полного перебора бесконечно ходит туда-сюда, а мемоизированный поиск считывает длину, которая ещё вычисляется. - Рекурсия в Python. Значения, проходящие змеёй по сетке 100 × 100, образуют путь из 10 000 ячеек — в десять раз больше стандартного ограничения в 1 000 вызовов.
- Пропускать обработанные ячейки, как при заливке. Их сохранённая длина — именно то, что нужно текущей ячейке.
- Начинать только с наименьшего значения. В первом примере 1 даёт 5 ячеек, но ответ — 7, и путь начинается с 3.
- Начинать длины с 0. Для сетки с одинаковыми значениями ответ равен 1.
Частые вопросы3
Какова временная сложность задачи «Наидлиннейший возрастающий путь в матрице»?
Время и память O(m × n), при поиске в глубину с мемоизацией или при послойном удалении: каждая ячейка обрабатывается один раз и проверяет 4 соседние ячейки. Перебор каждого пути из каждой ячейки имеет экспоненциальную сложность.
«Наибольший возрастающий путь в матрице» — это задача на динамическое программирование или задача на графы?
И то и другое. Это самый длинный путь в ориентированном ациклическом графе, который решается с помощью динамического программирования по топологическому порядку. Сортировка ячеек от наибольшего значения к наименьшему задаёт другой допустимый порядок, но требует O(m × n × log(m × n)) операций на сортировку.
Чем это отличается от наибольшей возрастающей подпоследовательности?
Подпоследовательность может пропускать элементы, но сохраняет их порядок; здесь путь должен переходить в соседнюю клетку в одном из четырёх направлений. И в том, и в другом случае строго возрастающая цепочка не может вернуться к самой себе.
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
def longestIncreasingPath(matrix):
# Напишите код здесьТест #1
Тест #2
Тест #3
Аргумент
matrix = [[9, 8, 3], [2, 7, 4], [1, 6, 5]]
Ожидаемый вывод
7