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 дійсно оптимізує операції зі списком.