准确快速地计算概率


10

[这是一个精确计算概率的伙伴问题]

这项任务是关于编写代码以准确,快速地计算概率。输出应该是精确的概率,以最简化的形式写为分数。那是它永远不应该输出4/8,而应该1/2

对于某个正整数n,请考虑一个长度为1s和-1s的均匀随机字符串,n并将其称为A。现在将A其串联为第一个值。就是说A[1] = A[n+1]从1 A开始的索引 现在具有length 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]。在这种情况下,两个内积是02

您的代码必须输出两个内积均为零的概率。

复制MartinBüttner制作的表,我们得到以下示例结果。

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 = 64Python中。Mitch Schwartz的第1版
  • n = 106Python中。版本Mitch Schwartz 2015年6月11日
  • n = 151C ++中。米奇·施瓦茨港的答案by kirbyfan64sos
  • n = 165Python中。版本2015年6月11日,是Mitch Schwartz的“修剪”版本 N_MAX = 165
  • n = 945在Min_25中使用精确公式在Python中编写。惊人!
  • n = 1228由Mitch Schwartz 在Python中使用另一个精确公式(基于Min_25的先前答案)。
  • n = 2761Mitch Schwartz 在Python中使用相同的精确公式的更快实现。
  • n = 3250Python中使用Mitch Schwartz的Pypy使用相同的实现。此分数需要pypy MitchSchwartz-faster.py |tail避免控制台滚动开销。

我想知道numpy解决方案的运行速度是否会比Boost C ++快?
qwr

@qwr我认为numpy,numba和cython都将很有趣,因为它们属于Python家族。

2
我希望看到更多这些最快的代码问题
qwr 2015年

@qwr是我最喜欢的问题……谢谢!面临的挑战是要找到一种不仅仅涉及可以找到的最低层语言来编码完全相同的算法的算法。

您是将结果写入控制台还是文件?对我来说,使用pypy写入文件似乎是最快的。控制台会大大减慢该过程。
gnibbler 2015年

Answers:


24

蟒蛇

的封闭式公式p(n)

在此处输入图片说明

的指数生成函数p(n)

在此处输入图片说明

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 2015年

1
@Lembik:我添加了一个(粗略的)证明。
2015年

1
@qwr:谢谢。我也这么认为 !
2015年

1
@ mbomb007:是的。但是,这是一个实现任务,而不是计算任务。因此,我不会用C ++编写代码。
2015年

9

蟒蛇

注意:祝贺Min_25找到一个封闭形式的解决方案!

感谢您提出有趣的问题!可以使用DP来解决,尽管我目前并不很想优化速度以获得更高的分数。高尔夫可能会很好。

N=39在运行Python 2.7.5的旧笔记本电脑上,代码在10秒内到达。

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是的第一个元素Ab是最后一个元素Bs是的内积A[:-1]B,并且t是的内积A[1:-1]B[:-1],使用Python切片表示法。我的代码没有存储数组AB任何地方,因此我使用这些字母分别引用要添加到A和的下一个元素B。这个变量命名选择使解释有些尴尬,但允许A*b+a*B代码本身看起来不错。请注意,要添加到的元素A是倒数第二个元素,因为最后一个元素始终与第一个元素相同。我已经使用了包括马丁布特内尔的惯用伎俩0两次B候选人以获得适当的概率分布。字典X(名为YN+1)保持根据所述元组的值跟踪所有可能的阵列的计数。变量nd代表分子和分母,这就是为什么我n将问题语句的重命名为的原因N

逻辑的关键部分是您可以仅使用元组中的值从N进行更新N+1。问题中指定的两个内积由s+A*B和给出t+A*b+a*B。如果稍微检查一下定义,这很明显;请注意,[A,a][b,B]分别是array A和的最后两个元素B

请注意,st是根据来界定的,并且是有限的N,对于使用快速语言进行的快速实现,我们可以避免使用字典来支持数组。

考虑到仅符号不同的值,可以利用对称性。我没有调查过。

备注1:字典的大小以平方增长N,其中size表示键值对的数量。

备注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加一个的计数加倍0B而不是执行两次这些操作
  • 分母总是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 {cases} \ frac {\ sum _ {i = 0} ^ {\ lfloor n / 2 \ rfloor} \ binom {2i} {i} ^ 2 \ binom {n} {2i} 4 ^ {n-2i}} {8 ^ n}&n \ text {奇数} \ \ 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 {偶数} \ \ end {cases}

这是基于Min_25的Python实现:

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种方式]

其中,xy代表两个点的产品,我们都指望路数,从移动(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:]mBBABABB

现在,我们只需要证明几乎没有允许的对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)的公式甚至等于nm的a(n,m)= {2 *(n选择m); 奇偶校验为0。}只允许一个奇偶校验,因为A [i]!= A [i + 1]必须出现偶数次,因此A [0] = A [n]。因数2是由于初始选择A [0] = 1或A [0] = -1而引起的。

一旦将(A [i]!= A [i + 1])的数目固定为q(在c(n)公式中命名为i),它将分为长度为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 ++

只是Mitch Schwartz的(出色)Python答案的一部分。主要的区别是,我使用的2表示-1a变量,做类似的东西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。)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.