Ролл, чтобы увидеть все стороны!

10

Допустим, у вас есть 20-гранный кубик. Вы начинаете бросать этот кубик и должны бросить его несколько десятков раз, прежде чем наконец бросить все 20 значений. Вы задаетесь вопросом, сколько рулонов мне нужно, чтобы получить 50% шанс увидеть все 20 значений? И сколько бросков nкубика с одной стороны мне нужно бросить, прежде чем я переверну все nстороны?

После некоторого исследования вы обнаружите, что существует формула для расчета вероятности броска всех nзначений после rбросков.

P(r, n) = n! * S(r, n) / n**r

где S(a, b)обозначает числа Стирлинга второго рода , количество способов разбить набор из n объектов (каждый рулон) на k непустых подмножеств (каждая сторона).

Вы также найдете последовательность OEIS , которую мы назовем R(n), которая соответствует наименьшей, rгде она P(r, n)составляет не менее 50%. Задача состоит в том, чтобы nкак можно быстрее вычислить й член этой последовательности.

Соревнование

  • Учитывая n, найти наименьшее, r где P(r, n)больше или равно 0.5или 50%.
  • Ваш код теоретически должен обрабатывать любое неотрицательное целое число в nкачестве входных данных, но мы будем тестировать ваш код только в диапазоне 1 <= n <= 1000000.
  • Для озвучивания, мы будем принимать общее время , необходимое для запуска R(n)на входах 1через 10000.
  • Мы проверим правильность ваших решений, запустив нашу версию of R(n)на вашем выводе, чтобы увидеть, если P(your_output, n) >= 0.5и P(your_output - 1, n) < 0.5, т.е. что ваш вывод на самом деле является наименьшим rдля данного n.
  • Вы можете использовать любое определение для S(a, b)вашего решения. В Википедии есть несколько определений, которые могут быть полезны здесь.
  • Вы можете использовать встроенные модули в своих решениях, включая те, которые рассчитывают S(a, b), или даже те, которые рассчитывают P(r, n)напрямую.
  • Вы можете жестко закодировать до 1000 значений R(n)и миллион чисел Стирлинга, хотя ни одно из них не является жестким ограничением, и его можно изменить, если вы сможете убедительно аргументировать их повышение или понижение.
  • Вам не нужно проверять все возможные rпромежутки между nтем, rчто мы ищем, но вам нужно найти самое маленькое, rа не только rгде- нибудь P(r, n) >= 0.5.
  • Ваша программа должна использовать язык, который можно свободно запускать в Windows 10.

Спецификации компьютера, на котором будут проверяться ваши решения, таковы i7 4790k, 8 GB RAM. Спасибо @DJMcMayhem за предоставление его компьютера для тестирования. Не стесняйтесь добавлять свои неофициальные сроки для справки, но официальное время будет предоставлено позже, как только диджей сможет его протестировать.

Контрольные примеры

n       R(n)
1       1
2       2
3       5
4       7
5       10
6       13
20      67       # our 20-sided die
52      225      # how many cards from a huge uniformly random pile until we get a full deck
100     497
366     2294     # number of people for to get 366 distinct birthdays
1000    7274
2000    15934
5000    44418
10000   95768
100000  1187943
1000000 14182022

Дайте мне знать, если у вас есть какие-либо вопросы или предложения. Удачи и хорошей оптимизации!

Sherlock9
источник
1
@JonathanAllan Знал, что я должен был выбрать другую формулировку. Спасибо за внимание.
Sherlock9

Ответы:

7

Python + NumPy, 3,95 секунды

from __future__ import division
import numpy as np

def rolls(n):
    if n == 1:
        return 1
    r = n * (np.log(n) - np.log(np.log(2)))
    x = np.log1p(np.arange(n) / -n)
    cx = x.cumsum()
    y = cx[:-1] + cx[-2::-1] - cx[-1]
    while True:
        r0 = np.round(r)
        z = np.exp(y + r0 * x[1:])
        z[::2] *= -1
        r = r0 - (z.sum() + 0.5) / z.dot(x[1:])
        if abs(r - r0) < 0.75:
            return np.ceil(r).astype(int)

for n in [1, 2, 3, 4, 5, 6, 20, 52, 100, 366, 1000, 2000, 5000, 10000, 100000, 1000000]:
    print('R({}) = {}'.format(n, rolls(n)))

import timeit
print('Benchmark: {:.2f}s'.format(timeit.timeit(lambda: sum(map(rolls, range(1, 10001))), number=1)))

Попробуйте онлайн!

Как это работает

При этом используется ряд замкнутой формы для P ( r , n ) и его производная по r , переупорядоченная для численной устойчивости и векторизации, чтобы выполнить метод Ньютона для поиска r, такого что P ( r , n ) = 0.5, округляя r до целого числа перед каждым шагом, пока шаг не переместится на r менее чем на 3/4. С хорошим начальным предположением, это обычно занимает одну или две итерации.

x i = log (1 - i / n ) = log (( i + 1) ⋅ (( n - i - 1) / n n - i ) / n )
cx i = log ( n ! / (( n - i - 1)! ⋅ n i + 1 )
y i = cx i + cx n - i - 2 - cx n - 1 = логарифмический бином ( n , i + 1)
z i = (-1) i + 1 ⋅ бином ( n , ) r
1 + ∑ z i = n! ⋅ S ( r , n ) / n r = P ( r , n )
z ix i + 1 = (-1) i + 1 ⋅ binom ( n , i + 1) ⋅ (( n - i - 1) / n ) r log (( n - i - 1) / n)
Σ г ях я + 1 = г / д г Р ( г , п )

Андерс Касеорг
источник
1
Отличная работа на весь ответ! Во-первых, я должен был понять, что это 0.366512было что log-то давным-давно. Будем использовать -log(log(2)в моей следующей итерации. Во-вторых, идея использовать метод Ньютона также очень умна, и я рад видеть, что это работает так хорошо. В-третьих, я почти наверняка собираюсь украсть exp(log(binom(n, i+1)) + r * log((n-i-1)/n)): P Слава отличному ответу! : D
Sherlock9
1
Я добавил официальные сроки! Хороший ответ Кстати:
Джеймс
2
Я действительно смущен. Я изменил numpyимпорт на from numpy import *и по какой-то причине время сократилось до 0 ... Попробуйте онлайн ?
Notjagan
Может быть, @notjagan кеш?
NoOneIsHere
1
Я хотел бы извиниться за несколько вещей: 1) мой плагиат вашего ответа, когда я пытался найти улучшения; 2) не владею этим должным образом и просто пытаюсь исправить мой ответ; 3) что это извинение заняло так много времени. Я был так огорчен, что сначала просто бросил этот вызов. В небольшой попытке репарации, я полагаю, было бы справедливо сказать вам, что мое основное улучшение в этом ответе изменилось с метода Ньютона на приращение r, так как ваше первоначальное приближение уже достаточно хорошее. Надеюсь еще раз увидеть вас в PPCG и извините за все.
Sherlock9