Нетерпеливые люди конечно же захотят организовать перебор всех возможных путей, ведущих от вершины треугольника к числам в нижнем ряду. Прежде чем программировать такой перебор, давайте подсчитаем пути. Каждый путь, дошедший до -й строки треугольника, можно продолжить до -й строки ровно двумя способами: шагнув или влево, или вправо. Так что с возрастанием количества строк число путей растёт в геометрической прогрессии, увеличиваясь с каждой новой строкой в два раза. Для треугольника с строками потребуется шаг, поэтому число возможных путей равно . После пары десятков строк количество вариантов достигнет десятков миллионов, и тут метод полного перебора (метод грубой силы) утратит всю свою брутальную привлекательность.
Всё же разберём этот метод, поскольку он по-своему интересен.
Если каждый шаг вниз и влево кодировать нулём, а вниз и вправо — единицей, путь будет кодироваться последовательностью нулей и единиц. Такие последовательности, как мы знаем, находятся во взаимно-однозначном соответствии с целыми неотрицательными числами (одним из возможных соответствий является запись числа в двоичной системе). Поэтому перебор путей в треугольнике c строками — это в сущности перебор не более чем -значных двоичных чисел, то есть чисел в диапазоне . Таким образом, следует перебирать целые неотрицательные числа из указанного диапазона. Каждое число разбирается на двоичные цифры (это удобно делать начиная с младших цифр). Пользуясь последовательностью цифр как маршрутом в треугольнике, суммируем встречающиеся числа. Для получившейся последовательности сумм индуктивно находим максимум.
Пронумеруем строки треугольника индексами , а числа в каждой строке — индексами , , . Обозначим число, стоящее в треугольнике в -й строке и на -м месте как . Заметим, что в типичном случае маршрут может придти к числу из двух чисел, расположенных на предыдущей строке: и . Исключение составляют числа, стоящие на правой и левой сторонах треугольника, соответственно (в него ведёт единственный путь из ) и (из ).
Разберём по отдельности все три случая. Введём обозначение для максимальной суммы среди всех путей, ведущих к числу .
Эти выражения означают, что вычисление сводится к вычислению значений и (или в особых случаях одного из них). Напрашивается рекурсивное решение, в котором вычислению посвящена отдельная процедура. Ответом в задаче будет максимальное из чисел ( — номер последней строки треугольника, ).
Чтобы не разбирать отдельно особые случаи (когда элемент находится на левой или правой границе треугольника и не имеет левого или правого верхнего соседа), положим , если или . Тем самым мы сделаем те пути, которые пытаются выйти за пределы треугольника, абсолютно непривлекательными с точки зрения вычисляемой вдоль них суммы. Они заведомо не выдержат конкуренции с путями, поворачивающими в другую сторону, что нам, собственно, и нужно.
Рекурсивное решение имеет свои недостатки. Во-первых, глубина рекурсивных вызовов достигает числа строк в треугольнике, а Perl, как мы знаем, нервно относится к глубокой рекурсии. Второй недостаток, гораздо более существенный, связан с тем, что рекурсивная процедура вызывается многократно для вычисления для одних и тех же , . Например, для треугольника на рисунке мы заставили процедуру выводить на экран числа , для которых она вызывается. Получилась такая последовательность вызовов: , , , , , , , , , , , , , , , , , , , , , , , , , . Видно, что для каждой пары процедура вызывается в среднем по раза. Аналогичный анализ для треугольника с пятнадцатью строками показывает, что для каждой пары процедура вызывается в среднем примерно раз. Количество избыточных повторных вызовов катастрофически растёт с увеличением размеров треугольника.
Тут нам на помощь приходит мемоизация. Будем хранить в памяти, помимо самого треугольника , ещё и таблицу . При вызове процедуры с параметрами ищем первым делом число в таблице, и, если находим, тут же возвращаем его. И уж если ячейка таблицы пока ещё не заполнена, только в этом случае вычисляем её, прибегая к рекурсивным вызовам, а вычислив, возвращаем.
Все предыдущие решения имеют ещё один недостаток, помимо уже упомянутых. В каждом из них необходимо перед началом вычислений держать в памяти весь треугольник (а в решении с мемоизацией ещё и ).
Между тем имеется быстрое решение, лишённое и этого недостатка. Оно использует идею индуктивного вычисления. Элементами последовательности служат строки треугольника, так что, обработав очередную строку, уже нет нужды возвращаться к ней вновь, а значит, не нужно держать в памяти весь треугольник.
Та функция, вычислению которой посвящена программа, не является индуктивной, однако можно построить индуктивное расширение. Это функция, вычисляющая таблицу . Так мы обозначим максимальную из сумм вдоль всевозможных путей, ведущих из вершины треугольника к -му числу в его последней строке, . Ясно, что искомая функция вычисляется, если известны значения , как наибольшее из них: .
Убедиться в индуктивности функции позволяют формулы перевычисления. Пусть — очередной ряд чисел из треугольника. Тогда (здесь — количество элементов в новой строке треугольника, и, заодно, номер этой строки).