Computador: você faz as contas


13

Esse desafio é parcialmente um desafio de algoritmos, envolve alguma matemática e é parcialmente simplesmente um desafio de código mais rápido.

Para algum número inteiro positivo n, considere uma sequência uniformemente aleatória de 1s e 0s de comprimento ne chame-a A. Agora considere também uma segunda sequência de comprimento aleatória, escolhida uniformemente, ncujos valores sejam -1, 0,ou 1e chame-a B_pre. Agora Bseja B_pre+ B_pre. Isso éB_pre concatenado para si mesmo.

Agora considere o produto interno de Ae B[j,...,j+n-1]e chamá-loZ_j e índice de 1.

Tarefa

A saída deve ser uma lista de n+1frações. O itermo th na saída deve ser a exata probabilidade de que todos dos primeiros itermos Z_jcom j <= iigual 0.

Ponto

O maior npara o qual o seu código fornece a saída correta em menos de 10 minutos na minha máquina.

Desempate

Se duas respostas tiverem a mesma pontuação, a primeira submetida vence.

No evento (muito, muito improvável) de alguém encontrar um método para obter pontuações ilimitadas, a primeira prova válida de tal solução será aceita.

Sugestão

Não tente resolver esse problema matematicamente, é muito difícil. A melhor maneira, na minha opinião, é voltar às definições básicas de probabilidade do ensino médio e encontrar maneiras inteligentes de obter o código para fazer uma enumeração exaustiva das possibilidades.

Línguas e bibliotecas

Você pode usar qualquer idioma que tenha um compilador / intérprete disponível gratuitamente / etc. para Linux e quaisquer bibliotecas que também estão disponíveis gratuitamente para Linux.

Minha máquina Os horários serão executados na minha máquina. Esta é uma instalação padrão do ubuntu em um processador AMD FX-8350 Eight-Core. Isso também significa que eu preciso poder executar seu código. Como conseqüência, use apenas software livre facilmente disponível e inclua instruções completas sobre como compilar e executar seu código.


Algumas saídas de teste. Considere apenas a primeira saída para cada um n. Isso é quando i=1. Pois nde 1 a 13 eles deveriam ser.

 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

Você também pode encontrar uma fórmula geral para i=1a http://oeis.org/A081671 .

Cabeçalho (dividido por idioma)

  • n = 15. Python + python paralelo + pypy em 1min49s por Jakube
  • n = 17. C ++ em 3min37s por Keith Randall
  • n = 16. C ++ em 2min38s por kuroi neko

1
@ Knerd Como posso dizer não. Vou tentar descobrir como executar seu código no Linux, mas qualquer ajuda muito apreciada.

Ok, desculpe por excluir comentários. Por tudo o que não leu, foi se F # ou C # são permitidos :)
Knerd

A outra pergunta novamente, você talvez tenha um exemplo de uma saída de entrada válida?
Knerd

Qual é a sua placa gráfica? Parece um trabalho para uma GPU.
Michael M.

1
@ Knerd Adicionei uma tabela de probabilidades à pergunta. Espero que seja útil.

Respostas:


5

C ++, n = 18 em 9 min em 8 threads

(Deixe-me saber se ele leva menos de 10 minutos na sua máquina.)

Aproveito várias formas de simetria na matriz B. Esses são cíclicos (deslocamento por uma posição), reversão (inverta a ordem dos elementos) e sinal (aceite o negativo de cada elemento). Primeiro, eu calculo a lista de Bs que precisamos tentar e seu peso. Então cada B é executado através de uma rotina rápida (usando instruções de contagem de bits) para todos os 2 ^ n valores de A.

Aqui está o resultado para 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

Compile o programa abaixo com 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;
}

Bom, que dispensa-me de trabalhar ainda mais em meu próprio monstro de estimação ...

Obrigado por isso. Você tem a entrada vencedora atual. Temos que lembrar -pthreadnovamente. Eu chego n=17na minha máquina.

Opa .. Você deveria ter recebido a recompensa completa. Desculpe, perdi o prazo.

@ Lembik: não há problema.
Keith Randall

6

Python 2 usando pypy e pp: n = 15 em 3 minutos

Também apenas uma força bruta simples. Interessante ver que quase chego à mesma velocidade que o kuroi neko com C ++. Meu código pode alcançarn = 12 em cerca de 5 minutos. E eu apenas o executo em um núcleo virtual.

editar: reduz o espaço de pesquisa por um fator de n

I notado, que um vector ciclado A*de Aproduz os mesmos números como probabilidades (mesmos números), o vector inicial Aquando iterar B. Por exemplo, o vector (1, 1, 0, 1, 0, 0)tem as mesmas probabilidades de cada um dos vectores (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 se escolhe uma forma aleatória B. Portanto, não tenho que iterar sobre cada um desses 6 vetores, mas apenas cerca de 1 e substituir count[i] += 1por count[i] += cycle_number.

Isso reduz a complexidade de Theta(n) = 6^npara Theta(n) = 6^n / n. Portanto n = 13, é cerca de 13 vezes mais rápido que a minha versão anterior. Calcula n = 13em cerca de 2 minutos e 20 segundos. Pois n = 14ainda é um pouco lento demais. Demora cerca de 13 minutos.

editar 2: Programação multi-core

Não estou muito feliz com a próxima melhoria. Decidi também tentar executar meu programa em vários núcleos. Nos meus núcleos 2 + 2, agora posso calcular n = 14em cerca de 7 minutos. Apenas um fator de 2 melhorias.

O código está disponível neste repositório do github: Link . A programação de múltiplos núcleos é um pouco feia.

edit 3: Reduzindo o espaço de pesquisa para Avetores e Bvetores

Notei a mesma simetria de espelho para os vetores Aque kuroi neko. Ainda não tenho certeza, por que isso funciona (e se funciona para cada um n).

A redução do espaço de pesquisa de Bvetores é um pouco mais inteligente. Substituí a geração dos vetores ( itertools.product) por uma função própria. Basicamente, começo com uma lista vazia e a coloco em uma pilha. Até que a pilha esteja vazia, removo uma lista, se não tiver o mesmo comprimento que n, gerei outras 3 listas (anexando -1, 0, 1) e empurrando-as para a pilha. Se uma lista tiver o mesmo comprimento n, posso avaliar as somas.

Agora que eu mesmo gero os vetores, posso filtrá-los, dependendo se consigo atingir a soma = 0 ou não. Por exemplo, se meu vetor Aé (1, 1, 1, 0, 0), e meu vetor Bparece (1, 1, ?, ?, ?), eu sei, que não posso preencher os ?valores com, para que A*B = 0. Portanto, não tenho que repetir todos esses 6 vetores Bdo formulário (1, 1, ?, ?, ?).

Podemos melhorar isso, se ignorarmos os valores de 1. Conforme observado na pergunta, os valores de i = 1são a sequência A081671 . Existem muitas maneiras de calcular isso. Eu escolho a recorrência simples: a(n) = (4*(2*n-1)*a(n-1) - 12*(n-1)*a(n-2)) / n. Como podemos calcular i = 1basicamente em pouco tempo, podemos filtrar mais vetores para B. Por exemplo, A = (0, 1, 0, 1, 1)e B = (1, -1, ?, ?, ?). Podemos ignorar vetores, onde o primeiro ? = 1, porque o A * cycled(B) > 0, para todos esses vetores. Espero que você possa acompanhar. Provavelmente não é o melhor exemplo.

Com isso eu posso calcular n = 15em 6 minutos.

editar 4:

Implementou rapidamente a grande ideia de kuroi neko, que diz isso Be -Bproduz os mesmos resultados. Aceleração x2. A implementação é apenas um hack rápido, no entanto. n = 15em 3 minutos.

Código:

Para o código completo, visite Github . O código a seguir é apenas uma representação dos principais recursos. Deixei de fora as importações, a programação multicore, a impressão dos resultados, ...

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:

Você precisa instalar o pypy (para o Python 2 !!!). O módulo python paralelo não é portado para o Python 3. Então você deve instalar o módulo python paralelo pp-1.6.4.zip . Extraia-o cdna pasta e ligue pypy setup.py install.

Então você pode ligar para o meu programa com

pypy you-do-the-math.py 15

Ele determinará automaticamente o número de CPUs. Pode haver algumas mensagens de erro após o término do programa, apenas as ignore. n = 16deve ser possível em sua máquina.

Resultado:

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%

Notas e idéias:

  • Eu tenho um processador i7-4600m com 2 núcleos e 4 threads. Não importa se eu uso 2 ou 4 threads. O uso da CPU é de 50% com 2 threads e 100% com 4 threads, mas ainda leva a mesma quantidade de tempo. Não sei porque. Eu verifiquei, que cada thread tem apenas a metade dos dados, quando existem 4 threads, verifiquei os resultados, ...
  • Eu uso muitas listas. Python não é muito eficiente no armazenamento, eu tenho que copiar muitas listas, ... Então pensei em usar um número inteiro. Eu poderia usar os bits 00 (para 0) e 11 (para 1) no vetor A e os bits 10 (para -1), 00 (para 0) e 01 (para 1) no vetor B. Para o produto de A e B, eu só precisaria calcular A & Be contar os blocos 01 e 10. O ciclismo pode ser feito com a mudança do vetor e o uso de máscaras ... Na verdade, implementei tudo isso, você pode encontrá-lo em alguns dos meus commits mais antigos no Github. Mas acabou sendo mais lento que nas listas. Eu acho que o pypy realmente otimiza as operações da lista.

No meu PC, a execução n = 12 leva 7:25, enquanto o meu lixo C ++ leva cerca de 1:23, o que o torna cerca de 5 vezes mais rápido. Com apenas dois núcleos verdadeiros, minha CPU ganhará algo como um fator de 2,5 em comparação com um aplicativo mono-threaded; portanto, uma verdadeira CPU de 8 núcleos deve ser executada três vezes mais rápido, e isso não conta com a melhoria básica da velocidade do mono-núcleo meu envelhecimento i3-2100. Se vale a pena passar por todos esses bastidores de C ++ para enfrentar um tempo de computação que cresce exponencialmente, o esforço é discutível.

Estou com uma sensação de codegolf.stackexchange.com/questions/41021/… ... A sequência de De Bruijn seria útil?
Kennytm

sobre multithreading, você pode extrair um pouco mais de seus núcleos 2 + 2 bloqueando cada thread em um. O ganho de x2 se deve ao deslocamento do agendador em torno de seus segmentos cada vez que um palito de fósforo é movido no sistema. Com o bloqueio do núcleo, você provavelmente obteria um ganho de x2,5. Porém, não faço ideia se o Python permite definir a afinidade do processador.

Obrigado, vou dar uma olhada. Mas sou praticamente um novato em multithreading.
Jakube

O nbviewer.ipython.org/gist/minrk/5500077 menciona isso, embora use uma ferramenta diferente para o paralelismo.

5

valentão lanoso - C ++ - muito lento

Bem, desde que um programador melhor assumiu a implementação do C ++, estou encerrando esse processo.

#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;
}

Construindo o executável

É uma fonte C ++ 11 autônoma que compila sem avisos e roda sem problemas:

  • Win7 e MSVC2013
  • Win7 e MinGW - g ++ 4.7
  • Ubuntu & g ++ 4.8 (em uma VM VirtualBox com 2 CPUs alocadas)

Se você compilar com o g ++, use: g ++ -O3 -pthread -std = c ++ 11 O
esquecimento -pthreadproduzirá um despejo de núcleo agradável e amigável.

Otimizações

  1. O último termo Z é igual ao primeiro (Bpre x A nos dois casos); portanto, os dois últimos resultados são sempre iguais, o que dispensa o cálculo do último valor Z.
    O ganho é negligenciável, mas codificá-lo não custa nada, portanto você pode usá-lo.

  2. Como Jakube descobriu, todos os valores cíclicos de um determinado vetor A produzem as mesmas probabilidades.
    Você pode computá-los com uma única instância de A e multiplicar o resultado pelo número de possíveis rotações. Os grupos de rotação podem ser facilmente pré-calculados em um período de tempo desprezível, portanto, esse é um enorme ganho de velocidade líquida.
    Como o número de permutações de um vetor de comprimento n é n-1, a complexidade cai de o (6 n ) para o (6 n / (n-1)), basicamente indo um passo além no mesmo tempo de computação.

  3. Parece que pares de padrões simétricos também geram as mesmas probabilidades. Por exemplo, 100101 e 101001.
    Não tenho prova matemática disso, mas, intuitivamente, quando apresentados com todos os padrões B possíveis, cada valor A simétrico será complicado com o valor B simétrico correspondente para o mesmo resultado global.
    Isso permite reagrupar mais alguns vetores A, para uma redução aproximada de 30% do número de grupos A.

  4. ERRADO Por alguma razão semi-misteriosa, todos os padrões com apenas um ou dois bits definidos produzem o mesmo resultado. Isso não representa muitos grupos distintos, mas ainda assim eles podem ser mesclados praticamente sem nenhum custo.

  5. Os vetores B e -B (B com todos os componentes multiplicados por -1) produzem as mesmas probabilidades.
    (por exemplo [1, 0, -1, 1] e [-1, 0, 1, -1]).
    Exceto pelo vetor nulo (todos os componentes iguais a 0), B e -B formam um par de vetores distintos .
    Isso permite reduzir o número de valores B pela metade considerando apenas um de cada par e multiplicando sua contribuição por 2, adicionando a contribuição global conhecida de B nulo a cada probabilidade apenas uma vez.

Como funciona

O número de valores B é enorme (3 n ); portanto, a pré-computação deles exigiria quantidades indecentes de memória, o que tornaria a computação mais lenta e eventualmente esgotaria a RAM disponível.
Infelizmente, não consegui encontrar uma maneira simples de enumerar o meio conjunto de valores B otimizados, por isso recorri à codificação de um gerador dedicado.

O poderoso gerador B foi muito divertido de codificar, embora as linguagens que suportam mecanismos de produção tivessem permitido programá-lo de uma maneira muito mais elegante.
Em resumo, o que ele faz é considerar o "esqueleto" de um vetor Bpre como um vetor binário em que 1s representa valores reais de -1 ou +1.
Entre todos esses valores potenciais + 1 / -1, o primeiro é fixo em +1 (selecionando um dos possíveis vetores B / -B) e todas as combinações possíveis possíveis + 1 / -1 são enumeradas.
Por fim, um sistema simples de calibração garante que cada rosca de trabalho processe uma faixa de valores aproximadamente do mesmo tamanho.

Os valores A são fortemente filtrados para reagrupar em pedaços equiprobáveis.
Isso é feito em uma fase de pré-computação que a força bruta examina todos os valores possíveis.
Esta parte tem um tempo de execução negligenciável de O (2 n ) e não precisa ser otimizada (o código já é ilegível o suficiente!).

Para avaliar o produto interno (que precisa ser testado apenas contra zero), os componentes -1 e 1 de B são reagrupados em vetores binários.
O produto interno é nulo se (e somente se) houver um número igual de + 1s e -1s entre os valores B correspondentes a valores A diferentes de zero.
Isso pode ser calculado com operações simples de mascaramento e contagem de bits, ajudadas por std::bitsetisso produzirão um código de contagem de bits razoavelmente eficiente sem a necessidade de recorrer a instruções intrínsecas feias.

O trabalho é igualmente dividido entre núcleos, com afinidade forçada da CPU (um pouquinho ajuda, ou é o que dizem).

Resultado de exemplo

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

Performances

O multithreading deve funcionar perfeitamente nisso, embora apenas os núcleos "verdadeiros" contribuam totalmente para a velocidade da computação. Minha CPU possui apenas 2 núcleos para 4 CPUs, e o ganho em uma versão de thread único é "apenas" cerca de 3,5.

Compiladores

Um problema inicial com o multithreading me levou a acreditar que os compiladores GNU estavam com desempenho pior que a Microsoft.

Após testes mais detalhados, parece que o g ++ vence o dia mais uma vez, produzindo código aproximadamente 30% mais rápido (a mesma proporção que eu notei em outros dois projetos pesados ​​de computação).

Notavelmente, a std::bitsetbiblioteca é implementada com instruções dedicadas de contagem de bits pelo g ++ 4.8, enquanto o MSVC 2013 usa apenas loops de troca de bits convencional.

Como se poderia esperar, compilar em 32 ou 64 bits não faz diferença.

Refinamentos adicionais

Notei alguns grupos A produzindo as mesmas probabilidades após todas as operações de redução, mas não consegui identificar um padrão que permitisse reagrupá-las.

Aqui estão os pares que eu notei para 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 que as duas últimas probabilidades devem ser sempre as mesmas. Isso ocorre porque o n + 1 ° produto interno é realmente o mesmo que o primeiro.

O que eu quis dizer foi que os primeiros n produtos internos são zero se e somente se os primeiros n + 1 forem. O último produto interno não fornece nenhuma informação nova, como você já fez antes. Portanto, o número de cadeias que fornecem n zero produtos é exatamente igual ao número que dá n + 1 zero produtos.

Fora de interesse, o que você estava computando exatamente?

Obrigado pela atualização, mas não entendo a linha "0 2160009216 2176782336". O que exatamente você está contando neste caso? A probabilidade de o primeiro produto interno ser zero é muito menor que isso.

Você poderia dar alguns conselhos sobre como compilar e executar isso? Eu tentei g ++ -Wall -std = c ++ 11 kuroineko.cpp -o kuroineko e ./kuroineko 12, mas dáterminate called after throwing an instance of 'std::system_error' what(): Unknown error -1 Aborted (core dumped)
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.