Unique Paths
Робот начинает движение из верхней левой ячейки сетки с m строками и n столбцами и должен добраться до нижней правой ячейки. Каждый ход перемещает его на одну ячейку вправо или на одну ячейку вниз. Верните количество различных путей, которыми он может пройти.
Функция
- minteger
- количество строк в сетке
- ninteger
- количество столбцов в сетке
- Возвращаетinteger
- количество различных путей от верхней левой ячейки до нижней правой ячейки
Ограничения
1 ≤ m, n ≤ 100- Ответ не превышает
2 × 109, поэтому помещается в знаковое 32-битное целое число.
Примеры
- Ввод
- m = 3n = 4
- Вывод
- 10
- Пояснение
- Каждый путь состоит из 2 ходов вниз и 3 ходов вправо, всего 5 ходов. Путь определяется тем, какие 2 из 5 ходов направлены вниз; выбрать их можно 10 способами.
- Ввод
- m = 1n = 6
- Вывод
- 1
- Пояснение
- С одной строкой робот может двигаться вправо только 5 раз, поэтому существует ровно один путь.
- Ввод
- m = 4n = 5
- Вывод
- 35
- Пояснение
- Каждый путь содержит 3 шага вниз и 4 шага вправо. Если выбрать, какие 3 из 7 шагов будут направлены вниз, получится 7 × 6 × 5 / 6 = 35 путей.
+14 скрытых тестов при отправке
Дополнительный вопрос
Для сетки 100 × 100 ответ содержит 59 цифр. Как вернуть его по модулю 10^9+7 с помощью формулы, если деление на i больше не работает?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Где мог находиться робот прямо перед тем, как шагнуть в ячейку?
Пути в ячейку — это пути в ячейку сверху плюс пути в ячейку слева. В верхней строке и левом столбце ровно по одному пути.
Заполняй подсчёты построчно, слева направо, сохраняя один ряд чисел. Или сразу считай последовательности ходов: путь — это выбор того, какие
m-1изm+n-2ходов направлены вниз.
Решение
Перечислять пути по одному бесполезно: в сетке 17 × 17 их уже 601,080,390. Нужно подсчитать их, не перечисляя. Пути в ячейку — это пути в ячейку сверху плюс пути в ячейку слева, поэтому сетка превращается в таблицу, которую можно заполнить за один проход. Путь также представляет собой последовательность движений вниз и вправо, а это позволяет получить формулу в замкнутом виде.
Подсчитайте каждый путь с помощью рекурсии
Верно, но не успевает на самых больших тестах
Идея
Подумай о последнем ходе робота в нижнюю правую клетку. Он попал туда либо вниз из клетки сверху, либо вправо из клетки слева, но не обоими способами. Поэтому количество путей через сетку m × n равно количеству путей через сетку на одну строку короче, uniquePaths(m-1, n), плюс количеству путей через сетку на один столбец уже, uniquePaths(m, n-1).
Рекурсия останавливается на сетке с одной строкой или одним столбцом, где робот может двигаться только прямо, поэтому существует ровно 1 путь. Каждый путь заканчивается одним из двух ходов, так что каждый путь учитывается один раз, и итог верен.
Это медленно, потому что каждый путь заканчивается базовым случаем, который возвращает 1, поэтому количество вызовов не меньше самого ответа. Для сетки 17 × 17 требуется более 600 миллионов вызовов, а в тестах встречаются ответы, близкие к 1.6 × 10^9. Одни и те же сетки меньшего размера вычисляются много раз: (m-1, n-1) достигается от каждого из двух родительских состояний, а по мере продвижения вниз количество повторов растёт.
Алгоритм
- Если
mилиnравно 1, верните 1: единственный путь — это прямая линия. - В противном случае посчитайте пути, последний шаг которых направлен вниз,
uniquePaths(m-1, n). - Посчитайте пути, последний шаг которых направлен вправо,
uniquePaths(m, n-1). - Верните их сумму.
def uniquePaths(m, n):
# One row or one column: the only path is a straight line
if m == 1 or n == 1:
return 1
# The last move came down from the row above or right from the column before
return uniquePaths(m - 1, n) + uniquePaths(m, n - 1)Заполняйте сетку по одной строке за раз
Идея
Рекурсия снова и снова обращается к одним и тем же ячейкам, а всего ячеек только m × n. Подсчитайте количество путей до каждой ячейки один раз в таком порядке, чтобы нужные ячейки всегда были готовы.
Состояние: paths[r][c] — количество путей от верхней левой ячейки до строки r, столбца c. Рекуррентное соотношение: paths[r][c] = paths[r-1][c] + paths[r][c-1] — количество путей, приходящих сверху, плюс количество путей, приходящих слева. Базовые случаи: до каждой ячейки в верхней строке и левом столбце ведёт 1 путь — прямая линия. Порядок: построчно, слева направо, чтобы ячейки сверху и слева были заполнены до того, как они понадобятся.
Для m = 3 и n = 4 строки будут такими: 1 1 1 1, затем 1 2 3 4, затем 1 3 6 10, и ответ — последняя ячейка, 10.
Теперь посмотрим, какие данные читает заполнение: только предыдущую строку и заполняемую строку. Поэтому храните одну строку. Перед обновлением row[c] в ней всё ещё содержится количество путей из предыдущей строки, а row[c-1] уже содержит новое количество путей слева, поэтому row[c] += row[c-1] — это всё рекуррентное соотношение. Время работы остаётся O(m × n), а объём памяти сокращается с O(m × n) до O(n).
Алгоритм
- Создайте
rowсnэлементами, все равны 1: это верхняя строка. - Повторите
m-1раз, по одному разу для каждой строки ниже верхней. - В каждой строке для
cот 1 доn-1прибавьтеrow[c-1]кrow[c].row[0]остаётся равным 1: это левый столбец. - Верните
row[n-1].
def uniquePaths(m, n):
# row[c] counts the paths into column c of the current row.
# The top row is all 1s: the only way along it is straight right.
row = [1] * n
for _ in range(m - 1):
for c in range(1, n):
# Paths from above (the old row[c]) plus paths from the left (the new row[c-1])
row[c] += row[c - 1]
return row[n - 1]Посчитайте количество ходов с помощью биномиального коэффициента
Идея
Каждый путь состоит ровно из m-1 шагов вниз и n-1 шагов вправо — всего m+n-2 шагов в некотором порядке. Любой порядок образует допустимый путь: робот никогда не делает больше m-1 шагов вниз или n-1 шагов вправо, поэтому он не выходит за пределы сетки. Следовательно, путь — это то же самое, что выбор m-1 из m+n-2 шагов, направленных вниз, а ответом будет биномиальный коэффициент C(m+n-2, m-1).
Таблица из предыдущего подхода — это треугольник Паскаля, повёрнутый на бок, поэтому результаты совпадают. Чтобы вычислить коэффициент, не используя огромные факториалы, последовательно умножайте на каждый множитель. При N = m+n-2 и k = min(m, n)-1 умножайте на N-k+i, а затем делите на i для i от 1 до k. После шага i текущее значение равно C(N-k+i, i) — целому числу, поэтому каждое деление выполняется без остатка.
Для m = 3 и n = 4: N = 5, k = 2, и значение меняется так: 1 × 4 / 1 = 4, затем 4 × 5 / 2 = 10. Выбор по более короткой стороне ограничивает цикл 99 шагами. Произведение перед последним делением равно k, умноженному на ответ. Для сетки 17 × 17 это 16 × 601,080,390, то есть примерно 9.6 × 10^9 — больше, чем позволяет 32-битный диапазон, поэтому храните значение в 64-битном целом числе.
Алгоритм
- Задайте
N = m+n-2— количество ходов, аk = min(m, n)-1. - Установите 64-битный счётчик в значение 1.
- Для
iот 1 доkумножайте счётчик наN-k+i, затем делите его наi. - Верните значение счётчика.
def uniquePaths(m, n):
# A path is m+n-2 moves; count the ways to choose which of them go down.
# Choose along the shorter side so the loop stays short.
moves = m + n - 2
k = min(m, n) - 1
count = 1
for i in range(1, k + 1):
# count goes from C(moves-k+i-1, i-1) to C(moves-k+i, i); the division is exact
count = count * (moves - k + i) // i
return count
Ловушки и крайние случаи
Подсчёт несложный, поэтому ошибки скрываются на краях сетки и в разрядности чисел.
- Вычисление
(m+n-2)!с последующим делением на два других факториала приводит к переполнению задолго до того, как оно произойдёт в ответе: 21! уже выходит за пределы 64-битного диапазона, аm+n-2достигает 105 в сетке размером 100 × 7. - Деление перед умножением, как в
count / i * (N-k+i), приводит к усечению, посколькуcountне всегда делится наiбез остатка. Сначала умножайте: произведение всегда делится без остатка. - Произведение
count × (N-k+i)может превысить 2^31, даже если ответ этого не делает. Храните его в 64-битном целом числе. - Если оставить верхнюю строку или левый столбец равными 0 вместо 1, все ячейки будут равны 0. В сетке с одной строкой или одним столбцом есть ровно 1 путь.
- Перестановка строк и столбцов не меняет ответ, поскольку
C(m+n-2, m-1) = C(m+n-2, n-1).
Частые вопросы4
Какова формула для уникальных путей?
Ответ — биномиальный коэффициент C(m+n-2, m-1). Каждый путь включает m-1 ходов вниз и n-1 ходов вправо в некотором порядке, а выбор того, какие из m+n-2 ходов направлены вниз, однозначно задаёт путь. Для сетки 3 × 4 это C(5, 2) = 10.
Какова временная сложность задачи Unique Paths?
Таблица динамического программирования требует времени O(m × n) и памяти O(n), если хранить одну строку. Формула биномиальных коэффициентов требует времени O(min(m, n)) и памяти O(1). При обычной рекурсии число вызовов не меньше числа путей, то есть оно экспоненциально по m + n.
Как решить задачу «Уникальные пути», если некоторые ячейки заблокированы?
Используй ту же таблицу и присвой заблокированной ячейке значение 0, чтобы через неё не проходил ни один путь. Верхняя строка и левый столбец перестают состоять только из единиц: в каждой ячейке после заблокированной в верхней строке количество путей равно 0. Формула больше не работает, потому что предполагает, что разрешён любой порядок ходов.
Почему таблица уникальных путей совпадает с треугольником Паскаля?
Каждая ячейка складывает значения ячейки сверху и ячейки слева — это правило построения треугольника Паскаля, если читать его по диагоналям. В ячейке строки r и столбца c находится C(r+c, r), поэтому в нижней правой ячейке находится C(m+n-2, m-1).
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
Python
def uniquePaths(m, n):
# Напишите код здесьСлучай 1
Случай 2
Случай 3
Ввод
m = 3 n = 4
Ожидается
10