Золотой маршрут по сетке состояний от начальной клетки к конечной, с переходами только вправо и вниз.

Динамическое программирование: теория

Словосочетание динамическое программирование впервые было использовано в 1940-х годах Р. Беллманом для описания процесса нахождения решения задачи, где ответ на одну задачу может быть получен только после решения задачи, «предшествующей» ей. В 1953 г. он уточнил это определение до современного. Вклад Беллмана в динамическое программирование был увековечен в названии уравнения Беллмана, центрального результата теории динамического программирования, который переформулирует оптимизационную задачу в рекурсивной форме. Слово «программирование» в словосочетании «динамическое программирование» в действительности к традиционному программированию (написанию кода) почти никакого отношения не имеет и происходит от словосочетания «математическое программирование», которое является синонимом слова «оптимизация». Поэтому слово «программа» в данном контексте скорее означает оптимальную последовательность действий для получения решения задачи. К примеру, определенное расписание событий на выставке иногда называют программой. Программа в данном случае понимается как допустимая последовательность событий.

Идея метода динамического программирования состоит в сведении (с помощью рекуррентных соотношений) исходной задачи к решению некоторых ее подзадач «меньшего размера» и использовании табличной техники для сохранения уже найденных ответов [2]. Рассказ о методе динамического программирования уместно начинать с легенды о лестнице фараона [12]. Золотую лестницу фараона из девяти ступеней необходимо было модернизировать, уменьшив количество ступеней в лестнице до четырех и используя небольшое количество золота, имеющееся в казне для наращивания ступеней. Золота было так мало, а всех вариантов модернизации так много, что не сносить бы несчастному казначею головы, если бы не его умный друг-жрец.  Жрец, прекрасно владея чудесным методом динамического программирования, сумел за короткий срок, не рассматривая всех вариантов, найти оптимальное решение и выгадать себе остаток золота из казны фараона.
Разбор задач по данному методу легче воспринимается с самой известной и простой задачи про маршрут [13].

Задача 1. Маршрут по таблице

В таблице $A$ размерности $N \times N$ клетки заполнены случайным образом цифрами от 0 до 9. Найти маршрут из левой верхней клетки $A(1, 1)$ в правую нижнюю клетку $A(N, N)$ такой, что:

Вывести маршрут как последовательность пар координат клеток, через которые он проходит (первая координата – номер строки, вторая – номер столбца).

Очевидно, что все кратчайшие маршруты идут только сверху вниз и слева направо и число клеток в каждом из них $2N-1$. При достаточно большом $N$ таких маршрутов огромное количество, что полным перебором решить данную задачу практически невозможно.

Итак, рассмотрим аналогичную задачу для всевозможных прямоугольных подтаблиц размеров $(N-I+1) \times (N-J+1)$ исходной таблицы (при $I, J = 1, 2, ...$) и построим вспомогательную таблицу $B$ размером $N \times N$ с суммой цифр, через которые проходит оптимальный путь в такой таблице.
Построение вспомогательной таблицы $B$ необходимо начать с конца таблицы $A$, что приведет к следующим формулам (рис. 1):

Картинка
  1. $B(N, N) = A(N, N)$
    последняя клетка обязательно входит в маршрут
  2. $B(N, J) = B(N,J+1) + A (N, J)$ при $J = N-1, N-2, ... , 1$
    если маршрут вышел в последнюю строку таблицы, то он обязательно будет проходить до конца по последней строке, так как он кратчайший
  3. $B(I, N) = B(I+1, N) + A (I, N)$ при $I = N-1, N-2, ..., 1$
    если маршрут вышел в последний столбец таблицы, то он обязательно будет проходить до конца по последнему столбцу
  4. $B(I, J) = max (B(I+1, J),  B(I, J+1)) + A (I, J)$ при $I<N$ и $J<N$
    внутри таблицы в маршрут будет входить та клетка из последующего столбца или последующей строки, в которой сумма цифр больше
Для таблицы A со строками 2,1,3; 4,2,1; 1,5,2 таблица лучших сумм B равна 15,10,6; 13,9,3; 8,7,2. Оптимальный путь проходит через значения 2,4,2,5,2 и даёт сумму 15.
Таблицу B заполняем от правого нижнего угла: B(3,3) = 2. Формула на схеме применяется к внутренним клеткам; граничные случаи разобраны выше. Затем из старта выбираем соседа с большей суммой B: 2 + 4 + 2 + 5 + 2 = 15. Открыть схему в полном размере.

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

Программа .1:
RANDOMIZE TIMER
CLS
INPUT "Введите размерность N: "; n
DIM a(n, n), b(n, n)
FOR i = 1 TO n
FOR j = 1 TO n
a(i, j) = INT(RND * 10)
PRINT a(i, j);
NEXT j
PRINT
NEXT i
'Формирование дополнительной матрицы
b(n, n) = a(n, n)
FOR i = n - 1 TO 1 STEP -1
b(i, n) = b(i + 1, n) + a(i, n)
b(n, i) = b(n, i + 1) + a(n, i)
NEXT i
FOR i = n - 1 TO 1 STEP -1
FOR j = n - 1 TO 1 STEP -1
b(i, j) = max(b(i + 1, j), b(i, j + 1)) + a(i, j)
NEXT j, i
PRINT
PRINT "Сумма чисел на всем кратчайшем пути = "; b(1, 1)
'Путь ищется обратным проходом
PRINT "( 1; 1)";
i = 1: j = 1
WHILE (i <> n) OR (j <> n)
IF i = n THEN                                    'Если дошли до нижней границы
j = j + 1
ELSE
IF j = n THEN                       'Если дошли до правой границы
i = i + 1
ELSE
IF b(i + 1, j) > b(i, j + 1) THEN i = i + 1 ELSE j = j + 1
END IF
END IF
PRINT USING " - (##;##)"; i; j;
WEND

FUNCTION max (a, b)
IF a > b THEN max = a ELSE max = b
END FUNCTION

Задача 2. Акирема

(Областная олимпиада 1998).
В государстве Акирема алфавит состоит из букв американского языка. Каждый акиремец (житель) имеет право пополнить словарь своего языка новым словом.
Слово формируется по следующим правилам:

Какое новое слово может  сформировать акиремец по предложенным словам.

Примечание. Данные вводятся из файла с именем INPUT.TXT. В файле они заданы так: числа N и M – на первой строке через разделитель, каждое слово задаётся с новой строки.
Вывод осуществляется на экран или в файл с именем OUTPUT.TXT.

Пример: из слов
A P P L E
W I N D O W S
M O U S E
B Y T E

должно получиться слово PWOY.

Указание. Задача легко сводится к предыдущей. Строится матрица A размера NxM, где N – количество слов, а M – количество букв в самом длинном слове (табл. 1). Массив первоначально заполняется кодами ASCII букв каждого слова, расположенного в матрице по строкам с выравниванием по левой стороне (для коротких слов в матрице на местах отсутствия букв проставляются нули).

Таблица 1


65

80

80

76

69

0

0

87

73

78

68

79

87

83

77

79

85

83

69

0

0

66

89

84

69

0

0

0

Затем массив преобразуется по следующему правилу (табл. 2):

Таблица 2


320

335

332

328

315

0

0

255

247

252

242

246

225

—

166

168

174

167

138

0

0

66

89

84

69

0

0

0

В первой строке матрицы ищется наибольший элемент – это и есть искомая максимальная сумма кодов ASCII. Обратным ходом, выбирая всегда наибольшее значение, необходимо двигаться сверху вниз (с первой строки до последней), составляя строку из соответствующих букв. Искомое слово найдено - PWOY.

Из APPLE, WINDOWS, MOUSE, BYTE выбираем P, W, O, Y по соседним столбцам. Их ASCII-коды 80, 87, 79, 89 дают 335. Для P выбираем лучшее допустимое продолжение: 80 + max(255,247,252) = 335.
Состояние D(i,j) хранит лучшую сумму от выбранной буквы до последнего слова. Переход разрешён только в тот же или соседний столбец следующей строки. Пустые позиции и цепочки без продолжения не учитываются; в таблице 2 знак «—» означает недопустимое состояние. Открыть схему в полном размере.

Программа 2:

    DEFINT A-Z
    OPEN "input.txt" FOR INPUT AS #1
    INPUT #1, n, m
    DIM Word$(1 TO n), Marks(0 TO n, 0 TO m + 1)

FOR i = 1 TO n
    INPUT #1, Word$(i)
    FOR j = 1 TO LEN(Word$(i))
    Marks(i, j) = ASC(MID$(Word$(i), j, 1)) + max(Marks(i - 1, j - 1), _
    Marks(i - 1, j), Marks(i - 1, j + 1))
    NEXT
    NEXT

k = 1
    FOR i = 2 TO LEN(Word$(n))
    IF Marks(n, i) > Marks(n, k) THEN k = i
    NEXT

FOR i = n TO 1 STEP -1
    s$ = MID$(Word$(i), k, 1) + s$
    IF Marks(i - 1, k - 1) > Marks(i - 1, k + 1) THEN k = k - 1
    IF Marks(i - 1, k + 1) > Marks(i - 1, k) THEN k = k + 1
    NEXT
    PRINT s$

FUNCTION max (a, b, c)
    IF a > b THEN
    IF a > c THEN max = a ELSE max = c
    ELSE
    IF b > c THEN max = b ELSE max = c
    END IF
    END FUNCTION

Задача 3. INTERNETомания

(Районная олимпиада 1999)

1Компания «Питерские сети» проводит подключение к сети INTERNET N-этажных домов. На каждом этаже компьютеры подключаются к концентратору (HUB) по топологии «звезда», т.е. каждый компьютер соединяется с концентратором отдельным кабелем (рис. 2).
Количество квартир на каждом этаже равно K (для упрощения предполагается, что квартиры расположены на одной прямой, и нумерация квартир идет слева направо). Расстояние между соседними квартирами и этажами одинаково. Схема дома представлена в виде прямоугольной таблицы, строки которой – этажи, а столбцы – квартиры. Квартиры, имеющие компьютер, обозначаются 1 (единицей), а остальные 0 (нулями). Переход с одного этажа на другой возможен только в ближайшие квартиры (рис. 3).

2-ая картинка к задаче INTERNETомания

Требуется написать программу, которая выведет номера квартир, начиная с первого этажа, в которых должны быть расположены HUB’ы, при условии, что общая длина кабеля, соединяющего все компьютеры в сети, минимальна.

Примечание.  Концентраторы (HUB) могут располагаться и в тех квартирах, где нет компьютеров.

Формат входных данных:
Входные данные расположены в текстовом файле с именем INPUT.TXT в следующем порядке:

Формат выходных данных:
Результат работы программы выводится в файл OUTPUT.TXT и представляет собой последовательность номеров квартир, начиная с первого этажа, в которых должны располагаться концентраторы HUB.1

Пример файла INPUT.TXT (рис. 4):

Строки файла: первая – числа $N$ и $K$; каждая следующая – этаж (от 5-го к 1-му), где 1 означает квартиру с компьютером, 0 – квартиру без компьютера.

Строка файла Что означает
5 3 $N = 5$ этажей, $K = 3$ квартиры на каждом этаже
1 1 0 5-й этаж
0 0 1 4-й этаж
1 1 1 3-й этаж
0 1 0 2-й этаж
1 0 0 1-й этаж
Пример файла OUTPUT.TXT
  1
  5
  8
  12
  14 
В доме из пяти этажей по три квартиры HUB размещены снизу вверх в квартирах 1,5,8,12,14. На каждом этаже компьютеры подключены к своему HUB, а между этажами HUB соединяются в том же или соседнем столбце. Сумма длин кабелей внутри этажей равна 3.
Размещение HUB из примера: 1 → 5 → 8 → 12 → 14. Состояние D(f,j) хранит лучшую стоимость сети до этажа f при HUB в столбце j: к минимуму по допустимым предшественникам прибавляются стоимость межэтажного перехода и кабели текущего этажа. Сумма 3 на рисунке относится только к кабелям внутри этажей. Открыть схему в полном размере.

Указание. Задача легко сводится к предыдущей.

Еще одно из сравнительно новых применений динамического программирования – молекулярная биология, где базы данных содержат биологические последовательности (ДНК, РНК, белков), насчитывающие в сумме многие миллионы «букв», так что в их анализе не обойтись без компьютеров. Молекулы ДНК, содержащие генетическую информацию, - это длинные слова из четырех букв (А, Г, Ц, Т). В процессе эволюции, в результате мутаций последовательности меняются: одна буква может замениться на другую, выпасть, а может добавиться новая. Возникают жизненно важные задачи: насколько похожи два фрагмента, каким наименьшим числом превращений можно один из них получить из другого, найти подпоследовательность наибольшей длины, входящую в тот или другой фрагмент.

Задача 4. Наибольшая общая подпоследовательность

Заданы два слова длиной N и M соответственно ($N, M \le 30$). Найти подпоследовательность максимальной длины, входящую в то и другое слово (подпоследовательность не обязана быть непрерывной).

В словах ABCD и ACBD можно сохранить буквы A,B,D в том же порядке, пропустив C. Одна из наибольших общих подпоследовательностей — ABD длины 3. Таблица L для префиксов слов заканчивается значением 3.
L(i,j) — длина НОП первых i букв первого слова и первых j букв второго. Нулевая строка и столбец равны 0. При совпадении букв берём диагональ плюс 1, иначе — максимум сверху и слева. Для ABCD и ACBD ответ равен 3; подходят ABD и ACD. Открыть схему в полном размере.

Задачи на динамическое программирование

Числа Фибоначчи

Вычислить $N$-ое число в последовательности Фибоначчи, — 1, 1, 2, 3, 5, 8, … — в которой первые два члена равны единице, а все остальные представляют собой сумму двух предыдущих.
Входные данные: Целое: $1 \le N \le 70$.
Выходные данные: Одно число — $N$-ый член последовательности.

Решение: условие задачи в виде формулы (динамический переход): $F_1=1$, $F_2=1$, $F_i=F_{i-1}+F_{i-2}, i \ge 3$.

Первые восемь чисел Фибоначчи: 1,1,2,3,5,8,13,21. База F[1]=F[2]=1. Из сохранённых F[6]=8 и F[7]=13 получаем F[8]=21.
Заполняем массив слева направо: для нового числа достаточно двух уже вычисленных ответов. В примере F[8] = F[7] + F[6] = 13 + 8 = 21. Открыть схему в полном размере.
const MaxN = 70;
var i,N : Integer;
    F : Array [1..MaxN] of Int64; { Массив для хранения вычисленных значений F }
begin
  { Чтение входных данных }
  Read(N);
  { Проверка корректности входных данных (assert работает только с опцией -Sa) }
  assert( (1 <= N) and (N <= MaxN) );
  { Инициализация "динамики", первые 2 значения F }
  F[1]:=1;  F[2]:=1;
  { Общий шаг динамики: в цикле вычисляем значения F }
  for i:=3 to N do
    F[i] := F[i-1] + F[i-2];
  { Вывод результата }
  write(F[N]);
end.

Реализация на Python

# Числа Фибоначчи, динамика снизу вверх: O(N) по времени, O(N) по памяти.
# F(1) = F(2) = 1, F(i) = F(i-1) + F(i-2).


def fibonacci(n: int) -> int:
    # f[i] - i-е число Фибоначчи; f[0] в задаче не нужно, f[1] и f[2] - база динамики.
    f = [0, 1, 1]
    for i in range(3, n + 1):
        f.append(f[i - 1] + f[i - 2])
    return f[n]


def main() -> None:
    print(f"F(10) = {fibonacci(10)}")
    print(f"F(70) = {fibonacci(70)}")


if __name__ == "__main__":
    main()

Реализация на C++

// Числа Фибоначчи, динамика снизу вверх: O(N) по времени, O(N) по памяти.
#include <cstddef>
#include <cstdio>
#include <vector>

// N-е число Фибоначчи: F(1) = F(2) = 1, F(i) = F(i-1) + F(i-2).
unsigned long long fibonacci(std::size_t n) {
    // f[i] - i-е число; f[0] в задаче не нужно, f[1] и f[2] - база динамики.
    std::vector<unsigned long long> f = {0, 1, 1};
    for (std::size_t i = 3; i <= n; ++i) {
        f.push_back(f[i - 1] + f[i - 2]);
    }
    return f[n];
}

int main() {
    std::printf("F(10) = %llu\n", fibonacci(10));
    std::printf("F(70) = %llu\n", fibonacci(70));
    return 0;
}

Реализация на Rust

// Числа Фибоначчи, динамика снизу вверх: O(N) по времени, O(N) по памяти.

/// N-е число Фибоначчи: F(1) = F(2) = 1, F(i) = F(i-1) + F(i-2).
pub fn fibonacci(n: usize) -> u64 {
    // f[i] - i-е число; f[0] в задаче не нужно, f[1] и f[2] - база динамики.
    let mut f: Vec<u64> = vec![0, 1, 1];
    for i in 3..=n {
        let next = f[i - 1] + f[i - 2];
        f.push(next);
    }
    f[n]
}

fn main() {
    println!("F(10) = {}", fibonacci(10));
    println!("F(70) = {}", fibonacci(70));
}

Мячик на лесенке

На вершине лесенки, содержащей $N$ ступенек, находится мячик, который начинает прыгать по ним вниз, к основанию. Мячик может прыгнуть на следующую ступеньку, на ступеньку через одну или через 2. (То есть, если мячик лежит на 8-ой ступеньке, то он может переместиться на 5-ую, 6-ую или 7-ую.). Определить число всевозможных "маршрутов" мячика с вершины на землю.
Входные данные: Целое: $1 \le N \le 40$.
Выходные данные: Одно число — количество маршрутов.

Входной файл Выходной файл
4 7
39 12960201916
13 1705

Решение:
$D_i$ - количество всевозможных способов попасть на $i$-ую ступеньку.
$D_1=1$ - только один способ попасть на первую ступеньку.
$D_2=2$ - попасть на 2-ую можно 2-мя способами: прыгнув на неё сразу или прыгнув с 1-ой ступеньки.
$D_3=4$ - попасть на 3-ую можно 4-мя способами:
1) прыгнув на неё сразу
2) прыгнув на 1-ую, потом на 3-ю
3) прыгнув на 2-ую, потом на 3-ю
4) прыгнув на 1-ую ступеньку, потом на 2-ую, потом на 3-ю.
$D_i=D_{i-1}+D_{i-2}+D_{i-3}, i \ge 4$ - на ступеньки начиная с 4-ой можно попасть с 3-х предыдущих. Мы знаем, сколькими способами можно попасть на каждую ступеньку из предыдущих. Общий принцип: Количество вариантов как попасть в какое-то состояние равно сумме количеств вариантов как попасть в предыдущие состояния.

Мячик с высоты 4 может прыгнуть на высоту 3,2 или 1. Число спусков на землю для высот 0,1,2,3,4 равно 1,1,2,4,7. Поэтому D[4]=D[3]+D[2]+D[1]=4+2+1=7.
На рисунке D[h] — число спусков с высоты h на землю: D[0] = 1, а для отрицательных высот D[h] = 0. Счёт подъёмов в объяснении выше даёт те же числа: каждому спуску соответствует единственный обратный маршрут. Для четырёх ступенек получаем 7 спусков. Открыть схему в полном размере.
var I,N : integer;
    D : array [1..40] of Int64;
begin
  read(N);
  D[1]:=1; D[2]:=2; D[3]:=4;
  for I:=4 to N do
    D[I] := D[I-1] + D[I-2] + D[I-3];
  writeln(D[N])
end.

Реализация на Python

# Мячик на лесенке, динамика: D[i] = D[i-1] + D[i-2] + D[i-3], O(N) по времени и по памяти.
def ball_routes(n: int) -> int:
    # ways[i] - число способов попасть на ступеньку i (нумерация снизу вверх), ways[0] не используется.
    ways = [0, 1, 2, 4]  # базовые значения для первых трёх ступенек
    if n < len(ways):
        return ways[n]
    for i in range(4, n + 1):
        ways.append(ways[i - 1] + ways[i - 2] + ways[i - 3])
    return ways[n]


def main() -> None:
    for n in (4, 13, 39):
        print(f"N = {n}: маршрутов {ball_routes(n)}")


if __name__ == "__main__":
    main()

Реализация на C++

// Мячик на лесенке, динамика: D[i] = D[i-1] + D[i-2] + D[i-3], O(N) по времени и по памяти.
#include <cstddef>
#include <cstdio>
#include <vector>

// Число маршрутов мячика с вершины лесенки из n ступенек на землю.
unsigned long long ball_routes(std::size_t n) {
    // ways[i] - число способов попасть на ступеньку i (нумерация снизу вверх), ways[0] не используется.
    std::vector<unsigned long long> ways = {0, 1, 2, 4};  // базовые значения для первых трёх ступенек
    if (n < ways.size()) {
        return ways[n];
    }
    for (std::size_t i = 4; i <= n; ++i) {
        ways.push_back(ways[i - 1] + ways[i - 2] + ways[i - 3]);
    }
    return ways[n];
}

int main() {
    const std::size_t tests[] = {4, 13, 39};
    for (std::size_t n : tests) {
        std::printf("N = %zu: маршрутов %llu\n", n, ball_routes(n));
    }
    return 0;
}

Реализация на Rust

// Мячик на лесенке, динамика: D[i] = D[i-1] + D[i-2] + D[i-3], O(N) по времени и по памяти.

/// Число маршрутов мячика с вершины лесенки из n ступенек на землю.
pub fn ball_routes(n: usize) -> u64 {
    // ways[i] - число способов попасть на ступеньку i (нумерация снизу вверх), ways[0] не используется.
    let mut ways: Vec<u64> = vec![0, 1, 2, 4]; // базовые значения для первых трёх ступенек
    if n < ways.len() {
        return ways[n];
    }
    for i in 4..=n {
        let next = ways[i - 1] + ways[i - 2] + ways[i - 3];
        ways.push(next);
    }
    ways[n]
}

fn main() {
    for n in [4, 13, 39] {
        println!("N = {}: маршрутов {}", n, ball_routes(n));
    }
}

Черепашка

На квадратной доске расставлены целые неотрицательные числа. Черепашка, находящаяся в левом верхнем углу, мечтает попасть в правый нижний. При этом она может переползать только в клетку справа или снизу и хочет, чтобы сумма всех чисел, оказавшихся у нее на пути, была бы максимальной. Определить эту сумму.

Вход: Первая строка — $N$ — размер доски ($1 \leq N \leq 50$).
Далее следует $N$ строк, каждая из которых содержит $N$ целых чисел, представляющие числа на доске.
Выход: Одно число — максимальная сумма.
Тест Ответ

3
2 6 0
5 0 6
4 1 2

16

Решение: $S_{ij}$ - максимальная набранная сумма, если черепашка находится в клетке $i,j$
Динамический переход: $S_{ij}=max(S_{i-1,j},S_{i,j-1})+A_{i,j}$
Инициализация: $S_{i,0} = 0$ $S_{0,j} = 0$
Ответ: $S_{N,N}$

Доска A: строки 2,6,0; 5,0,6; 4,1,2. Таблица лучших сумм S: 2,8,8; 7,8,14; 11,12,16. Один из оптимальных путей идёт по верхней строке и правому столбцу и набирает 2+6+0+6+2=16.
Здесь S(i,j) хранит сумму от старта до текущей клетки. Таблицу заполняем слева сверху; каждая клетка выбирает лучший ответ сверху или слева. В отличие от таблицы B в первой задаче, ответ находится в правом нижнем углу: S(3,3) = 16. Открыть схему в полном размере.

Реализация на Python

# Черепашка, динамика по клеткам (ходы вправо и вниз): O(N^2) по времени и по памяти.
def max_path_sum(board: list[list[int]]) -> int:
    n = len(board)
    # best[i][j] - максимум суммы пути из (0, 0) в клетку (i, j), нумерация с 1.
    # Нулевые строка и столбец - граница; она не портит ответ, так как все числа неотрицательны.
    best = [[0] * (n + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        for j in range(1, n + 1):
            best[i][j] = max(best[i - 1][j], best[i][j - 1]) + board[i - 1][j - 1]
    return best[n][n]


def main() -> None:
    board = [
        [2, 6, 0],
        [5, 0, 6],
        [4, 1, 2],
    ]
    print(f"Максимальная сумма: {max_path_sum(board)}")


if __name__ == "__main__":
    main()

Реализация на C++

// Черепашка, динамика по клеткам (ходы вправо и вниз): O(N^2) по времени и по памяти.
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <vector>

// Максимальная сумма чисел на пути из левого верхнего угла в правый нижний.
long long max_path_sum(const std::vector<std::vector<long long>>& board) {
    const std::size_t n = board.size();
    // best[i][j] - максимум суммы пути в клетку (i, j), нумерация с 1.
    // Нулевые строка и столбец - граница; она не портит ответ, так как все числа неотрицательны.
    std::vector<std::vector<long long>> best(n + 1, std::vector<long long>(n + 1, 0));
    for (std::size_t i = 1; i <= n; ++i) {
        for (std::size_t j = 1; j <= n; ++j) {
            best[i][j] = std::max(best[i - 1][j], best[i][j - 1]) + board[i - 1][j - 1];
        }
    }
    return best[n][n];
}

int main() {
    const std::vector<std::vector<long long>> board = {
        {2, 6, 0},
        {5, 0, 6},
        {4, 1, 2},
    };
    std::printf("Максимальная сумма: %lld\n", max_path_sum(board));
    return 0;
}

Реализация на Rust

// Черепашка, динамика по клеткам (ходы вправо и вниз): O(N^2) по времени и по памяти.

/// Максимальная сумма чисел на пути из левого верхнего угла в правый нижний.
pub fn max_path_sum(board: &[Vec<u64>]) -> u64 {
    let n = board.len();
    // best[i][j] - максимум суммы пути в клетку (i, j), нумерация с 1.
    // Нулевые строка и столбец - граница; она не портит ответ, так как все числа неотрицательны.
    let mut best = vec![vec![0u64; n + 1]; n + 1];
    for i in 1..=n {
        for j in 1..=n {
            best[i][j] = best[i - 1][j].max(best[i][j - 1]) + board[i - 1][j - 1];
        }
    }
    best[n][n]
}

fn main() {
    let board: Vec<Vec<u64>> = vec![vec![2, 6, 0], vec![5, 0, 6], vec![4, 1, 2]];
    println!("Максимальная сумма: {}", max_path_sum(&board));
}

Робот

В исследовательской лаборатории фирмы Robots&Co разработали новую модель робота. Главной особенностью данной модели робота является то, что он работает по заранее заданной программе, в которой могут присутствовать команды: сделать шаг на Юг, на Север, на Восток или на Запад. Робот исполняет программу строго последовательно и, дойдя до конца программы, останавливается. Специалисты из Robots&Co заинтересовались вопросом, сколько существует различных программ, состоящих из K инструкций, таких, что робот, выйдя из начала координат, придет в точку с координатами (X, Y). Оси координат располагаются параллельно сторонам света, и единица измерения, соответствует одному шагу робота. Напишите программу, которая дает ответ на этот вопрос.
Входные данные:
Во входном файле находятся три числа K, X и Y ($0 \le K \le 16, |X|, |Y| \le 16$), разделенные пробелами.
Выходные данные:
В выходной файл ваша программа должна поместить одно число — количество программ для робота.


e.in

e.out

5 -2 -3

10

16 16 15

0

16 3 3

56216160

Решение:

$W_{KXY}$ - количество программ для робота состоящих из $K$ инструкций, таких, что робот, выйдя из начала координат, придет в точку с координатами $(X, Y)$

$W_{0XY} = 0$ при $(X, Y) \ne (0, 0)$

$W_{000} = 1$ - в начальную клетку робот может попасть 1 способом.

Общий шаг: $W_{KXY} = W_{K-1,X-1,Y} + W_{K-1,X+1,Y} + W_{K-1,X,Y-1} + W_{K-1,X,Y+1}$

$Answer = W_{KXY}$

За две команды робот попадает из (0,0) в (1,1) двумя способами: Восток–Север и Север–Восток. В динамике оба предшественника цели дают по одному пути, ещё два недостижимы: W[2,1,1]=1+1+0+0=2.
W[k,x,y] — число программ из ровно k команд. База W[0,0,0] = 1; остальные состояния нулевого слоя равны 0. В примере показаны только пути к цели (1,1): два различных порядка команд дают две программы. Открыть схему в полном размере.

Реализация на Python

# Робот: число программ из k команд, приводящих в точку (x, y). Динамика по числу команд: O(k^3) по времени, O(k^2) по памяти.

# Команды робота: шаг на восток, на запад, на север и на юг.
STEPS = ((1, 0), (-1, 0), (0, 1), (0, -1))


def robot_programs(k: int, x: int, y: int) -> int:
    # ways[(a, b)] - число программ длины i, после которых робот стоит в точке (a, b).
    # При i = 0 робот стоит в начале координат, и есть одна пустая программа.
    ways = {(0, 0): 1}
    for _ in range(k):
        # Каждая программа длины i продолжается одной командой и даёт программу длины i + 1.
        next_ways: dict[tuple[int, int], int] = {}
        for (a, b), count in ways.items():
            for dx, dy in STEPS:
                cell = (a + dx, b + dy)
                next_ways[cell] = next_ways.get(cell, 0) + count
        ways = next_ways
    return ways.get((x, y), 0)


def main() -> None:
    print(f"K = 5, X = -2, Y = -3: {robot_programs(5, -2, -3)}")
    print(f"K = 16, X = 16, Y = 15: {robot_programs(16, 16, 15)}")
    print(f"K = 16, X = 3, Y = 3: {robot_programs(16, 3, 3)}")


if __name__ == "__main__":
    main()

Реализация на C++

// Робот: число программ из k команд, приводящих в клетку (x, y). Динамика по числу команд: O(k^3 log k) по времени, O(k^2) по памяти.
#include <cstdio>
#include <map>
#include <utility>

// Сдвиги команд: восток, запад, север, юг.
const int kDx[4] = {1, -1, 0, 0};
const int kDy[4] = {0, 0, 1, -1};

// Число программ длины k, после которых робот стоит в клетке (x, y).
unsigned long long robot_programs(int k, int x, int y) {
    // ways[{a, b}] - число программ длины i, после которых робот стоит в клетке (a, b).
    std::map<std::pair<int, int>, unsigned long long> ways = {{{0, 0}, 1}};
    for (int step = 0; step < k; ++step) {
        // Каждая программа длины step продолжается одной командой и даёт программу длины step + 1.
        std::map<std::pair<int, int>, unsigned long long> next;
        for (const auto& [cell, count] : ways) {
            for (int d = 0; d < 4; ++d) {
                next[{cell.first + kDx[d], cell.second + kDy[d]}] += count;
            }
        }
        ways = std::move(next);
    }
    const auto found = ways.find({x, y});
    return found == ways.end() ? 0 : found->second;
}

int main() {
    std::printf("K = 5, X = -2, Y = -3: %llu\n", robot_programs(5, -2, -3));
    std::printf("K = 16, X = 16, Y = 15: %llu\n", robot_programs(16, 16, 15));
    std::printf("K = 16, X = 3, Y = 3: %llu\n", robot_programs(16, 3, 3));
    return 0;
}

Реализация на Rust

// Робот: число программ из k команд, приводящих в клетку (x, y). Динамика по числу команд: O(k^3) по времени, O(k^2) по памяти.
use std::collections::HashMap;

/// Сдвиги команд: восток, запад, север, юг.
const STEPS: [(i32, i32); 4] = [(1, 0), (-1, 0), (0, 1), (0, -1)];

/// Число программ длины k, после которых робот стоит в клетке (x, y).
pub fn robot_programs(k: usize, x: i32, y: i32) -> u64 {
    // ways[(a, b)] - число программ длины i, после которых робот стоит в клетке (a, b).
    let mut ways: HashMap<(i32, i32), u64> = HashMap::new();
    ways.insert((0, 0), 1);
    for _ in 0..k {
        // Каждая программа длины i продолжается одной командой и даёт программу длины i + 1.
        let mut next: HashMap<(i32, i32), u64> = HashMap::new();
        for (&(a, b), &count) in &ways {
            for (dx, dy) in STEPS {
                *next.entry((a + dx, b + dy)).or_insert(0) += count;
            }
        }
        ways = next;
    }
    ways.get(&(x, y)).copied().unwrap_or(0)
}

fn main() {
    println!("K = 5, X = -2, Y = -3: {}", robot_programs(5, -2, -3));
    println!("K = 16, X = 16, Y = 15: {}", robot_programs(16, 16, 15));
    println!("K = 16, X = 3, Y = 3: {}", robot_programs(16, 3, 3));
}

Взрывоопасность

При переработке радиоактивных материалов образуются отходы двух видов — особо опасные (тип A) и неопасные (тип B). Для их хранения используются одинаковые контейнеры. После помещения отходов в контейнеры, последние укладываются вертикальной стопкой. Стопка считается взрывоопасной, если в ней подряд идет более двух контейнеров типа A. Для заданного количества контейнеров N определить число безопасных стопок.
Входные данные:
Одно число $1 \le N \le 30$.
Выходные данные:
Одно число — количество безопасных вариантов формирования стопки.


f.in

f.out

5

24

Решение:

Безопасные стопки разделены по числу A подряд сверху: 0,1,2. B переводит любое состояние в 0, A переводит 0 в 1 и 1 в 2; добавлять A после двух A нельзя. Для пяти контейнеров количества состояний 13,7,4, всего 24.
Для пустой стопки состояния равны (1,0,0). На каждом шаге новое состояние 0 получает сумму всех трёх прежних, новое 1 — прежнее 0, новое 2 — прежнее 1. Для N = 5 ответ 13 + 7 + 4 = 24; три и более A подряд запрещены. Открыть схему в полном размере.

Реализация на Python

# Взрывоопасность: число безопасных стопок из n контейнеров (без трёх контейнеров A подряд). Динамика: O(n) по времени, O(1) по памяти.


def safe_stacks(n: int) -> int:
    # Состояние стопки - сколько контейнеров A стоит сверху подряд: 0, 1 или 2 (три подряд - взрывоопасно, не считаем).
    # no_a - стопки без A сверху (в том числе пустая), one_a - с одной A сверху, two_a - с двумя A сверху.
    no_a, one_a, two_a = 1, 0, 0
    for _ in range(n):
        # Контейнер B сводит счётчик A к нулю, контейнер A увеличивает его на единицу.
        no_a, one_a, two_a = no_a + one_a + two_a, no_a, one_a
    return no_a + one_a + two_a


def main() -> None:
    for n in range(1, 6):
        print(f"N = {n}: безопасных стопок {safe_stacks(n)}")
    print(f"N = 30: безопасных стопок {safe_stacks(30)}")


if __name__ == "__main__":
    main()

Реализация на C++

// Взрывоопасность: число безопасных стопок из n контейнеров (без трёх контейнеров A подряд). Динамика: O(n) по времени, O(1) по памяти.
#include <cstdio>

// Число безопасных стопок из n контейнеров.
unsigned long long safe_stacks(int n) {
    // no_a - стопки без A сверху (в том числе пустая), one_a - с одной A сверху, two_a - с двумя A сверху.
    unsigned long long no_a = 1;
    unsigned long long one_a = 0;
    unsigned long long two_a = 0;
    for (int i = 0; i < n; ++i) {
        // Контейнер B сводит счётчик A к нулю, контейнер A увеличивает его на единицу.
        const unsigned long long after_b = no_a + one_a + two_a;
        two_a = one_a;
        one_a = no_a;
        no_a = after_b;
    }
    return no_a + one_a + two_a;
}

int main() {
    for (int n = 1; n <= 5; ++n) {
        std::printf("N = %d: безопасных стопок %llu\n", n, safe_stacks(n));
    }
    std::printf("N = 30: безопасных стопок %llu\n", safe_stacks(30));
    return 0;
}

Реализация на Rust

// Взрывоопасность: число безопасных стопок из n контейнеров (без трёх контейнеров A подряд). Динамика: O(n) по времени, O(1) по памяти.

/// Число безопасных стопок из n контейнеров.
pub fn safe_stacks(n: usize) -> u64 {
    // no_a - стопки без A сверху (в том числе пустая), one_a - с одной A сверху, two_a - с двумя A сверху.
    let (mut no_a, mut one_a, mut two_a) = (1u64, 0u64, 0u64);
    for _ in 0..n {
        // Контейнер B сводит счётчик A к нулю, контейнер A увеличивает его на единицу.
        (no_a, one_a, two_a) = (no_a + one_a + two_a, no_a, one_a);
    }
    no_a + one_a + two_a
}

fn main() {
    for n in 1..=5 {
        println!("N = {}: безопасных стопок {}", n, safe_stacks(n));
    }
    println!("N = 30: безопасных стопок {}", safe_stacks(30));
}

К-ичные числа

Требуется вычислить количество N-значных чисел в системе счисления с основанием K, таких, что их запись не содержит двух подряд идущих нулей. ($2 \le K \le 10, N + K \le 18$).
Входные данные:
Числа N и K в десятичной записи, разделенные пробелом или переводом строки.
Выходные данные:
Искомое число в десятичной записи.


g.in

g.out

4 3

44

5 2

8

Решение:

Состояния Z и D означают записи с нулевой и ненулевой последней цифрой. Ноль дописывается только после D, ненулевая цифра — после любого состояния. При K=3 для длин 1,2,3,4 суммы равны 2,6,16,44; для длины 4 Z=12 и D=32.
Первая цифра ненулевая: Z(1) = 0, D(1) = K − 1. Далее Z(n+1) = D(n), D(n+1) = (K − 1)(Z(n) + D(n)). Для четырёхзначных троичных чисел ответ 12 + 32 = 44. Открыть схему в полном размере.

Реализация на Python

# К-ичные числа: число n-значных записей в основании k без двух нулей подряд. Динамика по последней цифре: O(n) по времени, O(1) по памяти.


def k_ary_numbers(n: int, k: int) -> int:
    if n == 0:
        return 0
    # zero_end - записи длины i, оканчивающиеся нулём; digit_end - оканчивающиеся ненулевой цифрой.
    # Первая цифра не может быть нулём: для длины 1 есть k - 1 записей, все оканчиваются ненулевой цифрой.
    zero_end, digit_end = 0, k - 1
    for _ in range(n - 1):
        # Ноль можно дописать только после ненулевой цифры, ненулевую (k - 1 вариант) - после любой записи.
        zero_end, digit_end = digit_end, (k - 1) * (zero_end + digit_end)
    return zero_end + digit_end


def main() -> None:
    print(f"N = 4, K = 3: {k_ary_numbers(4, 3)}")
    print(f"N = 5, K = 2: {k_ary_numbers(5, 2)}")
    print(f"N = 8, K = 10: {k_ary_numbers(8, 10)}")


if __name__ == "__main__":
    main()

Реализация на C++

// К-ичные числа: число n-значных записей в основании k без двух нулей подряд. Динамика по последней цифре: O(n) по времени, O(1) по памяти.
#include <cstdio>

// Число n-значных записей в основании k без двух нулей подряд (первая цифра не ноль).
unsigned long long k_ary_numbers(int n, int k) {
    if (n == 0) {
        return 0;
    }
    const auto nonzero = static_cast<unsigned long long>(k - 1);  // ненулевых цифр в основании k
    // zero_end - записи длины i, оканчивающиеся нулём; digit_end - оканчивающиеся ненулевой цифрой.
    unsigned long long zero_end = 0;
    unsigned long long digit_end = nonzero;  // длина 1: первая цифра ненулевая
    for (int i = 1; i < n; ++i) {
        // Ноль ставим только после ненулевой цифры, ненулевую - после любой записи.
        const unsigned long long next_zero_end = digit_end;
        digit_end = nonzero * (zero_end + digit_end);
        zero_end = next_zero_end;
    }
    return zero_end + digit_end;
}

int main() {
    std::printf("N = 4, K = 3: %llu\n", k_ary_numbers(4, 3));
    std::printf("N = 5, K = 2: %llu\n", k_ary_numbers(5, 2));
    std::printf("N = 8, K = 10: %llu\n", k_ary_numbers(8, 10));
    return 0;
}

Реализация на Rust

// К-ичные числа: число n-значных записей в основании k без двух нулей подряд. Динамика по последней цифре: O(n) по времени, O(1) по памяти.

/// Число n-значных записей в основании k без двух нулей подряд (первая цифра не ноль).
pub fn k_ary_numbers(n: usize, k: u64) -> u64 {
    if n == 0 {
        return 0;
    }
    let nonzero = k - 1; // ненулевых цифр в основании k
    // zero_end - записи длины i, оканчивающиеся нулём; digit_end - оканчивающиеся ненулевой цифрой.
    let (mut zero_end, mut digit_end) = (0u64, nonzero); // длина 1: первая цифра ненулевая
    for _ in 1..n {
        // Ноль ставим только после ненулевой цифры, ненулевую - после любой записи.
        (zero_end, digit_end) = (digit_end, nonzero * (zero_end + digit_end));
    }
    zero_end + digit_end
}

fn main() {
    println!("N = 4, K = 3: {}", k_ary_numbers(4, 3));
    println!("N = 5, K = 2: {}", k_ary_numbers(5, 2));
    println!("N = 8, K = 10: {}", k_ary_numbers(8, 10));
}

Паровозики

N локомотивов, имеющих номера от 1 до N и установленных на железнодорожную колею, начинают двигаться в одну сторону, причем локомотив номер k изначально движется со скоростью k км/ч. Если локомотив, движущийся с большей скоростью, нагоняет более медленный локомотив, дальше они движутся один за другим со скоростью впереди идущего локомотива. Очевидно, через некоторое время после начала движения локомотивы разобьются на несколько групп, движущихся с разной скоростью.
Написать программу, определяющую, сколько начальных расстановок s из N! возможных дадут в результате p групп движущихся локомотивов.
Входные данные:
Два числа — $1 \le N \le 16$ и $1 \le p \le N$.
Выходные данные:
Одно число — s.


h.in

h.out

4 2

11

5 4

10

6 6

1

Паровозики 4,1,3,2 движутся вправо и образуют группы [4,1] со скоростью 1 и [3,2] со скоростью 2. Число всех расстановок четырёх паровозиков с двумя группами вычисляется как c(4,2)=c(3,1)+3c(3,2)=2+9=11.
На рисунке одна расстановка образует две группы; ответ задачи считает все такие расстановки. Введём c(n,p) — их количество для n паровозиков и p групп. База c(0,0) = 1, недопустимые состояния равны 0; переход c(n,p) = c(n−1,p−1) + (n−1)c(n−1,p) даёт c(4,2) = 11. Открыть схему в полном размере.

Самый быстрый паровозик n либо ставится впереди всех и образует новую группу, либо ставится позади одного из n − 1 остальных и число групп сохраняется. Эти два случая дают слагаемые динамического перехода.

uses SysUtils;

const Nmax=16;

var
  Save : array [1..nmax,1..nmax,1..nmax] of Int64;

function F( i,n,p:byte; x:integer ):Int64;
{ i-ый паровоз впереди,
  n паровозов всего
  p групп }
var j:byte;
begin
  if Save[i,n,p]=-1 then begin
    if x=0 then writeln( Format('F(i=%d,n=%d,p=%d)=',[i,n,p] ) );
    Result := 0;
    { Рассмотрим, как может i-ый паровозик оказаться впереди }
    { При этом есть 2 варианта: }
    {  1. Он уйдёт вперёд и образует новую группу }
    {    Т.е. он быстрее предыдущего паровозика }
    {    Пусть предыдущий едет со скоростью j }
    { Мы могли получить это из ситуации:
       j in [1..i-1]
       n-1 паровоз всего;
       p-1 группа всего;
       т.е. новый паровоз образовал новую группу }
    for j:=1 to i-1 do begin
      Result := Result + F(j,n-1,p-1,x+1);
      if x = 0 then writeln( Format('  + F(i=%d,n=%d,p=%d)=%d',[j,n-1,p-1,F(j,n-1,p-1,x+1)] ) );
    end;
    {   i-ый не образует новую группу, т.е. j>i }
    for j:=i+1 to n do begin
      Result := Result + F(i,n-1,p,x+1);
      if x = 0 then writeln( Format('  + F(i=%d,n=%d,p=%d)=%d *',[i,n-1,p,F(i,n-1,p,x+1)] ) );
    end;
    Save[i,n,p] := Result;
  end;
  F:=Save[i,n,p];
  if (i>n) or (p>n) then
    assert( Result = 0 );
end;

var i,j,NN,PP,n,p:byte;
    s:Int64;
begin
  readln(NN,PP);
  for i:=1 to NN do
    for n:=1 to NN do
      for p:=1 to NN do begin
        Save[i,n,p]:=-1;
        if (i>n) or (p>n) then Save[i,n,p]:=0;
        { Одна группа - и не первый впереди - не может быть :) }
        if (p=1) and (i<>1) then Save[i,n,p]:=0;
        { Кол-во групп = кол-ву паровозов и первый - самый быстрый }
        if (n=p) and (i=n) then Save[i,n,p]:=1;
      end;
  for i:=1 to NN do
    for n:=1 to NN do
      for p:=1 to NN do begin
        if (n=p) and (i<n) then assert( F(i,n,p,1) = 0 );
        if i>n then assert( F(i,n,p,1) = 0,
          Format('F(%d,%d,%d)=%d',[i,n,p,F(i,n,p,1)]) );
      end;
  s:=0;
  for i:=1 to NN do
    s:=s+F(i,NN,PP,1);
  writeln(s);
end.

Реализация на Python

# Паровозики: число расстановок n локомотивов, дающих groups групп (числа Стирлинга первого рода). Динамика: O(n^2) по времени и по памяти.
# c(i, k) = c(i - 1, k - 1) + (i - 1) * c(i - 1, k): самый быстрый локомотив либо первый и образует новую группу,
# либо стоит за одним из i - 1 локомотивов и не меняет число групп.


def locomotive_arrangements(n: int, groups: int) -> int:
    if not 0 <= groups <= n:
        return 0
    # stirling[i][k] - число расстановок i локомотивов по k группам.
    stirling = [[0] * (n + 1) for _ in range(n + 1)]
    stirling[0][0] = 1
    for i in range(1, n + 1):
        for k in range(1, i + 1):
            stirling[i][k] = stirling[i - 1][k - 1] + (i - 1) * stirling[i - 1][k]
    return stirling[n][groups]


def main() -> None:
    print(f"N = 4, p = 2: {locomotive_arrangements(4, 2)}")
    print(f"N = 5, p = 4: {locomotive_arrangements(5, 4)}")
    print(f"N = 6, p = 6: {locomotive_arrangements(6, 6)}")
    print(f"N = 16, p = 1: {locomotive_arrangements(16, 1)}")


if __name__ == "__main__":
    main()

Реализация на C++

// Паровозики: число расстановок n локомотивов, дающих groups групп (числа Стирлинга первого рода). Динамика: O(n^2) по времени и по памяти.
#include <cstddef>
#include <cstdio>
#include <vector>

// stirling[i][k] = c(i, k): число расстановок i локомотивов по k группам.
// c(i, k) = c(i - 1, k - 1) + (i - 1) * c(i - 1, k): самый быстрый локомотив либо первый и образует группу,
// либо стоит за одним из i - 1 локомотивов и не меняет число групп.
unsigned long long locomotive_arrangements(std::size_t n, std::size_t groups) {
    if (groups > n) {
        return 0;
    }
    std::vector<std::vector<unsigned long long>> stirling(n + 1, std::vector<unsigned long long>(n + 1, 0));
    stirling[0][0] = 1;
    for (std::size_t i = 1; i <= n; ++i) {
        for (std::size_t k = 1; k <= i; ++k) {
            stirling[i][k] = stirling[i - 1][k - 1] + static_cast<unsigned long long>(i - 1) * stirling[i - 1][k];
        }
    }
    return stirling[n][groups];
}

int main() {
    std::printf("N = 4, p = 2: %llu\n", locomotive_arrangements(4, 2));
    std::printf("N = 5, p = 4: %llu\n", locomotive_arrangements(5, 4));
    std::printf("N = 6, p = 6: %llu\n", locomotive_arrangements(6, 6));
    std::printf("N = 16, p = 1: %llu\n", locomotive_arrangements(16, 1));
    return 0;
}

Реализация на Rust

// Паровозики: число расстановок n локомотивов, дающих groups групп (числа Стирлинга первого рода). Динамика: O(n^2) по времени и по памяти.

/// c(i, k) = c(i - 1, k - 1) + (i - 1) * c(i - 1, k): самый быстрый локомотив либо первый и образует группу,
/// либо стоит за одним из i - 1 локомотивов и не меняет число групп.
pub fn locomotive_arrangements(n: usize, groups: usize) -> u64 {
    if groups > n {
        return 0;
    }
    // stirling[i][k] - число расстановок i локомотивов по k группам.
    let mut stirling = vec![vec![0u64; n + 1]; n + 1];
    stirling[0][0] = 1;
    for i in 1..=n {
        for k in 1..=i {
            stirling[i][k] = stirling[i - 1][k - 1] + ((i - 1) as u64) * stirling[i - 1][k];
        }
    }
    stirling[n][groups]
}

fn main() {
    println!("N = 4, p = 2: {}", locomotive_arrangements(4, 2));
    println!("N = 5, p = 4: {}", locomotive_arrangements(5, 4));
    println!("N = 6, p = 6: {}", locomotive_arrangements(6, 6));
    println!("N = 16, p = 1: {}", locomotive_arrangements(16, 1));
}