Двумерное динамическое программирование.

Сайт: Информатикс
Курс: Зимняя школа в "Эврике"
Книга: Двумерное динамическое программирование.
Напечатано:: Гость
Дата: Пятница, 27 Июнь 2025, 19:32

1. Подсчет числа маршрутов


Рассмотрим шахматную доску в левом верхнем углу которой находится король. 
Король может перемещаться только вправо, вниз или по диагонали вправо-вниз на одну клетку. 
Необходимо определить количество различных маршрутов короля, приводящих его в правый нижний угол.

Сопоставим каждой клетке ее координаты (i,j), где i будет обозначать номер строки на доске, j — номер столбца. 
Нумеровать строки будем сверху вниз, столбцы — слева направо, нумерация начинается с 0. 
Тогда начальное положение короля будет клетка (0,0).

Обозначим через F(i, j) количество способов прийти из клетки (0, 0) в клетку (i, j). В клетку (i, j) можно прийти из трех клеток
— слева из (i, j − 1), сверху из (i − 1, j) и по диагонали из (i − 1, j − 1). 
Поэтому число маршрутов ведущих в клетку равно числу маршрутов из всех ее предшественников, а именно:
F(i, j) = F(i, j − 1) + F(i − 1, j) + F(i − 1, j − 1) 
Отдельно нужно задать значения для граничных клеток, то есть когда i = 0 или j = 0. 
В результате получится таблица заполненная следующим образом:

    1    1    1    1    1
    1    3    5    7    9
    1    5   13   25   41
    1    7   25   63  129
    1    9   41  129  321

Для заполнения этой таблицы и подсчета числа маршрутов можно использовать следующую программу, в которой сначала создается двумерный
список, затем заполняются крайние клетки (первый столбец и первая строка), затем заполняются остальные элементы таблицы при помощи
приведенного выше рекуррентного соотношения. 
В данном примере n - число строк, m - число столбцов на доске.
F = [[0] * m for i in range(n)]
for i in range(n):
    F[i][0] = 1
for j in range(m):
    F[0][j] = 1
for i in range(1, n):
    for j in range(1, m):
        F[i][j] = F[i][j - 1] + F[i - 1][j] + F[i - 1][j - 1]
На этом примере можно составить общий план решения задачи методом динамического программирования. 
Этот план можно использовать для решения любых задач при помощи динамического программирования:
1. Записать то, что требуется найти в задаче, как целевую функцию от некоторого набора аргументов (числовых, строковых или еще каких-либо). 
2. Свести решение задачи для произвольного набора параметров к решению аналогичных подзадач для других наборов параметров (как правило, с
меньшими значениями параметров). 
Если задача несложная, то полезно бывает выписать явное рекуррентное соотношение, задающее значение функции для данного набора параметров. 
3. Задать начальные значения функции, то есть те наборы аргументов, при которых задача тривиальна и можно явно указать значение функции. 
4. Создать массив (или другую структуру данных) для хранения значений функции. 
Как правило, если функция зависит от одного целочисленного параметра, то используется одномерный массив, для функции от двух целочисленных 
параметров — двумерный массив и т. д. 
5. Организовать заполнение массива с начальных значений, определяя очередной элемент массива при помощи выписанного на шаге 2 рекуррентного 
соотношения или алгоритма. 

Для заполнения первой строки и первого столбца таблицы мы использовали «специальную» формулу, отличающуюся от общего случая. 
Но в некоторых задачах удобней бывает все значения вычислять по одной и той же формуле, а для граничных значений функции ввести специальные 
«фиктивные» элементы. 
В данной задаче тоже можно так поступить — введем специальную «каемочку» из одного фиктивного столбца слева и одной фиктивной строки 
сверху таблицы.

Для того, чтобы значения в остальной таблице вычислялись по общим формулам, во все клетки каемочки нужно записать число 0, 
кроме клетки (0, 0), в которую будет записано значение 1:

    1    0    0    0    0    0
    0    1    1    1    1    1
    0    1    3    5    7    9
    0    1    5   13   25   41
    0    1    7   25   63  129
    0    1    9   41  129  321

Теперь во всех остальных клетках таблицы значения могут быть вычислены по общей формуле: F(i, j) = F(i, j − 1) + F(i − 1, j) + F(i − 1, j − 1), а программа может выглядеть так:
F = [[0] * (m + 1) for i in range(n + 1)]
F[0][0] = 1
for i in range(1, n + 1):
    for j in range(1, m + 1):
        F[i][j] = F[i][j - 1] + F[i - 1][j] + F[i - 1][j — 1]

2. Маршрут наименьшей стоимости.


Теперь решим задачу о нахождении маршрута минимальной стоимости из левого верхнего угла в правый нижний, считая что для каждой клетке 
указана стоимость прохода через эту клетку. 
Сразу же будем считать, что таблица снабжена «каемочкой», поэтому начальная клетка будет иметь индексы (1, 1), конечная клетка — (n, m), 
а строка и столбец с индексом 0 будут относится к фиктивной каемочке.

Если считать, что стоимость прохода через клетку (i, j) записана в отдельном списке Price[i, j], то обозначив через С(i, j) стоимость 
кратчайшего пути из начальной клетки (1, 1) в клетку (i, j) получим рекуррентное соотношение: 
C(i, j) = min(C(i, j − 1), C(i − 1, j), C(i − 1, j − 1)) + Price[i, j]
При этом при вычислении граничных значений (в первом столбце и первой строке) необходимо учитывать только клетки из этого столбца и этой
строки (но не из предыдущего столбца и предыдущей строки). 
Это удобно реализовать, если заполнить предыдущую строк и предыдущий столбец «каемочкой», записав для клеток каемочки значения функции C 
равными некоторому очень большому числу. 
Это число мы будем обозначать как INF, в качестве значения INF следует взять число, заведомо больше, чем максимальное число, 
которое может быть записано в таблице C. А в угол «каемочки» нужно записать число 0: C[0][0] = 0.

INF = 10 ** 20
C = [[0] * (m + 1) for i in range(n + 1)]
C[0][0] = 0
for i in range(1, n + 1):
    C[i][0] = INF
for j in range(1, m + 1):
    C[0][j] = INF
for i in range(1, n + 1):
    for j in range(1, m + 1):
        C[i][j] = min(C[i][j - 1], C[i - 1][j], C[i - 1][j - 1]) + Price[i][j]

3. Восстановление ответа.


Теперь напишем восстановление ответа. 
Точно так же, как и в одномерном случае будем идти на конечной клетке в начальную, на каждом шаге выбирая ту из возможных 
предшествующих клеток, для которой значение функции C меньше.

Answer = []
i = n
j = m
while (i, j) != (0, 0):
    Answer.append((i, j))
    prev_C = min(C[i][j - 1], C[i - 1][j], C[i - 1][j - 1])
    if C[i][j - 1] == prev_C:
        i, j = i, j - 1
    elif C[i - 1][j] == prev_C:
        i, j = i - 1, j
    else:
        i, j = i - 1, j - 1
Answer = Answer[::-1]