2026, Jan 13 09:03
Как ускорить поиск пар для (y+n)^4 - y^4 = (x+k)^4 - x^4: двухуказательный скан и слияние потоков
Как ускорить поиск целочисленных решений четвертичных диофантовых уравнений: монотонность, два указателя, слияние потоков и куча для неограничённого поиска
Поиск целочисленных решений для четвертичных диофантовых уравнений может стремительно перерасти во времена выполнения, недостижимые на практике, если наивно перебирать огромные сетки. Типичный пример — поиск пар (x, y), где x ≠ y, удовлетворяющих (y + n)^4 − y^4 = (x + k)^4 − x^4 для фиксированных малых положительных целых n и k. Прямой двойной цикл по большим диапазонам x и y оказывается непозволительно медленным. Ниже — пошаговое руководство, как превратить такой грубый перебор в эффективный потоковый поиск, который масштабируется, поддерживает несколько наборов параметров и при необходимости может быть неограниченным.
Базовый грубый перебор и почему он не справляется
Вот прямой вариант, который проверяет каждую пару. Логика проста: вычислить обе стороны для всех x и y и сравнить, пропуская x == y. В принципе работает, но сложность квадратична по верхней границе поиска.
inc_n = 1
inc_k = 2
upper_bound = 10**6
for xx in range(1, upper_bound):
val_right = (xx + inc_k)**4 - xx**4
for yy in range(1, upper_bound):
if xx == yy:
continue
val_left = (yy + inc_n)**4 - yy**4
if val_left == val_right:
print(f"Match found: x = {xx}, y = {yy}")
Такой подход выполняет порядка upper_bound в квадрате сравнений. Для диапазонов вроде от 1 до 1,000,000 это неприемлемо.
Ключевое наблюдение: монотонность позволяет сканировать двумя указателями
Функция z ↦ (z + t)^4 − z^4 возрастает по z для любого фиксированного положительного t. Значит, при фиксированных n и k последовательности слева по y и справа по x строго возрастают. Когда две последовательности отсортированы, вложенные циклы не нужны: их можно проходить в ногу классической техникой двух указателей — сравнивать текущие значения, продвигать ту сторону, где значение меньше, и фиксировать совпадение, когда они равны.
Эффективный поиск для одной пары (n, k)
Код ниже один раз проходит по x и y, сравнивая лишь согласованные позиции и монотонно продвигаясь вперед. Он сохраняет исходную логику и выводит только решения с x ≠ y.
inc_n = 1
inc_k = 2
upper_bound = 10**6
i_idx = 1
j_idx = 1
while i_idx < upper_bound and j_idx < upper_bound:
right_now = (i_idx + inc_k)**4 - i_idx**4
left_now = (j_idx + inc_n)**4 - j_idx**4
if left_now == right_now:
if i_idx != j_idx:
print(f"Match found: x = {i_idx}, y = {j_idx}")
i_idx += 1
j_idx += 1
elif left_now < right_now:
j_idx += 1
else:
i_idx += 1
Так удается устранить квадратичный взрыв во внутреннем цикле благодаря упорядоченности.
Расширение на множество пар (n, k) с потоковым слиянием
Если нужно искать сразу по множеству значений n и k, думайте в терминах слияния отсортированных потоков. Для каждого фиксированного сдвига k последовательность по x значений (x + k)^4 − x^4 возрастает. Их можно порождать лениво и сливать в один поток за один проход. Та же идея работает и для n на левой стороне; фрагмент ниже показывает, как объединять множество правых последовательностей и обнаруживать равенства между соседними элементами объединенного потока.
from heapq import merge
cap = 10**4
def series(delta):
for xi in range(1, cap):
yield (xi + delta)**4 - xi**4, xi, delta
last_val = None
last_x = None
last_shift = None
for cur_val, cur_x, cur_shift in merge(*map(series, range(1, 101))):
if last_val == cur_val:
print(f'prev_n={last_shift}, cur_k={cur_shift}, x_now={cur_x}, y_prev={last_x}')
last_val, last_x, last_shift = cur_val, cur_x, cur_shift
В такой конфигурации, при k от 1 до 100 и x от 1 до 10,000, подход занимает около 1,5 секунды и требует минимум памяти, воспроизводя те же решения, что и существующий поиск на тех же диапазонах. Например, среди совпадений вы увидите известный случай с положительными целыми n = 74 и k = 24, а также несколько других:
n=74, k=24, x=134, y=59
n=75, k=25, x=133, y=59
n=27, k=5, x=497, y=271
n=63, k=35, x=257, y=193
n=64, k=36, x=256, y=193
n=54, k=10, x=994, y=542
n=81, k=15, x=1491, y=813
Секрет в том, чтобы рассматривать каждую фиксацию параметра как отсортированный генератор и сливать их, не материализуя большие массивы. Вы сравниваете каждый полученный элемент только с непосредственным предыдущим в порядке слияния.
Неограниченное перечисление с кучей
Можно снять верхние границы и по x, и по параметру k, если построить слияние вручную на куче. Стартуем с k = 1 и постепенно добавляем поток для k + 1, когда текущий поток продвигается от x = 1 к x = 2. Это удерживает число активных потоков минимальным и позволяет исследовать сколь угодно большие области, пока вы даете программе работать.
from heapq import heappush, heappop
from time import time
stop_at = time() + 3
minheap = []
def push_state(shift, x_idx):
heappush(minheap, ((x_idx + shift)**4 - x_idx**4, shift, x_idx))
push_state(1, 1)
prev_val = None
prev_x = None
prev_shift = None
while time() < stop_at:
curr_val, k_shift, x_pos = heappop(minheap)
if prev_val == curr_val:
print(f'prev_n={prev_shift}, cur_k={k_shift}, x_now={x_pos}, y_prev={prev_x}')
push_state(k_shift, x_pos + 1)
if x_pos == 1:
push_state(k_shift + 1, 1)
prev_val, prev_x, prev_shift = curr_val, x_pos, k_shift
Запущенный с небольшим временным окном, этот способ выдаёт десятки решений. Жесткие ограничения на x и k исчезают благодаря ленивому расширению фронтира поиска по мере необходимости.
Что происходит под капотом
Все улучшения опираются на возрастающие последовательности и потоковую обработку. При фиксированных n или k разность четвертых степеней растет с x, поэтому вложенные циклы не нужны — кандидатов можно просматривать за один проход. Для множества параметров вы объединяете отсортированные генераторы в единую возрастающую последовательность. Слияние на базе кучи гарантирует, что вы всегда сравниваете ближайших соседей в пространстве значений, без выделения больших буферов. Версия с двумя указателями заменяет O(B^2) обход сетки на O(B) шагов для границы B на последовательность. Варианты с объединением потоков обрабатывают суммарное число элементов, пропорциональное области поиска, поддерживая лишь небольшой фронтир.
Тонкий, но важный нюанс
В вариантах с объединенными потоками равенство проверяется только с непосредственным предыдущим значением в порядке слияния. Если одно и то же число встречается три раза и более в разных потоках, код напечатает совпадения лишь для соседних пар в этом порядке. Например, если три различных выбора (x, k) дают одинаковое значение, совпадение между первым и третьим напрямую не печатается. Если нужны все попарные комбинации среди равных значений, соберите их в «корзину», а затем перечислите все пары внутри нее.
Почему это важно
Даже для на вид простых уравнений диофантовы пространства поиска огромны. Структурные свойства, такие как монотонность, позволяют заменить грубую силу потоковыми алгоритмами с предсказуемым поведением и малым потреблением памяти. Прием масштабируется от проверки одной пары параметров до сканирования широких полос значений n и k и поддерживает открытое исследование, когда вы можете запускать поиск настолько долго, насколько хватает терпения. То, что метод быстро переоткрывает известные решения в положительных целых, например n = 74 и k = 24, показывает его практическую эффективность.
Практические рекомендации
Сначала устраните квадратичный внутренний цикл, применив сканирование двумя указателями для фиксированных параметров. Когда понадобится просмотреть множество наборов параметров, мыслите категориями слияния отсортированных генераторов: Python’s heapq.merge помогает, когда набор потоков известен заранее, а ручная куча дает гибкость наращивать набор на лету. Помните о проверке равенства только с соседним значением, если ожидаете, что одно и то же число встретится чаще двух раз, и соответствующим образом скорректируйте шаг вывода. С этими элементами можно перейти от наивного перебора к экономному по памяти, высокопроизводительному поиску, который уверенно справляется с требовательными диапазонами.