Розрахуйте ймовірність точно і швидко


10

[Це питання партнера, щоб точно обчислити ймовірність ]

Це завдання полягає в написанні коду для того, щоб точно і швидко обчислити ймовірність . Вихід повинен бути точною ймовірністю, записаною у вигляді дробу в найбільш скороченому вигляді. Тобто вона ніколи не повинна виводити, 4/8а навпаки 1/2.

Для деякого додатного цілого числа nрозглянемо рівномірно випадковий рядок довжиною 1s та -1s nі назвемо його А. Тепер приєднайтесь до Aсвого першого значення. Тобто A[1] = A[n+1]якщо індексація від 1. Aтепер має довжину n+1. Тепер також розглянемо другий випадковий рядок довжини n, перші nзначення якого становлять -1, 0 або 1 з вірогідністю 1/4,1 / 2, 1/4 кожне і назвемо його B.

Тепер розглянемо внутрішній продукт A[1,...,n]і Bі внутрішній продукт A[2,...,n+1]і B.

Наприклад, розглянемо n=3. Можливі значення для Aі Bможуть бути A = [-1,1,1,-1]і B=[0,1,-1]. У цьому випадку два внутрішні продукти є 0і 2.

Ваш код повинен виводити ймовірність того, що обидва внутрішні продукти дорівнюють нулю.

Копіюючи таблицю, виготовлену Мартином Бюттнером, ми маємо такі результати вибірки.

n   P(n)
1   1/2
2   3/8
3   7/32
4   89/512
5   269/2048
6   903/8192
7   3035/32768
8   169801/2097152

Мови та бібліотеки

Ви можете використовувати будь-яку вільно доступну мову та бібліотеки, які вам подобаються. Я повинен бути в змозі запустити ваш код, тому, будь-ласка, включіть повне пояснення, як запустити / скомпілювати ваш код у Linux.

Завдання

Ваш код повинен починатися з n=1і давати правильний вихід для кожного збільшення n в окремому рядку. Він повинен припинитися через 10 секунд.

Рахунок

Оцінка - це найвищий показник, nперш ніж ваш код зупиниться через 10 секунд при запуску на моєму комп’ютері. Якщо є нічия, виграє той, хто швидше потрапить до найвищої оцінки.

Таблиця записів

  • n = 64в Python . Версія 1 Мітча Шварца
  • n = 106в Python . Версія 11 червня 2015 року Мітчем Шварцом
  • n = 151в C ++ . Відповідь Мітта Шварца від kirbyfan64sos
  • n = 165в Python . Версія 11 червня 2015 р. "Обрізка" версія Мітча Шварца с N_MAX = 165.
  • n = 945в Python by Min_25, використовуючи точну формулу. Дивовижний!
  • n = 1228в Python від Мітча Шварца, використовуючи іншу точну формулу (на основі попередньої відповіді Min_25).
  • n = 2761в Python від Мітча Шварца з використанням більш швидкої реалізації тієї ж точної формули.
  • n = 3250в Python, використовуючи Pypy by Mitch Schwartz, використовуючи ту ж саму реалізацію. Цей показник повинен pypy MitchSchwartz-faster.py |tailуникати прокрутки консолі над головою.

Цікаво, чи нудне рішення буде працювати швидше, ніж Boost C ++?
qwr

@qwr Я думаю, що numpy, numba та cython були б цікаві, оскільки вони зберігаються в сім'ї Python.

2
Я хотів би побачити більше таких проблем із найшвидшим кодом
qwr

@qwr - це мій улюблений тип запитання ... Дякую! Завдання полягає в тому, щоб знайти той, який не передбачає кодування точно такого ж алгоритму мовою найнижчого рівня, яку ви можете знайти.

Ви записуєте результати на консоль чи файл? Використання pypy та запис у файл здається мені найшвидшим. Консоль значно сповільнює процес.
гніблер

Відповіді:


24

Пітон

Формула закритої форми p(n)є

введіть тут опис зображення

Експоненціальна генеруюча функція p(n)є

введіть тут опис зображення

де I_0(x)модифікована функція Бесселя першого роду.

Редагувати 11.06.2015:
- оновлено код Python.

Редагувати 13.06.2015:
- додано доказ наведеної формули.
- виправленоtime_limit .
- додано код PARI / GP.

Пітон

def solve():
  # straightforward implementation

  from time import time
  from itertools import count

  def binom(n, r):
    return facts[n] // (facts[r] * facts[n - r])

  def p(N):
    ans = 0
    for i in range(1 + N // 2):
      t = binom(2 * (N - 2 * i), N - 2 * i)
      t *= binom(N, 2 * i)
      t *= binom(4 * i, 2 * i)
      ans += t
    e = (ans & -ans).bit_length() - 1
    numer = ans >> e
    denom = 1 << (3 * N - 1 - e)
    return numer, denom

  facts = [1]
  time_limit = 10.0 + time()

  for i in count(1):
    facts.append(facts[-1] * (2 * i - 1))
    facts.append(facts[-1] * (2 * i))

    n, d = p(i)

    if time() > time_limit:
      break

    print("%d %d/%d" % (i, n, d))

solve()

PARI / GP

p(n) = polcoeff( (exp(x/2) + 1) * besseli(0, x/4) ^ 2, n) * n!;

Доказ:
Ця проблема схожа на двовимірну (обмежену) проблему випадкової ходи.

Якщо A[i] = A[i+1]ми можемо перейти від (x, y)до (x+1, y+1)[1] До речі,(x, y) [2 способи] або (x-1, y-1)[1] спосіб.

Якщо A[i] != A[i+1]ми можемо перейти від (x, y)до (x-1, y+1)[1] До речі,(x, y) [2 способи] або (x+1, y-1)[1] спосіб.

Нехай a(n, m) = [x^m]((x+1)^n + (x-1)^n), b(n) = [x^n](1+x)^{2n}і c(n)бути число способів , щоб перейти від (0, 0)до(0, 0) з nкроками.

Тоді, c(n) = \sum_{i=0}^n a(n, i) * b(i) * b(n-i).

Оскільки p(n) = c(n) / 8^nми можемо отримати вищезазначену формулу закритої форми.


1
Це .. ну .. дивовижно! Як на землі ви обчислили точну формулу?

1
Оце Так! Закрита форма завжди охайна!
qwr

1
@Lembik: Я додав (грубий) доказ.
Мін_25,

1
@qwr: Дякую Я також так думаю !
Мін_25,

1
@ mbomb007: Так. Але це швидше завдання з реалізації, а не обчислювальне завдання. Отже, я б не кодував це в C ++.
Мін_25,

9

Пітон

Примітка: Вітаємо Min_25 за знайдене рішення закритої форми!

Дякую за цікаву проблему! Це можна вирішити за допомогою DP, хоча в даний час я не дуже вмотивований оптимізувати швидкість, щоб отримати більш високий бал. Це може бути приємно для гольфу.

Код досягнуто N=39протягом 10 секунд на цьому старому ноутбуці під керуванням Python 2.7.5.

from time import*
from fractions import*
from collections import*

X={(1,0,0,0):1,(-1,0,0,0):1}

T=time()
N=0

while 1:
    Y=defaultdict(lambda:0)
    n=d=0
    for a,b,s,t in X:
        c=X[(a,b,s,t)]
        for A in ( (1,-1) if N else [a] ):
            for B in 1,0,0,-1:
                n+=c*(s+A*B==0==t+A*b+a*B)
                d+=c
                Y[(a,B,s+A*B,t+A*b)]+=c
    if time()>T+10: break
    N+=1
    print N,Fraction(n,d)
    X=Y

Для кортежу (a,b,s,t): aперший елемент A, bє останнім елементом B, sє внутрішнім продуктом A[:-1]і B, і tє внутрішнім продуктом A[1:-1]і B[:-1], використовуючи позначення зрізів Python. Мій код не зберігає масиви Aі Bніде, тому я використовую ці букви для посилання на наступні елементи, які потрібно додати, Aі Bвідповідно. Цей вибір імен змінних робить пояснення трохи незручним, але дозволяє приємно виглядати A*b+a*Bв самому коді. Зауважте, що доданий елемент Aє передостаннім, оскільки останній елемент завжди такий самий, як перший. Я використав фокус Мартіна Бюттнера, що включав 0двічіB кандидатів, щоб отримати правильний розподіл ймовірностей. Словник X(який названий YпоN+1) слідкує за кількістю всіх можливих масивів відповідно до значення кортежу. Змінні nта dпозначають чисельник та знаменник, саме тому я перейменував nзаяву проблеми якN .

Ключова частина логіки полягає в тому, що ви можете оновити Nдо N+1використання лише значень в кортежі. Два внутрішні продукти, зазначені у питанні, задані s+A*Bта t+A*b+a*B. Це зрозуміло, якщо ви трохи вивчите визначення; зауважимо, що [A,a]і [b,B]є останніми двома елементами масивів AіB відповідно.

Зверніть увагу , що sі tмаленькі і обмежені в відповідно доN , і для швидкої реалізації в швидкому мовою ми могли б уникнути словників на користь масивів.

Можливо, можливо скористатися симетрією, враховуючи значення, що відрізняються лише за знаком; Я не вдивлявся в це.

Зауваження 1 : Розмір словника зростає квадратично N, де розмір означає кількість пар ключових значень.

Зауваження 2 : Якщо ми встановимо верхню межу N, тоді ми можемо обрізати кортежі, для яких N_MAX - N <= |s|і так само t. Це можна зробити, вказавши поглинаючий стан або неявно зі змінною для проведення рахунку обрізаних станів (які потрібно було б помножити на 8 при кожній ітерації).

Оновлення : Ця версія швидша:

from time import*
from fractions import*
from collections import*

N_MAX=115

def main():
    T=time()

    N=1
    Y={(1,0,0,0):1,(1,1,1,0):1}
    n=1
    thresh=N_MAX

    while time() <= T+10:
        print('%d %s'%(N,Fraction(n,8**N/4)))

        N+=1
        X=Y
        Y=defaultdict(lambda:0)
        n=0

        if thresh<2:
            print('reached MAX_N with %.2f seconds remaining'%(T+10-time()))
            return

        for a,b,s,t in X:
            if not abs(s)<thresh>=abs(t):
                continue

            c=X[(a,b,s,t)]

            # 1,1

            if not s+1 and not t+b+a: n+=c
            Y[(a,1,s+1,t+b)]+=c

            # -1,1

            if not s-1 and not t-b+a: n+=c
            Y[(a,1,s-1,t-b)]+=c

            # 1,-1

            if not s-1 and not t+b-a: n+=c
            Y[(a,-1,s-1,t+b)]+=c

            # -1,-1

            if not s+1 and not t-b-a: n+=c
            Y[(a,-1,s+1,t-b)]+=c

            # 1,0

            c+=c

            if not s and not t+b: n+=c
            Y[(a,0,s,t+b)]+=c

            # -1,0

            if not s and not t-b: n+=c
            Y[(a,0,s,t-b)]+=c

        thresh-=1

main()

Впроваджено оптимізацію:

  • все вкласти main() - локальний доступ до змінних швидше, ніж глобальний
  • ручка N=1явно уникнути перевірки (1,-1) if N else [a](який втілює , що перший елемент кортежу відповідає, при додаванні елементів до Aпочинаючи з порожнього списку)
  • розкручуємо внутрішні петлі, що також виключає множення
  • подвоїти рахунок cдля додавання 0до, Bа не робити ці операції двічі
  • знаменник завжди, 8^Nтому нам не потрібно його слідкувати
  • Тепер зважаючи на симетрію: ми можемо виправити перший елемент Aяк 1і розділити знаменник на 2, оскільки дійсні пари (A,B)з A[1]=1та ті, що мають, A[1]=-1можна поставити у відповідність один на один, відкинувши A. Точно так само ми можемо виправити перший елемент Bяк негативний.
  • тепер з обрізкою. Вам потрібно буде поспілкуватися, N_MAXщоб побачити, який бал він може отримати на вашій машині. Можна було б переписати для N_MAXавтоматичного пошуку відповідного двійкового пошуку, але це здається непотрібним? Примітка. Нам не потрібно перевіряти стан обрізки до тих пір N_MAX / 2, поки ми не зможемо досягти невеликого прискорення, повторившись у дві фази, але я вирішив не робити простоту та чистоту коду.

1
Це дійсно чудова відповідь! Чи можете ви пояснити, що ви зробили у своєму прискоренні?

@Lembik Дякую :) Додав пояснення, ще одну невелику оптимізацію, і зробив її сумісною з Python3.
Мітч Шварц

На своєму комп’ютері я потрапив N=57для першої версії та N=75для другої.
kirbyfan64sos

Ваші відповіді були приголомшливими. Просто відповідь Min_25 була ще більше :)

5

Пітон

Використовуючи ідею Min_25 про випадкову прогулянку, я зміг дійти до іншої формули:

p (n) = \ початок {випадків} \ frac {\ sum _ {i = 0} ^ {\ lfloor n / 2 \ rfloor} \ binom {2i} {i} ^ 2 \ binom {n} {2i} 4 ^ {n-2i}} {8 ^ n} & n \ текст {непарний} \ \ frac {\ binom {n} {n / 2} ^ 2 + \ сума _ {i = 0} ^ {\ lfloor n / 2 \ rfloor} \ binom {2i} {i} ^ 2 \ біном {n} {2i} 4 ^ {n-2i}} {8 ^ n} & n \ текст {навіть} \ \ кінець {випадки}

Ось реалізація Python на основі Min_25:

from time import*
from itertools import*

def main():
    def binom(n, k):
        return facts[n]/(facts[k]*facts[n-k])

    def p(n):
        numer=0
        for i in range(n/2+1):
            t=binom(2*i,i)
            t*=t
            t*=binom(n,2*i)
            t<<=2*(n-2*i)
            numer+=t
        if not n&1:
            numer+=t
        e=(numer&-numer).bit_length()-1
        numer>>=e
        denom=1<<(3*n-e)
        return numer, denom

    facts=[1]
    time_limit=time()+10

    for i in count(1):
        facts.append(facts[-1]*i)

        n,d=p(i)

        if time()>time_limit:
            break

        print("%d %d/%d"%(i,n,d))

main()

Пояснення / доказ:

Спочатку ми вирішуємо пов'язану з ними проблему підрахунку, де ми це дозволяємо A[n+1] = -A[1]; тобто додатковий елемент, об'єднаний в, Aможе бути 1або -1незалежно від першого елемента. Тож нам не потрібно слідкувати, скільки разів A[i] = A[i+1]трапляється. У нас є така випадкова прогулянка:

Від (x,y)ми можемо перейти до (x+1,y+1)[1] До речі, (x+1,y-1)[1 шлях], (x-1,y+1)[1 шлях], (x-1,y-1)[1] шлях, (x,y)[4 способи]

де xі yстенд для двох продуктів точкових, і ми розраховуємо кількість способів , щоб перейти від (0,0)до (0,0)в nкроках. Цей підрахунок потім буде помножений 2на облік того факту, який Aможе починатися з 1або -1.

Ми маємо в виду перебування в (x,y)якості нульового кроку .

Ми повторюємо кількість ненульових ходів i, які повинні бути рівними, щоб повернутися до (0,0). Горизонтальні та вертикальні рухи складають дві незалежні одновимірні випадкові прогулянки, які можна підрахувати C(i,i/2)^2, де C(n,k)є двочленний коефіцієнт. (Для прогулянки з kкроками ліворуч і kкроків праворуч є C(2k,k)способи вибору порядку чергових кроків.) Крім того, є C(n,i)способи розміщення ходів і 4^(n-i)способи вибору нульових ходів. Таким чином ми отримуємо:

a(n) = 2 * sum_{i in (0,2,4,...,n)} C(i/2,i)^2 * C(n,i) * 4^(n-i)

Тепер нам потрібно повернутися до початкової проблеми. Визначте допустиму пару, яка (A,B)має бути конвертованою, якщо вона Bмістить нуль. Визначте пару, (A,B)що є майже допустимою, якщо A[n+1] = -A[1]і два крапкові продукти дорівнюють нулю.

Лема: Для даної nмайже допустимі пари знаходяться в одній співвідношенні з конвертованими парами.

Ми можемо (оборотно) перетворити конвертовану пару (A,B)в майже допустиму пару (A',B')шляхом відмови, A[m+1:]і B[m+1:]де mіндекс останнього нуля в B. Перевірка на це є простою: Якщо останній елемент Bдорівнює нулю, нам нічого не потрібно робити. В іншому випадку, коли ми заперечуємо останній елемент A, ми можемо заперечити останній елемент B, щоб зберегти останній член зміщеного крапкового добутку. Але це заперечує останнє значення не зміщеного крапкового добутку, тому ми виправляємо це, відкидаючи елемент другий-останній A. Але тоді це скидає значення другого до останнього значення зміщеного продукту, тому ми заперечуємо елемент другий-останній B. І так далі, до досягнення нульового елемента в B.

Тепер нам просто потрібно показати, що немає майже допустимих пар, для яких Bне міститься нуль. Щоб крапковий продукт дорівнював нулю, ми повинні мати рівну кількість 1та -1умови, які потрібно скасувати. Кожен -1термін складається з (1,-1)або (-1,1). Отже, парність числа, -1що виникають, фіксується відповідно до n. Якщо перший і останній елементи Aмають різні ознаки, ми змінюємо паритет, тому це неможливо.

Таким чином ми отримуємо

c(n) = a(n)/2 if n is odd, else a(n)/2 + C(n,n/2)^2

p(n) = c(n) / 8^n

яка дає наведену формулу (переіндексація за допомогою i' = i/2).

Оновлення: Ось більш швидка версія з тією ж формулою:

from time import*
from itertools import*

def main():
    time_limit=time()+10

    binoms=[1]
    cb2s=[1]
    cb=1

    for n in count(1):
        if n&1:
            binoms=[a+b for a,b in zip([0]+binoms,binoms)]
        else:
            binoms=[a+b for a,b in zip([0]+binoms,binoms+[binoms[-1]])]
            cb=(cb<<2)-(cb+cb)/(n/2)
            cb2s.append(cb*cb)

        numer=0
        for i in xrange(n/2+1):
            t=cb2s[i]*binoms[min(2*i,n-2*i)]
            t<<=2*(n-2*i)
            numer+=t
        if not n&1:
            numer+=t
        e=(numer&-numer).bit_length()-1
        numer>>=e
        denom=1<<(3*n-e)

        if time()>time_limit:
            break

        print("%d %d/%d"%(n,numer,denom))

main()

Впроваджено оптимізацію:

  • вбудована функція p(n)
  • використовувати рецидиви для біноміальних коефіцієнтів C(n,k)зk <= n/2
  • використовувати рецидиви для центральних біноміальних коефіцієнтів

Просто, щоб ви знали, p(n)не потрібно функціонувати кусочно. Загалом, якщо f(n) == {g(n) : n is odd; h(n) : n is even}тоді ви можете писати f(n) == (n-2*floor(n/2))*g(n) + ((n+1)-2*(floor((n+1)/2)))*h(n)або використовувати n mod 2замість (n-2*floor(n/2)). Дивіться тут
mbomb007

1
@ mbomb007 Це очевидно і нецікаво.
Мітч Шварц

3

Пояснення формули Мін_25

Min_25 опублікував чудовий доказ, але це пройшло певний час. Це трохи пояснень для заповнення між рядками.

a (n, m) представляє кількість способів вибору A таким, що A [i] = A [i + 1] m разів. Формула для (n, m) еквівалентна a (n, m) = {2 * (n вибрати m) для nm навіть; 0 для nm непарних.} Дозволений лише один паритет, оскільки A [i]! = A [i + 1] повинен виникати парне число разів, так що A [0] = A [n]. Коефіцієнт 2 обумовлений початковим вибором A [0] = 1 або A [0] = -1.

Після того, як число (A [i]! = A [i + 1]) закріплено як q (назване i у формулі c (n)), воно розділиться на два 1D випадкових прогулянки довжиною q і nq. b (m) - це кількість способів здійснити одновимірний випадковий хід m кроків, який закінчується там же, де він розпочався, і має 25% шансів рухатися ліворуч, 50% шансу залишитися на місці і 25% шансу рухається праворуч. Більш очевидним способом констатувати функцію, що генерує, є [x ^ m] (1 + 2x + x ^ 2) ^ n, де 1, 2x і x ^ 2 являють собою ліворуч, ні переміщення, і праворуч відповідно. Але тоді 1 + 2x + x ^ 2 = (x + 1) ^ 2.


Ще одна причина любити PPCG! Дякую.

2

C ++

Просто порт із (відмінного) відповіді Python Мітчем Шварцом. Основна відмінність полягає в тому , що я використовував 2для подання -1для aзмінного і зробив що - щось схоже на b, що дозволило мені використовувати масив. Використовуючи Intel C ++ -O3, я отримав N=141! Моя перша версія отримала N=140.

Для цього використовується Boost. Я спробував паралельну версію, але зіткнувся з деякими проблемами.

#include <boost/multiprecision/gmp.hpp>
#include <boost/typeof/typeof.hpp>
#include <boost/rational.hpp>
#include <boost/chrono.hpp>
#include <boost/array.hpp>
#include <iostream>
#include <utility>
#include <map>

typedef boost::multiprecision::mpz_int integer;
typedef boost::array<boost::array<std::map<int, std::map<int, integer> >, 3>, 2> array;
typedef boost::rational<integer> rational;

int main() {
    BOOST_AUTO(T, boost::chrono::high_resolution_clock::now());

    int N = 1;
    integer n = 1;
    array* Y = new array, *X = NULL;
    (*Y)[1][0][0][0] = 1;
    (*Y)[1][1][1][0] = 1;

    while (boost::chrono::high_resolution_clock::now() < T+boost::chrono::seconds(10)) {
        std::cout << N << " " << rational(n, boost::multiprecision::pow(integer(8), N)/4) << std::endl;
        ++N;
        delete X;
        X = Y;
        Y = new array;
        n = 0;

        for (int a=0; a<2; ++a)
            for (int b=0; b<3; ++b)
                for (BOOST_AUTO(s, (*X)[a][b].begin()); s != (*X)[a][b].end(); ++s)
                    for (BOOST_AUTO(t, s->second.begin()); t != s->second.end(); ++t) {
                        integer c = t->second;
                        int d = b&2 ? -1 : b, e = a == 0 ? -1 : a;

                        if (s->first == -1 && t->first+d+e == 0) n += c;
                        (*Y)[a][1][s->first+1][t->first+d] += c;

                        if (s->first == 1 && t->first-d+e == 0) n += c;
                        (*Y)[a][1][s->first-1][t->first-d] += c;

                        if (s->first == 1 && t->first+d-e == 0) n += c;
                        (*Y)[a][2][s->first-1][t->first+d] += c;

                        if (s->first == -1 && t->first-d-e == 0) n += c;
                        (*Y)[a][2][s->first+1][t->first-d] += c;

                        c *= 2;

                        if (s->first == 0 && t->first+d == 0) n += c;
                        (*Y)[a][0][s->first][t->first+d] += c;

                        if (s->first == 0 && t->first-d == 0) n += c;
                        (*Y)[a][0][s->first][t->first-d] += c;
                    }
    }

    delete X;
    delete Y;
}

Це потрібно g++ -O3 kirbyfan64sos.cpp -o kirbyfan64sos -lboost_system -lboost_timer -lboost_chrono -lrt -lgmpскласти. (Спасибі aditsu.)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.