В задаче Project Euler 1003 рассматривается процесс с камнями на целочисленных позициях прямой. В позиции \(0\) изначально лежит положительное число камней \(n\). Дальше позиции обрабатываются слева направо.
Если в позиции \(i\) находится \(m\) камней, то:
Оставшиеся одиночные камни будем называть singletons. Singleton считается lonely, если от него до любого другого singleton расстояние хотя бы \(3\). Число \(n\) называется sad, если все оставшиеся singletons lonely.
Нужно просуммировать все sad-числа, у которых singletons появляются только на позициях \(0 \le i < k\), для заданного значения \(k\).
На первый взгляд это задача про симуляцию. Но прямая симуляция по \(n\) быстро становится бессмысленной: пространство возможных \(n\) непонятно сверху, а сам процесс для некоторых \(n\) вообще не завершается. Поэтому я пошёл не от \(n\), а от множества позиций, где остаются одиночные камни.
Самая естественная первая идея — взять \(n\), запустить процесс и посмотреть, где остались singletons. Для маленьких \(n\) это работает, но как основная стратегия плохо масштабируется.
Проблем несколько.
Во-первых, в задаче не дано явной верхней границы для \(n\). Мы хотим найти все sad-числа, у которых singletons лежат ниже \(k\), но из этого напрямую не следует удобная граница на само \(n\).
Во-вторых, процесс может не завершаться. Значит, симулятор должен уметь отличать «ещё не дошли до стабилизации» от «процесс будет продолжаться бесконечно». Это уже не просто цикл по позициям.
В-третьих, условие sad относится не к промежуточным кучам, а к позициям одиночных остатков. То есть естественный объект задачи — не траектория всех камней, а бинарная последовательность чётностей: была ли позиция нечётной в момент обработки.
Поэтому удобнее развернуть задачу наоборот: зафиксировать возможное множество позиций одиночных камней и восстановить из него \(n\).
Пусть \(a_i\) — число камней в позиции \(i\) в момент её обработки. Обозначим через \(b_i\) чётность этого числа: \(b_i = a_i \bmod 2\), где \(b_i\) равно \(0\) или \(1\).
Если \(b_i = 1\), значит в позиции \(i\) остаётся singleton. Если \(b_i = 0\), одиночного остатка в этой позиции нет.
Также введём \(q_i = \lfloor a_i / 2 \rfloor\). Это число камней, которое уходит из позиции \(i\) в позицию \(i + 1\), и такое же число уходит в позицию \(i + 3\).
Тогда для каждой позиции верно:
\[ a_i = b_i + 2q_i. \]
С другой стороны, камни в позицию \(i\) могут прийти только из позиций \(i - 1\) и \(i - 3\), кроме стартовой позиции \(0\), где изначально лежит \(n\) камней. Поэтому
\[ a_i = n[i=0] + q_{i-1} + q_{i-3}, \]
где \(q_j = 0\) для отрицательных индексов, а \([i=0]\) — индикаторная функция.
Совмещая две формулы, получаем основную рекурсию:
\[ b_i + 2q_i = n[i=0] + q_{i-1} + q_{i-3}. \]
Это уже почти вся задача. Осталось понять, как извлечь из этой рекурсии удобное условие на множество позиций, где \(b_i = 1\).
Для работы с такой рекурсией удобно использовать производящие функции. Введём два формальных степенных ряда:
\[ B(x)=\sum_i b_i x^i,\qquad Q(x)=\sum_i q_i x^i. \]
Смысл здесь простой: последовательность чисел упаковывается в многочлен или степенной ряд. Сдвиг индекса превращается в умножение на \(x\). Поэтому рекурсия с задержками на \(1\) и \(3\) шага превращается в алгебраическое равенство.
Из основной рекурсии получается:
\[ B(x)+2Q(x)=n+(x+x^3)Q(x). \]
Переносим члены с \(Q(x)\) в одну сторону:
\[ (2-x-x^3)Q(x)=n-B(x). \]
Дальше появляется ключевое разложение:
\[ 2-x-x^3=(1-x)(x^2+x+2). \]
Именно оно превращает задачу из динамики камней в задачу о сравнении многочленов по модулю квадратного многочлена.
Если singletons конечны, то после последней позиции, где \(b_i = 1\), последовательность \(q_i\) удовлетворяет однородной рекурсии:
\[ 2q_i=q_{i-1}+q_{i-3}. \]
Для этой задачи важно, что хвост последовательности стабилизируется. Поэтому \((1-x)Q(x)\) становится обычным многочленом. Обозначим его через \(C(x)\):
\[ C(x)=(1-x)Q(x). \]
Тогда из предыдущего равенства следует:
\[ (x^2+x+2)C(x)=n-B(x). \]
А значит,
\[ B(x)\equiv n \pmod{x^2+x+2}. \]
Это центральный переход. Мы больше не обязаны симулировать процесс для каждого \(n\). Вместо этого можно смотреть на возможные множества позиций singletons и проверять, какой остаток даёт соответствующий многочлен.
Такой подход близок к стандартной идее работы в фактор-кольце многочленов: мы считаем многочлены не буквально, а с точностью до кратности выбранного многочлена \(x^2+x+2\).
Пусть \(T\) — множество позиций, где остаются singletons:
\[ T=\{i:b_i=1\}. \]
Тогда соответствующий многочлен имеет вид:
\[ B_T(x)=\sum_{i\in T}x^i. \]
Условие lonely означает, что любые две позиции из \(T\) отличаются хотя бы на \(3\). То есть в множестве \(T\) нельзя одновременно держать позиции на расстоянии \(1\) или \(2\).
Теперь каждому такому множеству \(T\) можно сопоставить остаток \(B_T(x)\) по модулю \(P(x)=x^2+x+2\).
Так как модуль имеет степень \(2\), любой остаток имеет вид \(ux+v\). Поэтому удобно заранее посчитать пары \((u_i, v_i)\), такие что
\[ x^i \equiv u_i x+v_i \pmod{x^2+x+2}. \]
Из сравнения \(x^2 \equiv -x - 2\) получаем начальные значения \((u_0,v_0)=(0,1)\) и \((u_1,v_1)=(1,0)\), а дальше для \(i \ge 2\):
\[ u_i=-u_{i-1}-2u_{i-2},\qquad v_i=-v_{i-1}-2v_{i-2}. \]
Это обычная линейная рекурсия второго порядка; похожие объекты часто возникают при вычислениях с многочленами по модулю. В терминах алгоритмов это просто динамическое вычисление остатков степеней \(x\).
Для множества \(T\) имеем:
\[ B_T(x)\equiv \left(\sum_{i\in T}u_i\right)x+ \sum_{i\in T}v_i \pmod{x^2+x+2}. \]
Чтобы остаток был обычным числом \(n\), коэффициент при \(x\) должен исчезнуть:
\[ \sum_{i\in T}u_i=0. \]
После этого кандидат на \(n\) равен сумме соответствующих \(v_i\):
\[ n=\sum_{i\in T}v_i. \]
Это и есть главный критерий для генерации кандидатов.
На этом месте легко ошибиться и решить, что любое множество \(T\) с нулевым коэффициентом при \(x\) уже даёт sad-число. Это не так.
Сравнение по модулю \(x^2+x+2\) гарантирует только алгебраическую совместимость множества singleton-позиций с некоторым числом \(n\). Но исходный процесс требует больше: все величины \(q_i\) должны быть неотрицательными целыми числами, потому что это реальные количества камней.
Поэтому после получения кандидата нужно восстановить \(C(x)\) из равенства
\[ C(x)=\frac{n-B_T(x)}{x^2+x+2}. \]
Затем используется связь \(C(x)=(1-x)Q(x)\). Если коэффициенты \(C(x)\) обозначить через \(c_i\), то коэффициенты \(Q(x)\) получаются как частичные суммы:
\[ q_i=c_0+c_1+\dots+c_i. \]
Все эти \(q_i\) должны быть неотрицательны. Это точная проверка, а не эвристика.
Такой двухэтапный фильтр удобен практически: сначала дешёвое модульное условие, потом строгая проверка только для сравнительно малого числа кандидатов.
Хороший способ убедиться, что вывод не сломан, — прогнать публичные примеры из условия.
Для второго sad-числа в условии множество singleton-позиций равно \(\{2,5,8,13\}\). В терминах многочленов это
\[ B_T(x)=x^2+x^5+x^8+x^{13}. \]
После редукции по модулю \(x^2+x+2\) получается константа, соответствующая примеру из условия.
Для третьего sad-числа множество имеет вид \(\{1,13\}\), то есть
\[ B_T(x)=x+x^{13}. \]
Оно снова даёт константный остаток, совпадающий с публичным примером.
Также алгоритм должен воспроизводить два контрольных значения из условия: \(S(14)\) и \(S(30)\). Эти проверки полезны тем, что ловят разные классы ошибок:
После математического сведения остаётся перебрать множества \(T\) с расстоянием между элементами хотя бы \(3\). Количество таких множеств растёт экспоненциально. Это похоже на подсчёт независимых множеств в графе-пути с дополнительными рёбрами между вершинами на расстоянии \(2\).
Для \(k=80\) полный перебор всех допустимых \(T\) уже неприятен. Но структура условия хорошо подходит для meet-in-the-middle. Идея стандартная: вместо одного большого перебора по \(80\) позициям делим задачу на две половины по \(40\) позиций.
Левая половина: позиции \(0\ldots39\). Правая половина: позиции \(40\ldots79\).
Для каждой половины перебираются все подмножества с внутренним расстоянием хотя бы \(3\). Для каждого такого подмножества сохраняются:
Граничная маска нужна потому, что lonely-условие может нарушиться через разрез. Если делить между \(39\) и \(40\), то потенциальные конфликты только такие:
\[ (38,40),\qquad (39,40),\qquad (39,41). \]
Других конфликтов через границу нет, потому что расстояние уже будет хотя бы \(3\).
Дальше левая и правая половины склеиваются так, чтобы:
Это резко уменьшает объём работы по сравнению с прямым перебором.
Были как минимум три возможных направления.
Первое — симулировать процесс по \(n\). Я его отбросил, потому что нет хорошей естественной верхней границы для \(n\), а часть процессов не завершается. Такой подход удобен для проверки маленьких примеров, но плохо подходит как основное решение.
Второе — строить динамическое программирование по позициям и состояниям хвоста. Это возможно, но состояние получается менее прозрачным: нужно хранить достаточно информации о будущих переносах, чётности и lonely-ограничении. В итоге решение превращается в аккуратный, но довольно технический автомат.
Третье — перейти к множеству singleton-позиций и использовать производящие функции. Этот путь оказался самым чистым: условие lonely становится простым ограничением на расстояния в множестве \(T\), а восстановление \(n\) сводится к остатку многочлена по модулю \(x^2+x+2\).
После этого meet-in-the-middle — естественная инженерная оптимизация. Она не меняет математику решения, а только делает перебор достаточно быстрым.
Публично я не привожу готовую реализацию, но алгоритм можно описать так:
В моей реализации такой подход работает примерно за 15 секунд в обычном интерпретируемом окружении. Конкретное время, конечно, зависит от машины, версии интерпретатора и деталей реализации.
Главная идея решения — не моделировать камни напрямую, а смотреть на позиции одиночных остатков. Производящие функции переводят процесс в сравнение
\[ B_T(x)\equiv n \pmod{x^2+x+2}. \]
После этого sad-условие превращается в ограничение на расстояния внутри множества \(T\), а поиск всех подходящих \(n\) становится конечным перебором lonely-множеств с алгебраическим фильтром.
Meet-in-the-middle делает этот перебор практичным для нужного значения \(k\), а восстановление \(q_i\) через \(C(x)=(1-x)Q(x)\) даёт строгую финальную проверку, что найденный кандидат действительно соответствует исходному процессу с неотрицательным числом камней на каждом шаге.