Python 2 с использованием pypy и pp: n = 15 за 3 минуты
Также просто грубая сила. Интересно видеть, что я почти получаю ту же скорость, что и курои нэко с 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: Ссылка . Многоядерное программирование делает его немного уродливым.
редактировать 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:
Быстро реализовал отличную идею Куроя Неко, которая гласит, что B
и -B
дает те же результаты. Ускорение х2. Реализация - это только быстрый взлом. 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 действительно оптимизирует операции со списком.