確率を正確かつ迅速に計算する


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 である長さの2番目のランダムな文字列についても検討し、Bと呼びます。

今の内積考えるA[1,...,n]Bとの内積をA[2,...,n+1]してB

たとえば、を検討してくださいn=3。以下のための可能な値Aとは、B可能性A = [-1,1,1,-1]B=[0,1,-1]。この場合、2つの内積は0および2です。

コードは、両方の内積がゼロである確率を出力する必要があります。

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=1nで、別の行にnが増えるたびに正しい出力を与える必要があります。10秒後に停止します。

スコア

スコアはn、コンピューターで実行したときにコードが10秒後に停止する前に到達した最高点です。同点の場合、勝者が最も早く最高スコアに到達する人になります。

エントリの表

  • n = 64Python。Mitch Schwartzによるバージョン1
  • n = 106Python。2015年6月11日バージョン:Mitch Schwartz
  • n = 151C ++。ミッチ・シュワルツ港の回答by kirbyfan64sos
  • n = 165Python。バージョン2015年6月11日、ミッチシュワルツによる「剪定」バージョン N_MAX = 165
  • n = 945正確な数式を使用してMin_25 でPythonで。すごい!
  • n = 1228(Min_25の以前の回答に基づく)別の正確な式を使用したMitch Schwartz によるPythonで
  • n = 2761同じ正確な数式のより高速な実装を使用したMitch Schwartz によるPythonで
  • n = 3250Pythonの使用Pypyを同じ実装を使用ミッチ・シュワルツによって。このスコアはpypy MitchSchwartz-faster.py |tail、コンソールのスクロールのオーバーヘッドを回避する必要があります。

派手なソリューションはBoost C ++よりも速く実行されるのでしょうか?
qwr

@qwr numpy、numba、およびcythonは、Pythonファミリー内で維持されるため、すべて興味深いものになると思います。

2
私はより多くのこれらの最速のコードの問題の見てみたい
QWR

@qwrこれは私のお気に入りの質問です...ありがとうございます!課題は、見つけることができる最低レベルの言語でまったく同じアルゴリズムをコーディングするだけではないものを見つけることです。

結果をコンソールまたはファイルに書き込んでいますか?私にとっては、pypyを使用してファイルに書き込むのが最も速いようです。コンソールはプロセスをかなり遅くします。
gnibbler 2015年

回答:


24

パイソン

の閉形式の式p(n)

ここに画像の説明を入力してください

の指数生成関数p(n)

ここに画像の説明を入力してください

ここI_0(x)で、第1種変形ベッセル関数です。

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()

パリ/ GP

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

証明:
この問題は、2次元(制限付き)のランダムウォーク問題に似ています。

場合は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:(大まかな)証明を追加しました。
Min_25

1
@qwr:ありがとう。私もそう思います !
Min_25

1
@ mbomb007:はい。ただし、これはコンピューティングタスクではなく実装タスクです。そのため、C ++ではコーディングしません。
Min_25

9

パイソン

注:閉形式の解を見つけるためのMin_25におめでとうございます!

興味深い問題をありがとう!それはDPを使用して解決できますが、現在、より高いスコアを取得するために速度を最適化する意欲はあまりありません。ゴルフにいいかも。

N=39Python 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どこにも保存しないので、これらの文字を使用しAB、それぞれとに追加される次の要素を参照します。この変数名の選択により、説明が少し厄介になりますA*b+a*Bが、コード自体の見栄えが良くなります。A最後の要素は常に最初の要素と同じであるため、追加される要素は最後から2番目の要素であることに注意してください。MartinBüttnerの02回のインクルードのトリックを使用しましたB適切な確率分布を得るための候補。辞書X(命名されたYためにN+1)タプルの値に応じて、すべての可能な配列の数を追跡します。変数ndは分子と分母を表しnていNます。そのため、問題のステートメントの名前をに変更しました。

ロジックの重要な部分は、タプルの値のみNN+1使用してからに更新できることです。質問で指定された2つの内積は、s+A*Bおよびによって与えられt+A*b+a*Bます。これは、定義を少し調べれば明らかです。なお、[A,a]及び[b,B]アレイの最後の2つの要素であるAB夫々 。

stは小さく、に従って制限されていることに注意してください。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空のリストから要素を追加するときに、タプルの最初の要素が一貫していることを強制します)
  • 内部ループを展開します。これにより、乗算もなくなります。
  • これらの操作を2回実行する代わりにc、a 0を追加する数をB2倍にします
  • 分母は常にある8^Nので、追跡する必要はありません
  • ここで対称性を考慮に入れます。有効なペアのとのペアはを否定することで1対1に対応できるため、Aas の最初の要素を修正し1、分母をで除算できます。同様に、最初の要素を非負に修正できます。2(A,B)A[1]=1A[1]=-1AB
  • 今剪定で。あなたはN_MAXそれがあなたのマシンで何点得られるかを見るためにいじる必要があるでしょう。N_MAX二分探索により自動的に適切なものを見つけるように書き直すこともできますが、不要なようです。注:周囲N_MAX / 2に到達するまで枝刈り条件を確認する必要がないため、2つのフェーズで繰り返すことで少し速度を上げることができますが、単純さとコードのクリーンさのためにしないようにしました。

1
これは本当に素晴らしい答えです!スピードアップで何をしたのか説明してもらえますか?

@Lembikありがとう:)説明に加えて小さな最適化を追加し、Python3に準拠させました。
ミッチシュワルツ2015年

私のコンピューターでN=57は、最初のバージョンとN=752番目のバージョンを取得しました。
kirbyfan64sos 2015年

あなたの答えは素晴らしいものでした。それは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 {even} \ \ 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-1A[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つの方法]

どこxy2つのドットの製品のために立って、私たちはいくつかの方法から移動するために数えている(0,0)(0,0)nステップ。そのカウントは、またはで始まる可能性が2あるという事実を説明するために乗算されます。A1-1

私たちはゼロ移動(x,y)としてとどまることを参照します

ゼロ以外の動きの数を繰り返しますi。これは、に戻るために均一でなければなりません(0,0)。水平および垂直の動きはによってカウントすることができる2つの独立した一次元のランダムウォーク、作るC(i,i/2)^2、ここで、C(n,k)二項係数です。(kkステップと右ステップのあるウォークの場合、ステップ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)を定義します。2つのドット積が両方ともゼロである場合、ほぼ許容されるペアを定義します。B(A,B)A[n+1] = -A[1]

補題:与えられたnについて、ほぼ許容できるペアは、変換可能なペアと1対1で対応しています。

and を否定することにより(A,B)、(可逆的に)変換可能なペアをほぼ許容できるペアに変換できます。ここで、(A',B')はの最後のゼロのインデックスです。これのチェックは簡単です:の最後の要素がゼロの場合、何もする必要はありません。それ以外の場合、の最後の要素を否定するとき、シフトされた内積の最後の項を保存するために、の最後の要素を否定できます。しかし、これはシフトされていない内積の最後の値を無効にするため、の最後から2番目の要素を無効にすることでこれを修正します。しかし、これにより、シフトされた積の最後から2番目の値が破棄されるため、の最後から2番目の要素を無効にします。など、でゼロ要素に到達するまで。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 [i] = A [i + 1] m回となるようにAを選択する方法の数を表します。a(n、m)の式は、nmでもa(n、m)= {2 *(nはmを選択)と同等です。0はnmの奇数です。} A [i]!= A [i + 1]は偶数回発生する必要があるため、A [0] = A [n]になるため、1つのパリティのみが許可されます。係数2は、最初の選択A [0] = 1またはA [0] = -1によるものです。

(A [i]!= A [i + 1])の数がq(c(n)式ではiという名前)に固定されると、長さqとnqの2つの1Dランダムウォークに分離されます。b(m)は、開始した同じ場所で終了するmステップの1次元ランダムウォークを取得する方法の数で、左に移動する可能性は25%、静止している可能性は50%、確率は25%です。右に移動します。生成関数を示すより明白な方法は、[x ^ m](1 + 2x + x ^ 2)^ nです。ここで、1、2xおよびx ^ 2は、それぞれ左、移動なし、および右を表します。しかし、1 + 2x + x ^ 2 =(x + 1)^ 2となります。


PPCGを愛するもう1つの理由!ありがとうございました。

2

C ++

ミッチ・シュワルツによる(優れた)Python回答の移植版です。主な違いは、変数2を表す-1ために使用し、に対してa同様のbことをしたため、配列を使用できることです。でインテル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に感謝)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.