Найдите количество различных способов записи числа 1 в виде суммы дробей, в каждом из которых числитель равен 1, а знаменатель равен степени 2.

Мне дано число n, и я должен найти количество различных способов записать число 1 в виде суммы n дробей, где каждая дробь имеет следующий формат:

  • Числитель всегда равен 1.
  • Знаменатель — это степень числа 2 (например, 2^1, 2^2 и т. д.).

Два способа записи 1 в виде суммы таких дробей НЕ являются различными, если они содержат одни и те же дроби. Например, скажем, n=4. Одним из способов записи 1 в виде суммы 4 дробей будет следующий: 1/2 + 1/4 + 1/8 + 1/8. Но запись как 1/8 + 1/4 + 1/2 + 1/8 считается такой же (поскольку она содержит точно такие же дроби, только порядок изменился) и, следовательно, НЕ отличается от первого способа записи. Таким образом, для n=4 будет только два способа записать 1 в виде суммы 4 дробей. Первым будет 1/2 + 1/4 + 1/8 + 1/8 (упомянутый выше), а вторым будет 1/4 + 1/4 + 1/4 + 1/4. Таким образом, результат будет 2. Границы n: 2 <= n <= 2000.

Первые несколько я написал на бумаге (для n=2, для n=3, для n=4 и еще несколько) и подумал, что результаты являются частью последовательности Фибоначчи, так что я пробовал, но когда я отправил исходник на сайт, он сказал, что это неверно. У меня есть ощущение, что я должен использовать динамическое программирование, но я не уверен, как это реализовать. Любая помощь будет очень признательна. Большое спасибо!


person Community    schedule 23.09.2018    source источник
comment
У меня такое чувство, что я должен использовать динамическое программирование – почему? Кроме того, что вы на самом деле пробовали? я не вижу кода   -  person Fureeish    schedule 23.09.2018
comment
@Fureeish Я уже сказал, что пытался использовать последовательность Фибоначчи и извлечь ответ из последовательности, но это оказалось неверным. Какой смысл выкладывать неверный код? Кроме этого, я ничего не пробовал, потому что не знаю, как решить проблему. Мне нужна идея. Вот почему я задал вопрос.   -  person    schedule 23.09.2018
comment
Какой смысл публиковать неправильный код? — чтобы мы могли помочь вам проверить, что не так с кодом. Вставьте свой код, объясните ход мысли, и тогда мы сможем увидеть вашу возможную ошибку в реализации. Конечно, если вы не доказали, что ваш алгоритм и вся идея неверны. Более того, Stack Overflow — это не место, где люди будут разбираться в ваших алгоритмах за вас. Это место для конкретных вопросов по программированию. Не место для каких-либо идей о том, как начать решать эту проблему? тип вопросов.   -  person Fureeish    schedule 23.09.2018


Ответы (1)


Я предлагаю вам начать с нескольких раундов 2048. Но не вини меня, если не можешь остановиться.

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

   0100
   0100
   0100
   0010
   0001
+  0001
= 10000

Начните складывать наименьшие целые числа. Их должно быть два. В противном случае вы не могли бы обнулить последний бит. Исключение из этого правила, если n=1, т.е. у вас есть только одно слагаемое. Таким образом, вы можете сложить два самых маленьких числа, чтобы получить одно, которое в два раза больше. Затем продолжайте, пока не дойдете до одного числа. Вы можете сделать то же самое с двоичными дробями, чтобы избежать шага масштабирования, если хотите.

Таким образом, важным инвариантом здесь является то, что вы можете добавлять термины таким образом, чтобы они оставались отрицательными степенями двойки. Последовательность дополнений сформирует двоичное дерево со значением узла, указываемым глубиной: корень имеет 1, следующий уровень 1/2, следующий 1/4 и так далее. Каждый узел является либо листом без дочерних элементов, либо внутренним узлом с двумя дочерними элементами. Вас интересуют бинарные деревья с n листьями, но деревья считаются равными, если они имеют одинаковое количество листьев на каждом уровне.

Чтобы начать думать рекурсивно, как перейти от дерева с n листьями к дереву с n+1 листьями? Вы добавляете пару дочерних элементов к существующему листу. Давайте напишем код на Python.

def expansions(v):
    for i in range(len(v) - 1):
        if v[i]:
            yield tuple(x - 1 if j == i else x + 2 if j == i + 1 else x
                        for j, x in enumerate(v))
    yield v[:-1] + (v[-1] - 1, 2)  # start a new level

s = set([(1,)])  # start with a bare root
for n in range(1, 21):
    print("{:4d}: {:10d}".format(n, len(s)))
    s = set(y for x in s for y in expansions(x))

Затем перейдите на http://oeis.org/ и введите последовательность, которую вы получили с трудом. Поиск отобразит одно обращение, A002572, которое описано как

Количество разбиений 1 на n степеней 1/2

Бинго. К сожалению, он не имеет закрытой формулы. Но есть список значений, предварительно рассчитанных для n=2000. Итак, вот сценарий оболочки для определения результата для любого заданного n путем поиска в нем:

wget -qO- 'http://oeis.org/A002572/b002572.txt' | tail -n+${n:?} | head -n1 | cut -d' ' -f2

Если вам нужен более серьезный ответ, я предлагаю вам следовать ссылкам, приведенным в OEIS. Или попытайтесь понять эту функцию v, которая описана как

v(c, d) — количество разбиений d на натуральные числа вида d = c + c1 + c2 + … + cn, где c1 ≤ 2 c, ci+1 ≤ 2 ci< /под>.

и который используется для магического динамического программирования в Mathematica, Maple и Pari.

Так где связь? Чтобы ответить на этот вопрос, переключитесь с рассмотрения листовых узлов на рассмотрение внутренних узлов. Если у вас n конечных узлов, то у вас есть d=n−1 внутренних узлов. v(1,d) подсчитывает способы размещения этих внутренних узлов путем разделения количества внутренних узлов в соответствии со слоем. Вам нужен один внутренний узел в корне, если только у вас не n=1, что не полностью охвачено этим соображением. И каждый последующий слой может иметь как минимум ноль внутренних узлов и не более чем удвоенное количество внутренних узлов предыдущего слоя.

За исключением угловых случаев, основная рекурсия

v(c, d) = sum(v(i, d-c) for i=1..2*c)

потому что если d=c+c1+c2< /sub>+…+cm, тогда это соответствует dc =c1+c2+…+cm, поэтому вам нужны способы разделения d-c, где i=c1 находится между 0 и 2 c . Это можно использовать для хорошей реализации динамического программирования. Вот все значения, которые вам нужно вычислить до d=20, т. е. n=21:

v(c,d)   c=1   c=2   c=3   c=4   c=5   c=6   c=7   c=8   c=9
 d= 1:     1
 d= 2:     1     1
 d= 3:     2     1     1
 d= 4:     3     2     1     1
 d= 5:     5     4     2     1     1
 d= 6:     9     7     4     2     1     1
 d= 7:    16    12     7     4     2     1     1
 d= 8:    28    22    13     7     4     2     1     1
 d= 9:    50    39    24    13     7     4     2     1     1
 d=10:    89    70    42    24    13     7     4     2
 d=11:   159   126    76    43    24    13     7     4
 d=12:   285   225   137    78    43    24    13     7
 d=13:   510   404   245   140    78    43    24    13
 d=14:   914   725   441   251   141    78
 d=15:  1639  1299   792   452
 d=16:  2938  2331  1420   812
 d=17:  5269  4182  2550  1457
 d=18:  9451  7501
 d=19: 16952 13458
 d=20: 30410
person MvG    schedule 25.09.2018