Вопрос или проблема
В игре YoGiOh игрок начинает с того, что тянет 5 карт из колоды, содержащей от 40 до 60 карт. Затем игрок может использовать стартовые карты из этих 5 карт, чтобы разработать финальную комбинацию, которую будет сложно сломать противнику. Не каждая стартовая рука хороша. Некоторые карты требуют наличия одной или нескольких других конкретных карт на руке для развития. Сложно оптимизировать колоду для комбинаций из 2 карт.
Я пишу программу, чтобы оценить свои шансы на наличие комбинаций. Она работает, но 40 секунд для маленькой колоды из 42 карт не очень эффективно. Я ищу оптимизацию.
Ввод:
Код карты | Название карты | Количество | Комбинация |
---|---|---|---|
0 | Эффект Вейлера | 3 | |
1 | Дух Теньи – Адхара | 2 | 1D |
2 | Макс “С” | 3 | |
3 | Призрачный Огурец и Снежная Кролик | 1 | |
4 | Цветок Пепла и Радости Весны | 3 | |
5 | Невероятная Эклесия, Добродетельная | 3 | 51 56 57 58 5C 5D 5E 5G 5J |
6 | Меч Души Мо Е | 3 | 61 66 67 68 6C 6D 6E 6G 6J |
7 | Меч Души Тайя | 2 | 71 7C 7D |
8 | Стратег Слов Долгие | 3 | 81 87 8C 8D 8E 8G 8J |
9 | Бистиал Друисвурм | 2 | |
A | Бистиал Магнхамут | 1 | |
B | Бистиал Балдрак | 1 | |
C | Дух Теньи – Вишуда | 1 | |
D | Дух Теньи – Ашуна | 3 | |
E | Арх-Немезис Протос | 1 | |
F | Кубок Желаний | 1 | |
G | Пояснительная Книга Мечей | 3 | G G1 G7 GC GD GE GG GJ |
H | Вызванный К Grave | 2 | |
I | Бесконечная Неопределенность | 3 | |
J | Темный Взрыв Мечей | 1 |
Это колода на 42 карты с картами “Слов Долгие”. Комбинация “1D” означает, что если у игрока есть “Дух Теньи – Адхара” с кодом 1 и “Дух Теньи – Ашуна” с кодом D, рука играбельна.
Колоду можно закодировать как 0001122234445556667788899ABCDDDEFGGGHHIIIJ. Объяснение: “000”, потому что мы используем 3 копии карты “Эффект Вейлера” с кодом 0.
Рука с комбинациями содержит символы в разделе комбинаций. 04568 содержит 56 и 68: это означает, что 2 разные комбинации играбельны.
from itertools import combinations
from collections import Counter
from math import comb
import functools
import operator
import time
import multiprocessing
import concurrent.futures
#многопоточность
number_threads = 10
number_process = 6
#разделить список на равные части
def chunkify(iterable,n):
lst = list(iterable)
return [lst[i::n] for i in range(n)]
#конец многопоточности
######ВВОД
deck_input="0001122234445556667788899ABCDDDEFGGGHHIIIJ"
success_input="1D 51 56 57 58 5C 5D 5E 5G 5J 61 66 67 68 6C 6D 6E 6G 6J 71 7C 7D 81 87 8C 8D 8E 8G 8J G1 G7 GC GD GE GG GJ"
number_card_in_hand = 5
######КОНЕЦ ВВОДА
#обработка входных данных
deck_counter = Counter(deck_input)
all_possible_hand_combination = list(combinations(deck_input, number_card_in_hand))
successes_list_of_counter = []
for combo in success_input.split(' '):
successes_list_of_counter.append(Counter(combo))
successes_set_of_counter = []
#удаление дубликатов объектов счетчиков
for i in successes_list_of_counter:
if i not in successes_set_of_counter:
successes_set_of_counter.append(i)
#конец обработки входных данных
#тест на наличие какого-либо успешного шаблона в руке, оценка в зависимости от количества разных стартовых карт в руке
def is_success_with_score(hand, successes_set_of_counter = successes_set_of_counter):
counter_hand = Counter(hand)
res = 0
for s in successes_set_of_counter:
if counter_hand >= s:
res+=1
res = min(res, len(hand))
return res
def countingWorker(possible_hand_chunk):
return count_successes(hand_combination = possible_hand_chunk)
def count_success_thread_starter():
with concurrent.futures.ThreadPoolExecutor(max_workers=number_threads) as executor:
worker_input = chunkify(all_possible_hand_combination, number_threads)
futures = [executor.submit(countingWorker, chunk) for chunk in worker_input]
res={"hand_with_starter":0, "total_number_starter":0}
for future in futures:
fr = future.result() # блокировка до возвращения результата
res["hand_with_starter"] += fr["hand_with_starter"]
res["total_number_starter"] += fr["total_number_starter"]
return res
def count_success_process_starter():
pool = multiprocessing.Pool(processes=number_process)
worker_inputs = chunkify(all_possible_hand_combination, number_threads)
res={"hand_with_starter":0, "total_number_starter":0}
for worker_result in pool.map(countingWorker, worker_inputs):
res["hand_with_starter"] += worker_result["hand_with_starter"]
res["total_number_starter"] += worker_result["total_number_starter"]
return res
def count_successes(hand_combination = all_possible_hand_combination, deck = deck_input, number_card_in_hand = number_card_in_hand, successes_set_of_counter = successes_set_of_counter):
res={"hand_with_starter":0, "total_number_starter":0}
for hand_tuple in hand_combination:
success_score = is_success_with_score(hand_tuple)
if success_score > 0:
res["hand_with_starter"] += 1
res["total_number_starter"] += success_score
return res
def total_hands(deck = deck_input, number_card_in_hand = number_card_in_hand):
return comb(len(deck), number_card_in_hand)
print('Колода:',deck_input)
print('Счетчик колоды:',deck_counter)
print('Размер колоды:',len(deck_input))
print('Размер руки:',number_card_in_hand)
all_possible_hand_combination = list(combinations(deck_input, number_card_in_hand))
time_start = time.time()
c = count_success_thread_starter()
print('Шанс получить хотя бы 1 стартовую карту',c["hand_with_starter"]/total_hands())
print('Среднее количество разных стартовых карт в руке',c["total_number_starter"]/total_hands())
time_end = time.time()
print('Прошедшее время:', time_end-time_start)
Вывод кода:
Колода: 0001122234445556667788899ABCDDDEFGGGHHIIIJ
Размер колоды: 42
Размер руки: 5
Шанс получить хотя бы 1 стартовую карту 0.8039293825558267
Среднее количество разных стартовых карт в руке 1.921834370165564
Прошедшее время: 38.807377576828
профилировщик
195255471 вызовов функций (195254994 примитивных вызовов) за 47.338 секунд
Упорядочено по: накопленное время
ncalls tottime percall cumtime percall filename:lineno(function)
24/1 0.000 0.000 47.338 47.338 {встроенный метод builtins.exec}
1 0.045 0.045 47.338 47.338 2card combo.py:1(<module>)
1 0.349 0.349 47.261 47.261 2card combo.py:200(count_successes)
658008 3.395 0.000 46.912 0.000 2card combo.py:165(is_success_with_score)
12502152 8.150 0.000 42.003 0.000 __init__.py:802(__ge__)
12502323 6.879 0.000 32.231 0.000 {встроенный метод builtins.all}
87423969 22.410 0.000 26.229 0.000 __init__.py:806(<genexpr>)
в настоящее время решение с потоками и без потоков работают одинаково медленно. Решение с процессами работает очень медленно.
Я думал, что генератор комбинаций медленный, но ему понадобилось всего 0.04 секунды, чтобы кэшировать список. Я попробовал многопоточность, многопроцессорность. Результаты те же или хуже.
Стоит ли мне использовать numpy и перемножать тензоры? 🙁
Ответ или решение
Чтобы эффективно определить вероятность формирования хорошей конечной доски в игре Yu-Gi-Oh! Master Duel с помощью Python, вам нужно оптимизировать свой код и использовать более быстрые алгоритмы. Давайте разберем ваш итеративный подход и предложим некоторые улучшения.
Оптимизация алгоритма
-
Использование
numpy
:
Библиотекаnumpy
может помочь улучшить производительность, особенно при работе с массивами и матрицами. Вместо использованияCounter
для подсчета карточек и проверки комбинаций, вы можете представлять вашу колоду в виде массива и производить операции более эффективно. -
Избегание повторного вычисления:
Вычисления, которые можно кэшировать или хранить, должны использоваться повторно, чтобы снизить время выполнения. Сохранение результатов для уже обработанных рук поможет избежать повторных вычислений. -
Уменьшение сложности:
Для каждой возможной руки вы можете сразу проверять, удовлетворяет ли она вашим условиям, вместо проверки всех возможных комбинаций. Это сокращает общее количество проверок.
Пример кода с оптимизациями
Вот пример того, как можно переписать ваш код с учетом вышеуказанных предложений:
import numpy as np
from itertools import combinations
from collections import Counter
from math import comb
import time
# Определим входные данные
deck_input = "0001122234445556667788899ABCDDDEFGGGHHIIIJ"
success_input = "1D 51 56 57 58 5C 5D 5E 5G 5J 61 66 67 68 6C 6D 6E 6G 6J 71 7C 7D 81 87 8C 8D 8E 8G 8J G1 G7 GC GD GE GG GJ"
number_card_in_hand = 5
# Преобразуем входные данные
deck_counter = Counter(deck_input)
success_combos = [Counter(combo) for combo in success_input.split()]
# Функция для проверки комбинаций в руке
def check_success(hand_counter, success_combos):
count = sum(1 for combo in success_combos if all(hand_counter[c] >= combo[c] for c in combo))
return count
# Генерация всех возможных рук
all_possible_hands = list(combinations(deck_input, number_card_in_hand))
# Счетчики для успешных рук
successful_hands = 0
total_starters = 0
# Оптимизированный обход рук
start_time = time.time()
for hand in all_possible_hands:
hand_counter = Counter(hand)
score = check_success(hand_counter, success_combos)
if score > 0:
successful_hands += 1
total_starters += score
# Определение общего количества рук
total_hands = comb(len(deck_input), number_card_in_hand)
# Результаты
print('Вероятность наличия хотя бы 1 стартерной карты:', successful_hands / total_hands)
print('Среднее количество разных стартеров в руке:', total_starters / total_hands)
print('Время выполнения:', time.time() - start_time)
Пояснения к коду
- Использование
Counter
: Здесь мы используемCounter
для представления карт в руке и проверяем, удовлетворяет ли она условиям для успеха. - Обход рук: Вместо использования многопоточности или множественных процессов, мы просто проходим все возможные руки, что и проще для отладки.
- Время выполнения: Код должен выполняться значительно быстрее, поскольку объем вычислений был уменьшен.
Заключение
Оптимизация вашего кода позволит уменьшить время выполнения, что является жизненно важным для обработки больших наборов комбинаций. Используйте библиотеки, такие как numpy
, чтобы воспользоваться векторизированными операциями, если это возможно.
Это базовое решение можно дополнительно улучшать, в зависимости от специфики вашей задачи.