我有一副 21 张牌和 6 名玩家。我会用字母表中的一个字母标记每张卡片。
我必须在某些条件下随机分配给每个玩家 3 张牌(总共 18 张牌,其中 3 张在牌组中),分为两个不同的类别:
andlist(x) = [[g, e, h],[k, m, f]]
,表示:'g, e, h, k, m, f 不能分配给玩家 x'orlist(x) = [[a, l, i], [j, d ,o], [b, n, c]]
,这意味着:'a 或 l 或 i,j 或 d 或 o,b 或 n 或 c 必须分配给玩家 x'。 当然这只是一个例子; andlist(x)
和 orlist(x)
可以为每个玩家包含不同数量的嵌套三元组。
我写了一个简单的代码,将 18 张牌完全随机地分配给 6 个玩家,直到满足条件为止(使用 while
循环),但是这种策略非常低效,因为它需要很长时间才能找到适当的分配。
在 Python 中是否有更有效的方法来处理这种情况?
答案 0 :(得分:0)
这似乎是一个有趣的小吃——我相信约束求解器或其他东西会更好,但这里有一些基于 orlist
定义我们可以迭代的某些基本情况的想法,递归树可能的解决方案,直到我们找到一个可行的解决方案。 (事后想来,这实际上并没有通过可能的手牌组合的完整树进行递归,因为 generate_new_solution
本身不是递归生成器...)
当然,它也可能有问题。
import random
import string
import itertools
from functools import reduce
from operator import or_
from typing import Iterable, Dict
deck_size = 21
player_count = 6
hand_size = 3
card_names = set(string.ascii_lowercase[i] for i in range(deck_size))
forbidden_cards = {
0: [set("geh"), set("kmf")],
}
required_sets = {
# 'a or l or i, and j or d or o, and b or n or c must be assigned to player x'.
0: [set("ali"), set("jdo"), set("bnc")],
1: [set("geh")],
2: [set("kmf")],
}
# Flatten the forbidden card dict sets for faster iteration
flat_forbidden_cards = {idx: frozenset(reduce(or_, forbidden_cards.get(idx, ()), set())) for idx in range(player_count)}
def is_solution_forbidden(solution: Dict[int, frozenset]) -> bool:
"""
Figure out if the solution is forbidden, i.e. any player has a forbidden card
"""
for idx, cards in solution.items():
if cards & flat_forbidden_cards.get(idx):
return True
return False
def is_solution_finished(solution: Dict[int, frozenset]) -> bool:
"""
Figure out if the solution is finished, i.e. all players have been dealt hand_size cards.
"""
return all(len(hand) == hand_size for hand in solution.values())
def get_root_solutions() -> Iterable[Dict[int, frozenset]]:
"""
Generate "root" solutions that satisfy the "required sets" constraints.
"""
required_roots = {
idx: [frozenset(card_ids) for card_ids in itertools.product(*required_sets.get(idx, ()))]
for idx in range(player_count)
}
for base_sol_cards in itertools.product(*required_roots.values()):
yield {i: cards for i, cards in enumerate(base_sol_cards)}
def iterate_solutions(base_solution: Dict[int, frozenset]) -> Iterable[Dict[int, frozenset]]:
"""
Recursive generator for solutions based on `base_solution`.
Only ever yields finished solutions.
"""
new_sol = generate_new_solution(base_solution)
if is_solution_finished(new_sol):
yield new_sol
else:
yield from iterate_solutions(new_sol)
def generate_new_solution(base_solution: Dict[int, frozenset]):
used_cards = reduce(or_, base_solution.values())
available_cards = card_names - used_cards
deal = {}
for idx in range(player_count):
if len(base_solution[idx]) < hand_size:
allowed_player_cards = available_cards - set(deal.values()) - flat_forbidden_cards[idx]
if not allowed_player_cards:
raise RuntimeError("Ran out of cards to pick")
deal[idx] = random.choice(list(allowed_player_cards))
new_sol = base_solution.copy()
for idx, hand in base_solution.items():
if idx in deal:
new_sol[idx] = frozenset(hand | {deal[idx]})
assert not is_solution_forbidden(new_sol)
return new_sol
for root_sol in get_root_solutions():
assert not is_solution_forbidden(root_sol) # Should never happen, good to check though
for solution in iterate_solutions(root_sol):
print({idx: "".join(sorted(hand)) for idx, hand in solution.items()})
输出是这样的(尽管实际上有 243 行,而不是 6 行),描述每个玩家手中牌的字典。
{0: 'bdi', 1: 'hmu', 2: 'efq', 3: 'apt', 4: 'cko', 5: 'jns'}
{0: 'bjl', 1: 'eno', 2: 'ikm', 3: 'hqs', 4: 'adg', 5: 'frt'}
{0: 'abd', 1: 'glm', 2: 'fjn', 3: 'ior', 4: 'hps', 5: 'equ'}
{0: 'bio', 1: 'hmp', 2: 'fjl', 3: 'aeu', 4: 'drt', 5: 'kns'}
{0: 'blo', 1: 'agh', 2: 'fru', 3: 'mnp', 4: 'des', 5: 'cqt'}