Computer: fai i conti


13

Questa sfida è in parte una sfida di algoritmi, comporta un po 'di matematica ed è in parte semplicemente una sfida di codice più veloce.

Per qualche intero positivo n, si consideri una stringa uniformemente casuale di 1s e 0S di lunghezza ne chiamare A. Ora considera anche una seconda stringa casuale di lunghezza scelta in modo uniforme i ncui valori sono -1, 0,o 1e chiamala B_pre. Ora lascia Bessere B_pre+ B_pre. Questo è B_preconcatenato a se stesso.

Ora considera il prodotto interno di Ae B[j,...,j+n-1]e chiamalo Z_je indicizza da 1.

Compito

L'output dovrebbe essere un elenco di n+1frazioni. Il itermine ° nella uscita dovrebbe essere il esatto probabilità che tutti i primi itermini Z_jcon j <= iuguale 0.

Punto

Il più grande nper il quale il tuo codice fornisce l'output corretto in meno di 10 minuti sulla mia macchina.

Tie Breaker

Se due risposte hanno lo stesso punteggio, vince la prima inviata.

Nel caso (molto) improbabile che qualcuno trovi un metodo per ottenere punteggi illimitati, verrà accettata la prima prova valida di tale soluzione.

Suggerimento

Non cercare di risolvere matematicamente questo problema, è troppo difficile. Il modo migliore dal mio punto di vista è tornare alle definizioni di base della probabilità dal liceo e trovare modi intelligenti per ottenere dal codice un elenco esaustivo delle possibilità.

Lingue e biblioteche

Puoi usare qualsiasi lingua che abbia un compilatore / interprete / ecc. Liberamente disponibile. per Linux e tutte le librerie che sono anche disponibili gratuitamente per Linux.

La mia macchina I tempi verranno eseguiti sulla mia macchina. Questa è un'installazione ubuntu standard su un processore a otto core AMD FX-8350. Questo significa anche che devo essere in grado di eseguire il tuo codice. Di conseguenza, utilizzare solo software gratuito facilmente disponibile e includere istruzioni complete su come compilare ed eseguire il codice.


Alcune uscite di prova. Considera solo il primo output per ciascuno n. Questo è quando i=1. Per n1 a 13 dovrebbero essere.

 1: 4/6
 2: 18/36
 3: 88/216
 4: 454/1296
 5: 2424/7776
 6: 13236/46656
 7: 73392/279936
 8: 411462/1679616
 9: 2325976/10077696
10: 13233628/60466176
11: 75682512/362797056
12: 434662684/2176782336
13: 2505229744/13060694016

È inoltre possibile trovare una formula generale per i=1a http://oeis.org/A081671 .

Classifica (suddivisa per lingua)

  • n = 15. Python + parallel python + pypy in 1min49s di Jakube
  • n = 17. C ++ in 3min37s di Keith Randall
  • n = 16. C ++ in 2min38s di kuroi neko

1
@Knerd Come posso dire di no. Proverò a capire come eseguire il codice in Linux, ma ogni aiuto è molto apprezzato.

Ok, scusa per l'eliminazione dei commenti. Per tutto ciò che non ha letto, è stato se F # o C # sono ammessi :)
Knerd

L'altra domanda di nuovo, hai forse un esempio di output di input valido?
Knerd,

Qual è la tua scheda grafica? Sembra un lavoro per una GPU.
Michael M.,

1
@Knerd Ho aggiunto invece una tabella di probabilità alla domanda. Spero sia utile

Risposte:


5

C ++, n = 18 in 9 min su 8 thread

(Fammi sapere se arriva a meno di 10 minuti sulla tua macchina.)

Approfitto di diverse forme di simmetria nell'array B. Questi sono ciclici (spostamento di una posizione), inversione (inverti l'ordine degli elementi) e segno (prendi il negativo di ciascun elemento). Per prima cosa computo l'elenco dei Bs che dobbiamo provare e il loro peso. Quindi ogni B viene eseguito attraverso una routine veloce (usando le istruzioni bitcount) per tutti i 2 ^ n valori di A.

Ecco il risultato per n == 18:

> time ./a.out 18
 1: 16547996212044 / 101559956668416
 2:  3120508430672 / 101559956668416
 3:   620923097438 / 101559956668416
 4:   129930911672 / 101559956668416
 5:    28197139994 / 101559956668416
 6:     6609438092 / 101559956668416
 7:     1873841888 / 101559956668416
 8:      813806426 / 101559956668416
 9:      569051084 / 101559956668416
10:      510821156 / 101559956668416
11:      496652384 / 101559956668416
12:      493092812 / 101559956668416
13:      492186008 / 101559956668416
14:      491947940 / 101559956668416
15:      491889008 / 101559956668416
16:      449710584 / 101559956668416
17:      418254922 / 101559956668416
18:      409373626 / 101559956668416

real    8m55.854s
user    67m58.336s
sys 0m5.607s

Compilare il programma seguente con g++ --std=c++11 -O3 -mpopcnt dot.cc

#include <stdio.h>
#include <stdlib.h>
#include <vector>
#include <thread>
#include <mutex>
#include <chrono>

using namespace std;

typedef long long word;

word n;

void inner(word bpos, word bneg, word w, word *cnt) {
    word maxi = n-1;
    for(word a = (1<<n)-1; a >= 0; a--) {
        word m = a;
        for(word i = maxi; i >= 0; i--, m <<= 1) {
            if(__builtin_popcount(m&bpos) != __builtin_popcount(m&bneg))
                break;
            cnt[i]+=w;
        }
    }
}

word pow(word n, word e) {
    word r = 1;
    for(word i = 0; i < e; i++) r *= n;
    return r;
}

typedef struct {
    word b;
    word weight;
} Bentry;

mutex block;
Bentry *bqueue;
word bhead;
word btail;
word done = -1;

word maxb;

// compute -1*b
word bneg(word b) {
    word w = 1;
    for(word i = 0; i < n; i++, w *= 3) {
        word d = b / w % 3;
        if(d == 1)
            b += w;
        if(d == 2)
            b -= w;
    }
    return b;
}

// rotate b one position
word brot(word b) {
    b *= 3;
    b += b / maxb;
    b %= maxb;
    return b;
}

// reverse b
word brev(word b) {
    word r = 0;
    for(word i = 0; i < n; i++) {
        r *= 3;
        r += b % 3;
        b /= 3;
    }
    return r;
}

// individual thread's work routine
void work(word *cnt) {
    while(true) {
        // get a queue entry to work on
        block.lock();
        if(btail == done) {
            block.unlock();
            return;
        }
        if(bhead == btail) {
            block.unlock();
            this_thread::sleep_for(chrono::microseconds(10));
            continue;
        }
        word i = btail++;
        block.unlock();

        // thread now owns bqueue[i], work on it
        word b = bqueue[i].b;
        word w = 1;
        word bpos = 0;
        word bneg = 0;
        for(word j = 0; j < n; j++, b /= 3) {
            word d = b % 3;
            if(d == 1)
                bpos |= 1 << j;
            if(d == 2)
                bneg |= 1 << j;
        }
        bpos |= bpos << n;
        bneg |= bneg << n;
        inner(bpos, bneg, bqueue[i].weight, cnt);
    }
}

int main(int argc, char *argv[]) {
    n = atoi(argv[1]);

    // allocate work queue
    maxb = pow(3, n);
    bqueue = (Bentry*)(malloc(maxb*sizeof(Bentry)));

    // start worker threads
    word procs = thread::hardware_concurrency();
    vector<thread> threads;
    vector<word*> counts;
    for(word p = 0; p < procs; p++) {
        word *cnt = (word*)calloc(64+n*sizeof(word), 1);
        threads.push_back(thread(work, cnt));
        counts.push_back(cnt);
    }

    // figure out which Bs we actually want to test, and with which weights
    bool *bmark = (bool*)calloc(maxb, 1);
    for(word i = 0; i < maxb; i++) {
        if(bmark[i]) continue;
        word b = i;
        word w = 0;
        for(word j = 0; j < 2; j++) {
            for(word k = 0; k < 2; k++) {
                for(word l = 0; l < n; l++) {
                    if(!bmark[b]) {
                        bmark[b] = true;
                        w++;
                    }
                    b = brot(b);
                }
                b = bneg(b);
            }
            b = brev(b);
        }
        bqueue[bhead].b = i;
        bqueue[bhead].weight = w;
        block.lock();
        bhead++;
        block.unlock();
    }
    block.lock();
    done = bhead;
    block.unlock();

    // add up results from threads
    word *cnt = (word*)calloc(n,sizeof(word));
    for(word p = 0; p < procs; p++) {
        threads[p].join();
        for(int i = 0; i < n; i++) cnt[i] += counts[p][i];
    }
    for(word i = 0; i < n; i++)
        printf("%2lld: %14lld / %14lld\n", i+1, cnt[n-1-i], maxb<<n);
    return 0;
}

Bene, questo mi dispensa dal lavorare ulteriormente sul mio mostro domestico ...

Grazie per questo. Hai l'attuale iscrizione vincente. Dobbiamo ricordare di -pthreadnuovo. Arrivo n=17sulla mia macchina.

Oops .. Avresti dovuto ottenere la generosità. Scusa se ho perso la scadenza.

@Lembik: nessun problema.
Keith Randall,

6

Python 2 usando pypy e pp: n = 15 in 3 minuti

Anche solo una semplice forza bruta. Interessante vedere che ho quasi la stessa velocità di kuroi neko con C ++. Il mio codice può raggiungere n = 12in circa 5 minuti. E lo eseguo solo su un core virtuale.

modifica: riduce lo spazio di ricerca di un fattore di n

Ho notato, che un vettore ciclato A*di Aproduce gli stessi numeri probabilità (stessi numeri) come vettore originale Aquando iterate sopra B. Es Il vettore (1, 1, 0, 1, 0, 0)ha le stesse probabilità quanto ciascuno dei vettori (1, 0, 1, 0, 0, 1), (0, 1, 0, 0, 1, 1), (1, 0, 0, 1, 1, 0), (0, 0, 1, 1, 0, 1)e (0, 1, 1, 0, 1, 0)quando si sceglie una casuale B. Pertanto non devo ripetere ciascuno di questi 6 vettori, ma solo circa 1 e sostituirlo count[i] += 1con count[i] += cycle_number.

Questo riduce la complessità da Theta(n) = 6^na Theta(n) = 6^n / n. Pertanto n = 13, è circa 13 volte più veloce della mia versione precedente. Calcola n = 13in circa 2 minuti e 20 secondi. Perché n = 14è ancora un po 'troppo lento. Ci vogliono circa 13 minuti.

modifica 2: programmazione multi-core

Non molto contento del prossimo miglioramento. Ho deciso di provare anche a eseguire il mio programma su più core. Sui miei 2 + 2 core ora posso calcolare n = 14in circa 7 minuti. Solo un fattore di miglioramento 2.

Il codice è disponibile in questo repository github: Link . La programmazione multi-core rende un po 'brutta.

modifica 3: riduzione dello spazio di ricerca per Avettori e Bvettori

Ho notato la stessa simmetria speculare per i vettori Adi Kuroi Neko. Ancora non sono sicuro, perché questo funziona (e se funziona per ciascuno n).

La riduzione dello spazio di ricerca per i Bvettori è un po 'più intelligente. Ho sostituito la generazione dei vettori ( itertools.product), con una propria funzione. Fondamentalmente, inizio con un elenco vuoto e lo metto in pila. Fino a quando lo stack è vuoto, rimuovo un elenco, se non ha la stessa lunghezza di n, creo altri 3 elenchi (aggiungendo -1, 0, 1) e inserendoli nello stack. Se un elenco ha la stessa lunghezza di n, posso valutare le somme.

Ora che ho generato i vettori da solo, posso filtrarli a seconda che riesca a raggiungere la somma = 0 o meno. Ad esempio, se il mio vettore Aè (1, 1, 1, 0, 0), e il mio vettore Bsembra (1, 1, ?, ?, ?), lo so, che non posso riempire i ?valori, quindi A*B = 0. Quindi non devo ripetere tutti i 6 vettori Bdel modulo (1, 1, ?, ?, ?).

Possiamo migliorare su questo, se ignoriamo i valori per 1. Come notato nella domanda, i valori per i = 1sono la sequenza A081671 . Esistono molti modi per calcolarli. Ho scelto il semplice ripetersi: a(n) = (4*(2*n-1)*a(n-1) - 12*(n-1)*a(n-2)) / n. Dal momento che possiamo calcolare i = 1praticamente in pochissimo tempo, possiamo filtrare più vettori per B. Ad esempio, A = (0, 1, 0, 1, 1)e B = (1, -1, ?, ?, ?). Possiamo ignorare i vettori, in cui il primo ? = 1, perché il A * cycled(B) > 0, per tutti questi vettori. Spero che tu possa seguire. Probabilmente non è il miglior esempio.

Con questo posso calcolare n = 15in 6 minuti.

modifica 4:

La grande idea di kuroi neko implementata rapidamente, che dice ciò Be -Bproduce gli stessi risultati. Speedup x2. L'implementazione è solo un trucco rapido, però. n = 15in 3 minuti.

Codice:

Per il codice completo visita Github . Il seguente codice è solo una rappresentazione delle caratteristiche principali. Ho lasciato fuori le importazioni, la programmazione multicore, la stampa dei risultati, ...

count = [0] * n
count[0] = oeis_A081671(n)

#generating all important vector A
visited = set(); todo = dict()
for A in product((0, 1), repeat=n):
    if A not in visited:
        # generate all vectors, which have the same probability
        # mirrored and cycled vectors
        same_probability_set = set()
        for i in range(n):
            tmp = [A[(i+j) % n] for j in range(n)]
            same_probability_set.add(tuple(tmp))
            same_probability_set.add(tuple(tmp[::-1]))
        visited.update(same_probability_set)
        todo[A] = len(same_probability_set)

# for each vector A, create all possible vectors B
stack = []
for A, cycled_count in dict_A.iteritems():
    ones = [sum(A[i:]) for i in range(n)] + [0]
    # + [0], so that later ones[n] doesn't throw a exception
    stack.append(([0] * n, 0, 0, 0, False))

    while stack:
        B, index, sum1, sum2, used_negative = stack.pop()
        if index < n:
            # fill vector B[index] in all possible ways,
            # so that it's still possible to reach 0.
            if used_negative:
                for v in (-1, 0, 1):
                    sum1_new = sum1 + v * A[index]
                    sum2_new = sum2 + v * A[index - 1 if index else n - 1]
                    if abs(sum1_new) <= ones[index+1]:
                        if abs(sum2_new) <= ones[index] - A[n-1]:
                            C = B[:]
                            C[index] = v
                            stack.append((C, index + 1, sum1_new, sum2_new, True))
            else:
                for v in (0, 1):
                    sum1_new = sum1 + v * A[index]
                    sum2_new = sum2 + v * A[index - 1 if index else n - 1]
                    if abs(sum1_new) <= ones[index+1]:
                        if abs(sum2_new) <= ones[index] - A[n-1]:
                            C = B[:]
                            C[index] = v
                            stack.append((C, index + 1, sum1_new, sum2_new, v == 1))
        else:
            # B is complete, calculate the sums
            count[1] += cycled_count  # we know that the sum = 0 for i = 1
            for i in range(2, n):
                sum_prod = 0
                for j in range(n-i):
                    sum_prod += A[j] * B[i+j]
                for j in range(i):
                    sum_prod += A[n-i+j] * B[j]
                if sum_prod:
                    break
                else:
                    if used_negative:
                        count[i] += 2*cycled_count
                    else:
                        count[i] += cycled_count

Uso:

Devi installare pypy (per Python 2 !!!). Il modulo Python parallelo non è portato su Python 3. Quindi devi installare il modulo Python parallelo pp-1.6.4.zip . Estrai, cdnella cartella e chiama pypy setup.py install.

Quindi puoi chiamare il mio programma con

pypy you-do-the-math.py 15

Determinerà automaticamente il numero di CPU. Potrebbero esserci dei messaggi di errore dopo aver terminato il programma, ignorali. n = 16dovrebbe essere possibile sulla tua macchina.

Produzione:

Calculation for n = 15 took 2:50 minutes

 1  83940771168 / 470184984576  17.85%
 2  17379109692 / 470184984576   3.70%
 3   3805906050 / 470184984576   0.81%
 4    887959110 / 470184984576   0.19%
 5    223260870 / 470184984576   0.05%
 6     67664580 / 470184984576   0.01%
 7     30019950 / 470184984576   0.01%
 8     20720730 / 470184984576   0.00%
 9     18352740 / 470184984576   0.00%
10     17730480 / 470184984576   0.00%
11     17566920 / 470184984576   0.00%
12     17521470 / 470184984576   0.00%
13     17510280 / 470184984576   0.00%
14     17507100 / 470184984576   0.00%
15     17506680 / 470184984576   0.00%

Note e idee:

  • Ho un processore i7-4600m con 2 core e 4 thread. Non importa se utilizzo 2 o 4 thread. L'utilizzo della cpu è del 50% con 2 thread e del 100% con 4 thread, ma richiede comunque lo stesso tempo. Non so perché. Ho controllato, che ogni thread ha solo la metà dei dati, quando ci sono 4 thread, ho controllato i risultati, ...
  • Uso molte liste. Python non è abbastanza efficiente nella memorizzazione, devo copiare un sacco di elenchi, ... Quindi ho pensato di usare un numero intero. Potrei usare i bit 00 (per 0) e 11 (per 1) nel vettore A, e i bit 10 (per -1), 00 (per 0) e 01 (per 1) nel vettore B. Per il prodotto di A e B, dovrei solo calcolare A & Be contare i blocchi 01 e 10. Il ciclismo può essere fatto spostando il vettore e usando le maschere, ... In realtà ho implementato tutto questo, lo puoi trovare in alcuni dei miei vecchi impegni su Github. Ma si è scoperto, per essere più lento rispetto alle liste. Immagino che pypy ottimizzi davvero le operazioni dell'elenco.

Sul mio PC la corsa n = 12 impiega 7:25 mentre il mio pezzo di spazzatura in C ++ dura circa 1:23, il che lo rende circa 5 volte più veloce. Con solo due core veri, la mia CPU otterrà qualcosa di simile a un fattore 2,5 rispetto a un'applicazione mono-thread, quindi una vera CPU a 8 core dovrebbe eseguire qualcosa come 3 volte più veloce, e questo non conta con il miglioramento della velocità mono-core di base rispetto il mio invecchiamento i3-2100. Tuttavia, se valga la pena di esaminare tutti questi cerchi in C ++ per affrontare un tempo di calcolo in crescita esponenziale, è discutibile.

Ho la sensazione di codegolf.stackexchange.com/questions/41021/… ... Sarebbe utile la sequenza de Bruijn?
kennytm,

sul multithreading, potresti spremere un po 'di più dei tuoi 2 + 2 core bloccando ogni thread su uno. Il guadagno x2 è dovuto al fatto che lo scheduler si sposta attorno ai thread ogni volta che si sposta un fiammifero nel sistema. Con il blocco del core, probabilmente otterresti invece un guadagno x2.5. Non ho idea se Python consente di impostare l'affinità del processore.

Grazie, lo esaminerò. Ma sono praticamente un principiante nel multithreading.
Jakube,

nbviewer.ipython.org/gist/minrk/5500077 ne parla in parte, sebbene usando uno strumento diverso per il parallelismo.

5

bullo lanoso - C ++ - troppo lento

Bene, dato che un programmatore migliore ha preso l'implementazione del C ++, sto chiamando smettere per questo.

#include <cstdlib>
#include <cmath>
#include <vector>
#include <bitset>
#include <future>
#include <iostream>
#include <iomanip>

using namespace std;

/*
6^^n events will be generated, so the absolute max
that can be counted by a b bits integer is
E(b*log(2)/log(6)), i.e. n=24 for a 64 bits counter

To enumerate 3 possible values of a size n vector we need
E(n*log(3)/log(2))+1 bits, i.e. 39 bits
*/
typedef unsigned long long Counter; // counts up to 6^^24

typedef unsigned long long Benumerator; // 39 bits
typedef unsigned long      Aenumerator; // 24 bits

#define log2_over_log6 0.3869

#define A_LENGTH ((size_t)(8*sizeof(Counter)*log2_over_log6))
#define B_LENGTH (2*A_LENGTH)

typedef bitset<B_LENGTH> vectorB;

typedef vector<Counter> OccurenceCounters;

// -----------------------------------------------------------------
// multithreading junk for CPUs detection and allocation
// -----------------------------------------------------------------
int number_of_CPUs(void)
{
    int res = thread::hardware_concurrency();
    return res == 0 ? 8 : res;
}

#ifdef __linux__
#include <sched.h>
void lock_on_CPU(int cpu)
{
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(cpu, &mask);
    sched_setaffinity(0, sizeof(mask), &mask);
}
#elif defined (_WIN32)
#include <Windows.h>
#define lock_on_CPU(cpu) SetThreadAffinityMask(GetCurrentThread(), 1 << cpu)
#else
// #warning is not really standard, so this might still cause compiler errors on some platforms. Sorry about that.
#warning "Thread processor affinity settings not supported. Performances might be improved by providing a suitable alternative for your platform"
#define lock_on_CPU(cpu)
#endif

// -----------------------------------------------------------------
// B values generator
// -----------------------------------------------------------------
struct Bvalue {
    vectorB p1;
    vectorB m1;
};

struct Bgenerator {
    int n;                 // A length
    Aenumerator stop;      // computation limit
    Aenumerator zeroes;    // current zeroes pattern
    Aenumerator plusminus; // current +1/-1 pattern
    Aenumerator pm_limit;  // upper bound of +1/-1 pattern

    Bgenerator(int n, Aenumerator start=0, Aenumerator stop=0) : n(n), stop(stop)
    {
        // initialize generator so that first call to next() will generate first value
        zeroes    = start - 1;
        plusminus = -1;
        pm_limit  = 0;
    }

    // compute current B value
    Bvalue value(void)
    {
        Bvalue res;
        Aenumerator pm = plusminus;
        Aenumerator position = 1;
        int i_pm = 0;
        for (int i = 0; i != n; i++)
        {
            if (zeroes & position)
            {
                if (i_pm == 0)  res.p1 |= position; // first non-zero value fixed to +1
                else         
                {
                    if (pm & 1) res.m1 |= position; // next non-zero values
                    else        res.p1 |= position;
                    pm >>= 1;
                }
                i_pm++;
            }
            position <<= 1;
        }
        res.p1 |= (res.p1 << n); // concatenate 2 Bpre instances
        res.m1 |= (res.m1 << n);
        return res;
    }

    // next value
    bool next(void)
    {
        if (++plusminus == pm_limit)
        {
            if (++zeroes == stop) return false;
            plusminus = 0;
            pm_limit = (1 << vectorB(zeroes).count()) >> 1;
        }
        return true;
    }

    // calibration: produces ranges that will yield the approximate same number of B values
    vector<Aenumerator> calibrate(int segments)
    {
        // setup generator for the whole B range
        zeroes = 0;
        stop = 1 << n;
        plusminus = -1;
        pm_limit = 0;

        // divide range into (nearly) equal chunks
        Aenumerator chunk_size = ((Aenumerator)pow (3,n)-1) / 2 / segments;

        // generate bounds for zeroes values
        vector<Aenumerator> res(segments + 1);
        int bound = 0;
        res[bound] = 1;
        Aenumerator count = 0;
        while (next()) if (++count % chunk_size == 0) res[++bound] = zeroes;
        res[bound] = stop;
        return res;
    }
};

// -----------------------------------------------------------------
// equiprobable A values merging
// -----------------------------------------------------------------
static char A_weight[1 << A_LENGTH];
struct Agroup {
    vectorB value;
    int     count;
    Agroup(Aenumerator a = 0, int length = 0) : value(a), count(length) {}
};
static vector<Agroup> A_groups;

Aenumerator reverse(Aenumerator n) // this works on N-1 bits for a N bits word
{
    Aenumerator res = 0;
    if (n != 0) // must have at least one bit set for the rest to work
    {
        // construct left-padded reverse value
        for (int i = 0; i != 8 * sizeof(n)-1; i++)
        {
            res |= (n & 1);
            res <<= 1;
            n >>= 1;
        }

        // shift right to elimitate trailing zeroes
        while (!(res & 1)) res >>= 1;
    }
    return res;
}

void generate_A_groups(int n)
{
    static bitset<1 << A_LENGTH> lookup(0);
    Aenumerator limit_A = (Aenumerator)pow(2, n);
    Aenumerator overflow = 1 << n;
    for (char & w : A_weight) w = 0;

    // gather rotation cycles
    for (Aenumerator a = 0; a != limit_A; a++)
    {
        Aenumerator rotated = a;
        int cycle_length = 0;
        for (int i = 0; i != n; i++)
        {
            // check for new cycles
            if (!lookup[rotated])
            {
                cycle_length++;
                lookup[rotated] = 1;
            }

            // rotate current value
            rotated <<= 1;
            if (rotated & overflow) rotated |= 1;
            rotated &= (overflow - 1);
        }

        // store new cycle
        if (cycle_length > 0) A_weight[a] = cycle_length;
    }

    // merge symetric groups
    for (Aenumerator a = 0; a != limit_A; a++)
    {
        // skip already grouped values
        if (A_weight[a] == 0) continue;

        // regroup a symetric pair
        Aenumerator r = reverse(a);
        if (r != a)
        {
            A_weight[a] += A_weight[r];
            A_weight[r] = 0;
        }  
    }

    // generate groups
    for (Aenumerator a = 0; a != limit_A; a++)
    {
        if (A_weight[a] != 0) A_groups.push_back(Agroup(a, A_weight[a]));
    }
}

// -----------------------------------------------------------------
// worker thread
// -----------------------------------------------------------------
OccurenceCounters solve(int n, int index, Aenumerator Bstart, Aenumerator Bstop)
{
    OccurenceCounters consecutive_zero_Z(n, 0);  // counts occurences of the first i terms of Z being 0

    // lock on assigned CPU
    lock_on_CPU(index);

    // enumerate B vectors
    Bgenerator Bgen(n, Bstart, Bstop);
    while (Bgen.next())
    {
        // get next B value
        Bvalue B = Bgen.value();

        // enumerate A vector groups
        for (const auto & group : A_groups)
        {
            // count consecutive occurences of inner product equal to zero
            vectorB sliding_A(group.value);
            for (int i = 0; i != n; i++)
            {
                if ((sliding_A & B.p1).count() != (sliding_A & B.m1).count()) break;
                consecutive_zero_Z[i] += group.count;
                sliding_A <<= 1;
            }
        }
    }
    return consecutive_zero_Z;
}

// -----------------------------------------------------------------
// main
// -----------------------------------------------------------------
#define die(msg) { cout << msg << endl; exit (-1); }

int main(int argc, char * argv[])
{
    int n = argc == 2 ? atoi(argv[1]) : 16; // arbitray value for debugging
    if (n < 1 || n > 24) die("vectors of lenght between 1 and 24 is all I can (try to) compute, guv");

    auto begin = time(NULL);

    // one worker thread per CPU
    int num_workers = number_of_CPUs();

    // regroup equiprobable A values
    generate_A_groups(n);

    // compute B generation ranges for proper load balancing
    vector<Aenumerator> ranges = Bgenerator(n).calibrate(num_workers);

    // set workers to work
    vector<future<OccurenceCounters>> workers(num_workers);
    for (int i = 0; i != num_workers; i++)
    {
        workers[i] = async(
            launch::async, // without this parameter, C++ will decide whether execution shall be sequential or asynchronous (isn't C++ fun?).
            solve, n, i, ranges[i], ranges[i+1]); 
    }

    // collect results
    OccurenceCounters result(n + 1, 0);
    for (auto& worker : workers)
    {
        OccurenceCounters partial = worker.get();
        for (size_t i = 0; i != partial.size(); i++) result[i] += partial[i]*2; // each result counts for a symetric B pair
    }
    for (Counter & res : result) res += (Counter)1 << n; // add null B vector contribution
    result[n] = result[n - 1];                           // the last two probabilities are equal by construction

    auto duration = time(NULL) - begin;

    // output
    cout << "done in " << duration / 60 << ":" << setw(2) << setfill('0') << duration % 60 << setfill(' ')
        << " by " << num_workers << " worker thread" << ((num_workers > 1) ? "s" : "") << endl;
    Counter events = (Counter)pow(6, n);
    int width = (int)log10(events) + 2;
    cout.precision(5);
    for (int i = 0; i <= n; i++) cout << setw(2) << i << setw(width) << result[i] << " / " << events << " " << fixed << (float)result[i] / events << endl;

    return 0;
}

Costruire l'eseguibile

È un sorgente C ++ 11 autonomo che viene compilato senza avvisi e funziona senza problemi:

  • Win7 e MSVC2013
  • Win7 e MinGW - g ++ 4.7
  • Ubuntu & g ++ 4.8 (in una VM VirtualBox con 2 CPU allocate)

Se si compila con g ++, utilizzare: g ++ -O3 -pthread -std = c ++ 11
dimenticarsene -pthreadprodurrà un core dump piacevole e amichevole.

ottimizzazioni

  1. L'ultimo termine Z è uguale al primo (Bpre x A in entrambi i casi), quindi gli ultimi due risultati sono sempre uguali, il che dispensa dal calcolo dell'ultimo valore Z.
    Il guadagno è trascurabile, ma la codifica non costa nulla, quindi potresti anche usarlo.

  2. Come ha scoperto Jakube, tutti i valori ciclici di un dato vettore A producono le stesse probabilità.
    Puoi calcolarli con una singola istanza di A e moltiplicare il risultato per il numero delle sue possibili rotazioni. I gruppi di rotazione possono essere facilmente pre-calcolati in un tempo trascurabile, quindi si tratta di un enorme guadagno di velocità netto.
    Poiché il numero di permutazioni di un vettore n lunghezza è n-1, la complessità scende da o (6 n ) a o (6 n / (n-1)), sostanzialmente facendo un passo avanti per lo stesso tempo di calcolo.

  3. Sembra che coppie di schemi simmetrici generino anche le stesse probabilità. Ad esempio, 100101 e 101001.
    Non ne ho alcuna prova matematica, ma intuitivamente quando presentato con tutti i possibili modelli B, ogni valore A simmetrico verrà contorto con il corrispondente valore B simmetrico per lo stesso risultato globale.
    Ciò consente di raggruppare altri vettori A, per una riduzione approssimativa del 30% del numero di gruppi A.

  4. SBAGLIATO Per qualche motivo semi-misterioso, tutti i motivi con solo uno o due bit impostati producono lo stesso risultato. Ciò non rappresenta molti gruppi distinti, ma possono comunque essere uniti praticamente a costo zero.

  5. I vettori B e -B (B con tutti i componenti moltiplicati per -1) producono le stesse probabilità.
    (ad esempio [1, 0, -1, 1] e [-1, 0, 1, -1]).
    Ad eccezione del vettore null (tutti i componenti uguali a 0), B e -B formano una coppia di vettori distinti .
    Ciò consente di dimezzare il numero di valori B considerando solo una di ciascuna coppia e moltiplicando il suo contributo per 2, aggiungendo il contributo globale noto di null B a ciascuna probabilità una sola volta.

Come funziona

Il numero di valori B è enorme (3 n ), quindi il pre-calcolo richiederebbe quantità indecenti di memoria, che rallenterebbe il calcolo e alla fine esaurirebbe la RAM disponibile.
Sfortunatamente, non sono riuscito a trovare un modo semplice per enumerare il mezzo set di valori B ottimizzati, quindi ho fatto ricorso alla codifica di un generatore dedicato.

Il potente generatore di B è stato molto divertente da codificare, anche se i linguaggi che supportano i meccanismi di rendimento avrebbero permesso di programmarlo in un modo molto più elegante.
Quello che fa in poche parole è considerare lo "scheletro" di un vettore Bpre come un vettore binario in cui 1 rappresenta i valori effettivi -1 o +1.
Tra tutti questi valori potenziali + 1 / -1, il primo è fisso su +1 (selezionando quindi uno dei possibili vettore B / -B) e vengono elencate tutte le rimanenti possibili combinazioni + 1 / -1.
Infine, un semplice sistema di calibrazione assicura che ogni thread di lavoro elabori un intervallo di valori approssimativamente della stessa dimensione.

I valori sono fortemente filtrati per il raggruppamento in blocchi equiprobabili.
Questo viene fatto in una fase di pre-calcolo che la forza bruta esamina tutti i possibili valori.
Questa parte ha un tempo di esecuzione O (2 n ) trascurabile e non ha bisogno di essere ottimizzata (il codice è già abbastanza illeggibile così com'è!).

Per valutare il prodotto interno (che deve essere testato solo contro zero), i componenti -1 e 1 di B sono raggruppati in vettori binari.
Il prodotto interno è nullo se (e solo se) esiste un numero uguale di + 1s e -1s tra i valori B corrispondenti a valori A diversi da zero.
Questo può essere calcolato con semplici operazioni di mascheramento e conteggio dei bit, aiutato da std::bitsetciò produrrà un codice di conteggio dei bit ragionevolmente efficiente senza dover ricorrere a brutte istruzioni intrinseche.

Il lavoro è equamente diviso tra i core, con affinità forzata della CPU (ogni piccolo aiuto, o almeno così si dice).

Risultato di esempio

C:\Dev\PHP\_StackOverflow\C++\VectorCrunch>release\VectorCrunch.exe 16
done in 8:19 by 4 worker threads
 0  487610895942 / 2821109907456 0.17284
 1   97652126058 / 2821109907456 0.03461
 2   20659337010 / 2821109907456 0.00732
 3    4631534490 / 2821109907456 0.00164
 4    1099762394 / 2821109907456 0.00039
 5     302001914 / 2821109907456 0.00011
 6     115084858 / 2821109907456 0.00004
 7      70235786 / 2821109907456 0.00002
 8      59121706 / 2821109907456 0.00002
 9      56384426 / 2821109907456 0.00002
10      55686922 / 2821109907456 0.00002
11      55508202 / 2821109907456 0.00002
12      55461994 / 2821109907456 0.00002
13      55451146 / 2821109907456 0.00002
14      55449098 / 2821109907456 0.00002
15      55449002 / 2821109907456 0.00002
16      55449002 / 2821109907456 0.00002

Prestazioni

Il multithreading dovrebbe funzionare perfettamente su questo, anche se solo i core "veri" contribuiranno pienamente alla velocità di calcolo. La mia CPU ha solo 2 core per 4 CPU e il guadagno rispetto a una versione a thread singolo è "solo" di circa 3,5.

I compilatori

Un primo problema con il multithreading mi ha portato a credere che i compilatori GNU stessero funzionando peggio di Microsoft.

Dopo test più approfonditi, sembra che g ++ vinca ancora una volta il giorno, producendo un codice approssimativamente più veloce del 30% (lo stesso rapporto che ho notato su altri due progetti pesanti di calcolo).

In particolare, la std::bitsetlibreria è implementata con istruzioni di conteggio dei bit dedicate da g ++ 4.8, mentre MSVC 2013 utilizza solo loop di bit shift tradizionali.

Come ci si poteva aspettare, la compilazione in 32 o 64 bit non fa differenza.

Ulteriori perfezionamenti

Ho notato alcuni gruppi A che producono le stesse probabilità dopo tutte le operazioni di riduzione, ma non sono riuscito a identificare un modello che consentirebbe di raggrupparli.

Ecco le coppie che ho notato per n = 11:

  10001011 and 10001101
 100101011 and 100110101
 100101111 and 100111101
 100110111 and 100111011
 101001011 and 101001101
 101011011 and 101101011
 101100111 and 110100111
1010110111 and 1010111011
1011011111 and 1011111011
1011101111 and 1011110111

Penso che le ultime due probabilità dovrebbero essere sempre le stesse. Questo perché il prodotto interno n + 1 è in realtà lo stesso del primo.

Quello che volevo dire era che i primi n prodotti interni sono zero se e solo se lo sono i primi n + 1. L'ultimo prodotto interno non fornisce alcuna nuova informazione come hai già fatto prima. Quindi il numero di stringhe che danno n zero prodotti è esattamente uguale al numero che dà n + 1 prodotti zero.

Per interesse, cosa stavi calcolando esattamente?

Grazie per l'aggiornamento ma non capisco la riga "0 2160009216 2176782336". Cosa conti esattamente in questo caso? La probabilità che il primo prodotto interno sia zero è molto più piccola di quella.

Potresti dare qualche consiglio su come compilare ed eseguire questo? Ho provato g ++ -Wall -std = c ++ 11 kuroineko.cpp -o kuroineko e ./kuroineko 12 ma dàterminate called after throwing an instance of 'std::system_error' what(): Unknown error -1 Aborted (core dumped)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.