Сообщество Engee

Аддитивная цепочка

Автор
avatar-maximsidorovmaximsidorov
Notebook

Алгоритмы аддитивных цепочек на языке Julia

В данном примере рассматривается реализация алгоритма поиска минимальной длины аддитивной цепочки для заданных чисел на языке программирования Julia.

Введение

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

Это классическая задача в теории чисел и имеет приложения в оптимизации возведения в степень, вычислениях в криптографии, а также в анализе сложности некоторых алгоритмов.

Теоретическая часть

Аддитивная цепочка длины для представляет собой последовательность , такие как . Каждый элемент является суммой двух предыдущих членов, не обязательно различимых.

Цепь Брауэра для — это цепь сложения, где . Каждый элемент использует предыдущий элемент в качестве слагаемого.

В этом примере нас интересуют цепочки минимальной длины .

Задача

Для каждого вывести следующее:

  • ,

  • количество цепей Брауэра длины .

Функция trypermutation

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

Основная часть

Функция проверки текущего состояния последовательности — checksequence

In [ ]:
"""
Проверяет условия завершения рекурсии или продолжает её.

pos      -- текущее количество шагов (глубина рекурсии),
seq      -- текущая цепочка значений,
n        -- целевое значение,
minlen   -- максимальная разрешённая глубина (минимально найденная длина до этого момента).

Возвращает кортеж из:
(minimal_length_found, number_of_ways_with_that_length)
"""
function checksequence(pos, seq, n, minlen)
    # Если мы уже превысили известный минимум – дальше не стоит проверять
    pos > minlen || seq[1] > n ? (minlen, 0) :

    # Если первое значение стало больше N – цепочка не годится
    seq[1] == n ? (pos, 1) : # Мы достигли цели! Возвращаем кол-во шагов и одну найденную цепочку

    # Если ещё не достигли максимума, но можно продолжать перебирать:
    pos < minlen ? trypermutation(0, pos, seq, n, minlen) : (minlen, 0) # Иначе всё равно перешагнули
end
Out[0]:
checksequence

Рекурсивная функция попытки всех допустимых перестановок — trypermutation

In [ ]:
"""
trypermutation(i, pos, seq, n, minlen) 

i          -- индекс последнего элемента, который может быть слагаемым,
pos        -- сколько элементов есть в текущей цепочке (длина цепочки),
seq        -- сама цепочка чисел,
n          -- целевое (искомое) число,
minlen     -- текущая лучшая найденная длина цепочки.

Возвращает пару (лучшая_длина, число способов получить эту длину).
"""
function trypermutation(i, pos, seq, n, minlen)
    if i > pos
        return minlen, 0 # Ничего нового не нашлось – просто возвращаем минимум
    end
    
    new_seq = deepcopy(seq) # Создаём копию цепочки
    pushfirst!(new_seq, seq[1] + seq[i+1]) # Добавляем новое число в начало (слева): a[j] = a[1]+a[i+1]

    res1 = checksequence(pos + 1, new_seq, n, minlen) # Проверяем полученную цепочку дальше

    res2 = trypermutation(i + 1, pos, seq, n, res1[1]) # Пробуем другой вариант i -> i+1

    # Выбираем наилучший результат:
    if res2[1] < res1[1]
        return res2
    elseif res2[1] == res1[1]
        return res2[1], res1[2] + res2[2] # Если равно — складываем количества путей
    else
        error("Результат с большим i меньше результата меньшей i – логически ошибочно!")
    end
end
Out[0]:
trypermutation

Главная программа: вывод результатов для набора чисел

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

In [ ]:
for num in [7, 14, 21, 29, 32, 42, 64, 47, 79, 191, 382, 379]
    (minlen, nchains) = trypermutation(0, 0, [1], num, 12) # Стартовая цепочка — [1]; максимальная допустимая длина — 12
    println("N = ", num)
    println("Минимальная длина цепочек: L(n) = ", minlen)
    println("Количество цепочек такой длины (число Брауэра): ", nchains)
end
N = 7
Минимальная длина цепочек: L(n) = 4
Количество цепочек такой длины (число Брауэра): 5
N = 14
Минимальная длина цепочек: L(n) = 5
Количество цепочек такой длины (число Брауэра): 14
N = 21
Минимальная длина цепочек: L(n) = 6
Количество цепочек такой длины (число Брауэра): 26
N = 29
Минимальная длина цепочек: L(n) = 7
Количество цепочек такой длины (число Брауэра): 114
N = 32
Минимальная длина цепочек: L(n) = 5
Количество цепочек такой длины (число Брауэра): 1
N = 42
Минимальная длина цепочек: L(n) = 7
Количество цепочек такой длины (число Брауэра): 78
N = 64
Минимальная длина цепочек: L(n) = 6
Количество цепочек такой длины (число Брауэра): 1
N = 47
Минимальная длина цепочек: L(n) = 8
Количество цепочек такой длины (число Брауэра): 183
N = 79
Минимальная длина цепочек: L(n) = 9
Количество цепочек такой длины (число Брауэра): 492
N = 191
Минимальная длина цепочек: L(n) = 11
Количество цепочек такой длины (число Брауэра): 7172
N = 382
Минимальная длина цепочек: L(n) = 11
Количество цепочек такой длины (число Брауэра): 4
N = 379
Минимальная длина цепочек: L(n) = 12
Количество цепочек такой длины (число Брауэра): 6583

Как работает главный цикл?

Для каждого числа из списка выполняется следующее:

  1. Запускается рекурсивный перебор trypermutation, начинающий со стартовой цепочки [1].
  2. Рекурсия генерирует все возможные последовательности, добавляя новый член как сумму любых двух существующих.
  3. Функция checksequence отслеживает достижение нужного значения (== n) и фиксирует, если новая цепочка короче текущей.
  4. По окончании перебора выводятся два числа для каждого входного:
    • L(n) — минимальная длина аддитивной цепочки;
    • Число минимальных цепочек Брауэра (с разным порядком сложений, но той же длины).

Заключение

Мы рассмотрели реализацию алгоритма построения минимальных аддитивных цепочек методом полного перебора с использованием рекурсии в среде Julia. Для набора чисел были найдены минимальные длины соответствующих цепочек и подсчитано число таких цепочек (т.н. цепочек Брауэра). Эта техника полезна, например, для эффективных алгоритмов быстрого возведения в степень и анализа трудоемкости операций в криптографических системах.

Пример разработан с использованием материалов Rosetta Code