Комп’ютер: ти займаєшся математикою


13

Це завдання частково є викликом алгоритмів, включає деяку математику і частково просто найшвидший виклик коду.

Для деякого додатного цілого числа nрозглянемо рівномірно випадковий рядок довжини 1s і 0s nі назвіть його A. Тепер також розглянемо другий рівномірно обраний випадковий рядок довжини n, значення якого є -1, 0,або 1назвіть його B_pre. Тепер нехай Bбуде B_pre+ B_pre. Це пов'язане B_preз собою.

Тепер розглянемо скалярний твір Aі B[j,...,j+n-1]і назвіть його Z_jі індекс з 1.

Завдання

На виході повинен бути список n+1дробів. iЙ член на виході повинен бути точною вірогідністю того, що все з перших iтермінів Z_jз j <= iрівним 0.

Оцінка

Найбільший, nдля якого ваш код дає правильний висновок за 10 хвилин на моїй машині.

Tie Breaker

Якщо дві відповіді мають однаковий бал, той, хто подав перший, виграє.

У (дуже вельми) малоймовірному випадку, коли хтось знайде спосіб отримати необмежену кількість балів, буде прийнято перше дійсне підтвердження такого рішення.

Підказка

Не намагайтеся вирішити цю проблему математично, це занадто важко. На мій погляд, найкращий спосіб - повернутися до основних визначень вірогідності з середньої школи і знайти розумні способи отримати код, щоб зробити вичерпне перерахування можливостей.

Мови та бібліотеки

Ви можете використовувати будь-яку мову, у якій є доступний компілятор / перекладач / тощо. для Linux та будь-яких бібліотек, які також вільно доступні для Linux.

Моя машина Часи синхронізуються на моїй машині. Це стандартна установка ubuntu на восьмиядерний процесор AMD FX-8350. Це також означає, що мені потрібно мати можливість запускати ваш код. Як наслідок, використовуйте лише доступне безкоштовне програмне забезпечення та додайте повні інструкції щодо компіляції та запуску коду.


Деякі тестові результати. Розглянемо лише перший вихід для кожного n. Це коли i=1. Бо nвід 1 до 13 вони повинні бути.

 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

Загальну формулу ви також можете знайти на веб- i=1сайті http://oeis.org/A081671 .

Табло (розділено за мовою)

  • n = 15. Python + паралельний python + pypy за 1min49s від Jakube
  • n = 17. C ++ за 3 хв. 37 Кіт Рендалл
  • n = 16. С ++ за 2 хв. 38м. по kuroi neko

1
@Knerd Як я можу сказати, що ні. Я спробую розробити, як запустити ваш код в Linux, але будь-яка допомога дуже вдячна.

Добре, вибачте за видалення коментарів. Для всіх, хто не читав, це було, якщо дозволено F # або C # :)
Кнерд

Ще одне запитання: чи, можливо, є приклад вагомого вхідного виводу?
Кнерд

Яка ваша графічна картка? Виглядає як робота для GPU.
Майкл М.

1
@Knerd Я замість цього додав таблицю ймовірностей. Я сподіваюся, що це корисно.

Відповіді:


5

C ++, n = 18 за 9 хв на 8 ниток

(Повідомте мене, якщо він працює на вашій машині за 10 хвилин.)

Я використовую кілька форм симетрії в масиві B. Це циклічні (зсув на одну позицію), зворотні (зворотний порядок елементів) та знакові (приймають мінус кожного елемента). Спочатку я обчислюю список тих, хто нам потрібно спробувати, та їх вагу. Тоді кожен B виконується через швидку процедуру (з використанням інструкцій по бітковим рахункам) для всіх 2 ^ n значень А.

Ось результат для 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

Складіть програму нижче 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;
}

Добре, що позбавляє мене подальшої роботи над моїм власним домашнім монстром ...

Дякую за це У вас є поточний виграшний запис. Треба -pthreadзнову пам’ятати . Я добираюся до n=17своєї машини.

На жаль .. Ви повинні були отримати повну суму. Вибачте, що я пропустив термін.

@Lembik: немає проблем.
Кіт Рендалл

6

Python 2, використовуючи pypy і pp: n = 15 за 3 хвилини

Також просто проста груба сила. Цікаво побачити, що я майже отримую ту саму швидкість, що і kuroi neko з C ++. Мій код може отримати n = 12приблизно за 5 хвилин. І я запускаю його лише на одному віртуальному ядрі.

редагувати: зменшити простір пошуку на коефіцієнт n

Я помітив, що циклічне вектор A*з Aвиробляє одні і те ж число , як і ймовірності ( то ж номера) в якості вихідного вектора , Aколи я перебирати B. Наприклад , вектор (1, 1, 0, 1, 0, 0)має ту ж ймовірність , як кожен з векторів (1, 0, 1, 0, 0, 1), (0, 1, 0, 0, 1, 1), (1, 0, 0, 1, 1, 0), (0, 0, 1, 1, 0, 1)і (0, 1, 1, 0, 1, 0)при виборі випадкового чином B. Тому я не перебирати кожний із цих 6 векторів, але тільки близько 1 і замінити count[i] += 1з count[i] += cycle_number.

Це зменшує складність з Theta(n) = 6^nдо Theta(n) = 6^n / n. Тому n = 13це приблизно в 13 разів швидше, ніж моя попередня версія. Це розраховується n = 13приблизно за 2 хвилини 20 секунд. Бо n = 14це ще трохи надто повільно. Це займає близько 13 хвилин.

редагувати 2: Багатоядерне програмування

Не дуже задоволений наступним поліпшенням. Я вирішив також спробувати виконати свою програму на декількох ядрах. На моїх 2 + 2 ядрах я зараз можу обчислити n = 14приблизно за 7 хвилин. Лише коефіцієнт 2 покращення.

Код доступний у цьому github repo: Посилання . Багатоядерне програмування робить трохи некрасивим.

редагувати 3: Скорочення пошукового простору для Aвекторів та Bвекторів

Я помітив таку саму дзеркальну симетрію для векторів, Aяк це зробив і курой. Досі не впевнений, чому це працює (і якщо він працює для кожного n).

Скорочення пошукового простору для Bвекторів трохи розумніше. Я замінив покоління векторів ( itertools.product), власною функцією. В основному я починаю з порожнього списку і ставлю його на стек. Поки стек не порожній, я видаляю список, якщо він не має такої ж довжини, як n, я генерую 3 інші списки (додаючи -1, 0, 1) і натискаю їх на стек. Я список має таку ж довжину, як nі я можу оцінити суми.

Тепер, коли я сам генерую вектори, я можу їх фільтрувати залежно від того, чи зможу я досягти суми = 0 чи ні. Наприклад , якщо мій вектор Aє (1, 1, 1, 0, 0), і мій вектор Bвиглядає (1, 1, ?, ?, ?), я знаю, що я не можу заповнити ?зі значеннями, так що A*B = 0. Тому мені не доведеться повторювати всі 6 векторів Bформи (1, 1, ?, ?, ?).

Ми можемо покращити це, якщо проігнорувати значення для 1. Як зазначено у питанні, для значень для i = 1є послідовність A081671 . Існує багато способів їх обчислення. Я вибираю простий рецидив: a(n) = (4*(2*n-1)*a(n-1) - 12*(n-1)*a(n-2)) / n. Оскільки ми можемо обчислити i = 1в основному немає часу, ми можемо відфільтрувати більше векторів B. Наприклад A = (0, 1, 0, 1, 1)і B = (1, -1, ?, ?, ?). Ми можемо ігнорувати вектори, де перший ? = 1, тому що A * cycled(B) > 0, для всіх цих векторів. Я сподіваюся, що ви можете слідувати. Це, мабуть, не найкращий приклад.

З цим я можу порахувати n = 15за 6 хвилин.

редагувати 4:

Швидко реалізувати Kuroi Неко відмінну ідею, яка говорить, що Bі -Bробить ті ж самі результати. Швидкість x2. Однак впровадження - це лише швидкий злом. n = 15за 3 хвилини.

Код:

Щоб отримати повний код, відвідайте Github . Наступний код є лише поданням основних особливостей. Я залишив імпорт, багатоядерне програмування, друк результатів, ...

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

Використання:

Ви повинні встановити pypy (для Python 2 !!!). Паралельний модуль python не переноситься для Python 3. Потім потрібно встановити паралельний модуль python pp-1.6.4.zip . Витягніть його cdв папку і зателефонуйте pypy setup.py install.

Тоді ви можете зателефонувати з моєю програмою

pypy you-do-the-math.py 15

Він автоматично визначить кількість процесорів. Після закінчення програми можуть з’явитися деякі повідомлення про помилки, просто проігноруйте їх. n = 16має бути можливим на вашій машині.

Вихід:

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%

Примітки та ідеї:

  • У мене процесор i7-4600m з 2 ядрами і 4 потоками. Не має значення, якщо я використовую 2 або 4 нитки. Використання процесора становить 50% з 2 потоками і 100% з 4 потоками, але це все одно займає стільки ж часу. Я не знаю чому. Я перевірив, що кожен потік містить лише половину даних, коли є 4 потоки, перевірив результати, ...
  • Я використовую безліч списків. Python не дуже ефективний для зберігання, мені доведеться копіювати багато списків, ... Тому я подумав використовувати ціле число. Я міг би використовувати біти 00 (для 0) та 11 (для 1) у векторі A, а біти 10 (для -1), 00 (для 0) та 01 (для 1) у векторі B. Для продукту з A і B, я повинен був би лише обчислити A & Bі порахувати блоки 01 і 10. Велоспорт можна зробити зі зміщенням вектора та використанням масок, ... Я фактично все це реалізував, ви можете знайти його в деяких моїх старих комісіях на Github. Але виявилося повільніше, ніж зі списками. Я думаю, pypy дійсно оптимізує операції зі списком.

На моєму комп'ютері тривалість n = 12 займає 7:25, а мій мотлох C ++ займає близько 1:23, що робить його приблизно в 5 разів швидше. Маючи лише два справжні ядра, мій процесор отримає щось на зразок 2,5 коефіцієнта порівняно з однопотоковою програмою, тому справжній 8 ядер CPU повинен працювати щось на кшталт 3 рази швидше, і це не рахується з базовим покращенням швидкості одноядерного моє старіння i3-2100. Чи варто проходити всі ці обручі С ++ для вирішення експоненціально зростаючого часу обчислень, варто докладати зусиль, однак дискусійно.

Я відчуваю codegolf.stackexchange.com/questions/41021/… ... Чи корисна буде послідовність Де Бруйна ?
kennytm

про багатопотокове ви можете вичавити трохи більше 2 + 2 ядер, фіксуючи кожну нитку на одному. Коефіцієнт посилення x2 пояснюється тим, що планувальник зміщується навколо ваших потоків кожного разу, коли в систему переміщується відповідник. При блокуванні ядра, ви, ймовірно, отримаєте х2,5 посилення замість цього. Не маю уявлення, чи Python дозволяє встановити спорідненість процесора.

Спасибі, я розберуся. Але я майже новачок у багатопотоковому читанні.
Якубе

nbviewer.ipython.org/gist/minrk/5500077 має деякі згадки про це, хоч і використовує інший інструмент для паралелізму.

5

пухнастий хуліган - С ++ - занадто повільно

Отже, оскільки кращий програміст взяв на себе реалізацію C ++, я закликаю вийти з цього.

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

Побудова виконуваного файлу

Це автономне джерело C ++ 11, яке збирається без попереджень і працює безперебійно:

  • Win7 та MSVC2013
  • Win7 & MinGW - г ++ 4,7
  • Ubuntu & g ++ 4.8 (у VM VirtualBox з виділеними 2 процесорами)

Якщо ви компілюєте з g ++, використовуйте: g ++ -O3 -pthread -std = c ++ 11,
забувши про, -pthreadвийде гарний і дружній дамп для основного.

Оптимізація

  1. Останній Z-член дорівнює першому (Bpre x A в обох випадках), тому останні два результати завжди рівні, що відпускає обчислення останнього значення Z.
    Коефіцієнт посилення нехтуючий, але кодування не коштує нічого, щоб ви могли також використовувати його.

  2. Як виявив Якубе, всі циклічні значення заданого вектора A дають однакові ймовірності.
    Ви можете обчислити їх за допомогою одного екземпляра A і помножити результат на кількість можливих обертів. Групи обертання легко можна заздалегідь обчислити за недбалий проміжок часу, тому це величезне збільшення чистої швидкості.
    Оскільки кількість перестановок n вектора довжини дорівнює n-1, складність падає від o (6 n ) до o (6 n / (n-1)), в основному йде на крок далі за той же час обчислення.

  3. Здається, пари симетричних візерунків також генерують однакові ймовірності. Наприклад, 100101 та 101001.
    Я не маю математичного підтвердження цього, але інтуїтивно, коли представлено всі можливі B шаблони, кожне симетричне значення A буде зміщене з відповідним симетричним значенням B для того ж глобального результату.
    Це дозволяє перегрупувати ще декілька А векторів, щоб приблизно на 30% зменшити кількість груп А.

  4. WRONG З якихось напів загадкових причин всі візерунки з лише одним або двома бітами встановлені однаковий результат. Це не означає, що багато різних груп, але все-таки їх можна об'єднати практично без витрат.

  5. Вектори B і -B (B з усіма компонентами, помножені на -1) дають однакові ймовірності.
    (наприклад [1, 0, -1, 1] та [-1, 0, 1, -1]).
    За винятком нульового вектора (всі компоненти рівні 0), B і -B утворюють пару різних векторів.
    Це дозволяє скоротити кількість значень B удвічі, розглянувши лише одну з кожної пари та помноживши її внесок на 2, додавши відомий глобальний внесок нульового B у кожну ймовірність лише один раз.

Як це працює

Кількість значень B величезна (3 н ), тому для попереднього обчислення їх знадобиться непристойні обсяги пам’яті, що сповільнить обчислення і врешті-решт вичерпає доступну оперативну пам’ять.
На жаль, я не зміг знайти простий спосіб перерахувати половину набору оптимізованих значень B, тому вдався до кодування виділеного генератора.

Могутній генератор B кодував дуже цікаво, хоча мови, які підтримують механізми вихідних даних, дозволили б запрограмувати його набагато більш елегантно.
У двох словах, це "скелет" вектора Bpre як двійковий вектор, де 1s представляють фактичні значення -1 або +1.
Серед усіх цих значень потенціалу 1 / -1 перше фіксується +1 (таким чином вибираючи один з можливих B / -B векторів), і всі інші можливі + 1 / -1 комбінації перераховуються.
Нарешті, проста система калібрування гарантує, що кожен робочий потік буде обробляти діапазон значень приблизно однакового розміру.

Значення сильно фільтруються для перегрупування в нескінченні шматки.
Це робиться в попередній обчислювальній фазі, яка грубою силою вивчає всі можливі значення.
Ця частина має недбалий O (2 n ) час виконання і її не потрібно оптимізувати (код уже досить нечитабельний, як є!).

Для оцінки внутрішнього продукту (який потрібно лише перевірити на нуль) -1 і 1 компоненти В перегрупуються у бінарні вектори.
Внутрішній добуток є нульовим, якщо (і лише якщо) серед B значень, що відповідають нульовим значенням A, є рівна кількість + 1s та -1s.
Це можна обчислити за допомогою простих операцій маскування та підрахунку бітів, що допоможе std::bitsetцим створити досить ефективний код підрахунку бітів, не вдаючись до некрасивих внутрішніх інструкцій.

Робота рівномірно розділена між ядрами, з вимушеною спорідненістю процесора (кожен трохи допомагає, або так вони кажуть).

Приклад результату

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

Виступи

Багатопоточне читання повинно працювати на цьому ідеально, хоча тільки "справжні" ядра повністю сприятимуть швидкості обчислення. Мій процесор має лише 2 ядра для 4 процесорів, а коефіцієнт підсилення над однопотоковою версією "лише" близько 3,5.

Компілятори

Первісна проблема з багатопотоковою програмою змусила мене повірити, що компілятори GNU працюють гірше, ніж Microsoft.

Після більш ретельного тестування виявляється, що g ++ знову виграє день, виробляючи приблизно на 30% швидший код (те саме співвідношення я помітив і в двох інших важких для обчислення проектах).

Зокрема, std::bitsetбібліотека реалізована з виділеними інструкціями щодо кількості бітів g ++ 4.8, тоді як MSVC 2013 використовує лише петлі звичайних бітових зрушень.

Як можна було очікувати, компіляція в 32 або 64 біти не має ніякої різниці.

Подальші вдосконалення

Я помітив декілька груп А, що створювали однакові ймовірності після всіх операцій скорочення, але я не зміг визначити закономірність, яка дозволила б їх перегрупувати.

Ось пари, які я помітив за 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

Я думаю, що останні дві ймовірності повинні завжди бути однаковими. Це тому, що n + 1-й внутрішній продукт насправді такий же, як перший.

Я мав на увазі те, що перші n внутрішніх продуктів дорівнюють нулю тоді і лише тоді, коли перші n + 1 є. Самий останній внутрішній продукт не дає ніякої нової інформації, як ви це вже робили раніше. Отже, кількість рядків, що дають n нульових продуктів, точно така ж, як і кількість n + 1 нульових продуктів.

З цікавості, що ти робив замість цього саме?

Дякую за оновлення, але я не розумію рядок "0 2160009216 2176782336". На що саме ви рахуєте в цьому випадку? Ймовірність того, що перший внутрішній добуток дорівнює нулю, значно менша за це.

Не могли б ви дати поради, як компілювати та запускати це? Я спробував g ++ -Wall -std = c ++ 11 kuroineko.cpp -o kuroineko і ./kuroineko 12, але це даєterminate called after throwing an instance of 'std::system_error' what(): Unknown error -1 Aborted (core dumped)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.