Рассчитайте вероятность точно и быстро


10

[Это вопрос партнера, чтобы точно рассчитать вероятность ]

Эта задача о написании кода для точного и быстрого вычисления вероятности . Вывод должен быть точной вероятностью, записанной в виде дроби в наиболее сокращенной форме. То есть это никогда не должно выводиться, 4/8а скорее 1/2.

Для некоторого положительного целого числа nрассмотрим равномерно случайную строку длиной 1 с и 1 с nи назовем ее A. Теперь объединяем 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 Min_25 с использованием точной формулы. Удивительно!
  • n = 1228в Python Митч Шварц, используя другую точную формулу (на основе предыдущего ответа Min_25).
  • n = 2761в Python Митч Шварц, используя более быструю реализацию той же точной формулы.
  • n = 3250в Python с использованием Pypy Митч Шварц, используя ту же реализацию. Этот счет должен pypy MitchSchwartz-faster.py |tailизбежать прокрутки консоли.

Интересно, будет ли решение Numpy работать быстрее, чем Boost C ++?
Qwr

@qwr Я думаю, что numpy, numba и cython были бы интересны, так как они принадлежат к семейству Python.

2
Я хотел бы видеть больше этих проблем с быстрым кодом
qwr

@qwr это мой любимый вопрос ... Спасибо! Задача состоит в том, чтобы найти тот, который не просто включает в себя кодирование точно такого же алгоритма на языке самого низкого уровня, который вы можете найти.

Вы записываете результаты в консоль или в файл? Использование pypy и запись в файл кажется мне самым быстрым. Консоль значительно замедляет процесс.
Гнибблер

Ответы:


24

питон

Замкнутая форма формула p(n)IS

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

Экспоненциальная производящая функция p(n)IS

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

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

Изменить на 2015-06-11:
- обновлен код Python.

Изменить на 2015-06-13:
- добавлено доказательство вышеуказанной формулы.
- исправил 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: я добавил (грубое) доказательство.
Min_25

1
@qwr: Спасибо. Я тоже так думаю !
Min_25

1
@ mbomb007: Да. Но это задача реализации, а не вычислительная задача. Так что я бы не стал писать код на C ++.
Min_25

9

питон

Примечание: Поздравляем Min_25 с поиском решения в закрытой форме!

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

Код достиг N=3910 секунд на этом старом ноутбуке с 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поэтому нам не нужно следить за ним
  • Теперь, принимая во внимание симметрию: мы можем зафиксировать первый элемент Aas 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) = \ begin {case} \ frac {\ sum _ {i = 0} ^ {\ lfloor n / 2 \ rfloor} \ binom {2i} {i} ^ 2 \ binom {n} {2i} 4 ^ {n-2i}} {8 ^ n} & n \ text {odd} \ \ frac {\ binom {n} {n / 2} ^ 2 + \ sum _ {i = 0} ^ {\ lfloor n / 2 \ rfloor} \ binom {2i} {i} ^ 2 \ binom {n} {2i} 4 ^ {n-2i}} {8 ^ n} & n \ text {even} \ \ end {case}

Вот реализация 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

Экспликация формулы Min_25

Min_25 опубликовал отличное доказательство, но это заняло некоторое время. Это небольшое объяснение, чтобы заполнить между строк.

a (n, m) представляет количество способов выбора A, таких что A [i] = A [i + 1] m раз. Формула для a (n, m) эквивалентна a (n, m) = {2 * (n выберите m) для четного nm; 0 для нм нечетное.} Допускается только одна четность, потому что 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)), оно разделяется на два одномерных случайных блуждания длины 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 ++

Просто порт (превосходного) ответа Питона Митча Шварца. Основное отличие заключается в том , что я использовал 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.