我正在寻找一种方法来最大化由多个来源组成的共同集合的价值,每个来源都有固定数量的贡献。
示例问题:3个人各自拥有一手牌。每手牌包含一个唯一的集合,但3个集合可能重叠。每个玩家可以选择三张牌作为贡献放在中间。如何最大化9张贡献牌的总和,其中:
- 每个玩家恰好贡献3张牌
- 所有9张牌都是唯一的(如果可能)
- 解决方案能够扩展到200个可能的“牌”,40个贡献者和每个贡献者6个贡献。
我正在寻找一种方法来最大化由多个来源组成的共同集合的价值,每个来源都有固定数量的贡献。
示例问题:3个人各自拥有一手牌。每手牌包含一个唯一的集合,但3个集合可能重叠。每个玩家可以选择三张牌作为贡献放在中间。如何最大化9张贡献牌的总和,其中:
N_PLAYERS = 40, CARD_RANGE = (0, 400), N_CARDS = 200, N_PLAY = 6
改进:问题
改进:性能
代码:
import numpy as np
import cvxpy as cvx
np.random.seed(1)
""" Random problem """
N_PLAYERS = 5
CARD_RANGE = (0, 20)
N_CARDS = 10
N_PLAY = 3
card_set = np.arange(*CARD_RANGE)
p = np.empty(shape=(N_PLAYERS, N_CARDS), dtype=int)
for player in range(N_PLAYERS):
p[player] = np.random.choice(card_set, size=N_CARDS, replace=False)
print('Players and their cards')
print(p)
""" Preprocessing:
Conflict-constraints
-> if p[i, j] == p[x, y] => don't allow both
Could be made more efficient
"""
conflicts = []
for p_a in range(N_PLAYERS):
for c_a in range(N_CARDS):
for p_b in range(p_a + 1, N_PLAYERS): # sym-reduction
if p_b != p_a:
for c_b in range(N_CARDS):
if p[p_a, c_a] == p[p_b, c_b]:
conflicts.append( ((p_a, c_a), (p_b, c_b)) )
# print(conflicts) # debug
""" Solve """
# Decision-vars
x = cvx.Bool(N_PLAYERS, N_CARDS)
# Constraints
constraints = []
# -> Conflicts
for (p_a, c_a), (p_b, c_b) in conflicts:
# don't allow both -> linearized
constraints.append(x[p_a, c_a] + x[p_b, c_b] <= 1)
# -> N to play
constraints.append(cvx.sum_entries(x, axis=1) == N_PLAY)
# Objective
objective = cvx.sum_entries(cvx.mul_elemwise(p.flatten(order='F'), cvx.vec(x))) # 2d -> 1d flattening
# ouch -> C vs. Fortran storage
# print(objective) # debug
# Problem
problem = cvx.Problem(cvx.Maximize(objective), constraints)
problem.solve(verbose=False)
print('MIP solution')
print(problem.status)
print(problem.value)
print(np.round(x.T.value))
sol = x.value
nnz = np.where(abs(sol - 1) <= 0.01) # being careful with fp-math
sol_p = p[nnz]
assert sol_p.shape[0] == N_PLAYERS * N_PLAY
""" Output solution """
for player in range(N_PLAYERS):
print('player: ', player, 'with cards: ', p[player, :])
print(' plays: ', sol_p[player*N_PLAY:player*N_PLAY+N_PLAY])
输出:
Players and their cards
[[ 3 16 6 10 2 14 4 17 7 1]
[15 8 16 3 19 17 5 6 0 12]
[ 4 2 18 12 11 19 5 6 14 7]
[10 14 5 6 18 1 8 7 19 15]
[15 17 1 16 14 13 18 3 12 9]]
MIP solution
optimal
180.00000005500087
[[ 0. 0. 0. 0. 0.]
[ 0. 1. 0. 1. 0.]
[ 1. 0. 0. -0. -0.]
[ 1. -0. 1. 0. 1.]
[ 0. 1. 1. 1. 0.]
[ 0. 1. 0. -0. 1.]
[ 0. -0. 1. 0. 0.]
[ 0. 0. 0. 0. -0.]
[ 1. -0. 0. 0. 0.]
[ 0. 0. 0. 1. 1.]]
player: 0 with cards: [ 3 16 6 10 2 14 4 17 7 1]
plays: [ 6 10 7]
player: 1 with cards: [15 8 16 3 19 17 5 6 0 12]
plays: [ 8 19 17]
player: 2 with cards: [ 4 2 18 12 11 19 5 6 14 7]
plays: [12 11 5]
player: 3 with cards: [10 14 5 6 18 1 8 7 19 15]
plays: [14 18 15]
player: 4 with cards: [15 17 1 16 14 13 18 3 12 9]
plays: [16 13 9]
a_i in {0,1}
表示玩家A
是否打出牌面值为i
的卡牌,其中i
在{1,...,N}
中。请注意,如果玩家A
手中没有i
牌,则在开始时将a_i
设置为0
。B
和C
定义b_i
和c_i
变量。m_i in {0,1}
表示牌i
是否会出现在中间,即,其中一个玩家将打出面值为i
的牌。现在您可以说:
最大化 Sum(m_i . i)
,满足以下条件:
对于每个i in {1,...N,}
:
a_i, b_i, c_i, m_i
在 {0, 1}
中。m_i = a_i + b_i + c_i
Sum(a_i) = 3
,Sum(b_i) = 3
,Sum(c_i) = 3
讨论
注意,限制条件1和2强制每张卡片在中间的唯一性。
我不确定商业或非商业求解器可以处理多大的问题,但请注意,这实际上是一个二进制线性规划问题,比一般的整数线性规划问题更容易解决,因此对于您寻找的规模可能值得尝试。
对每个手牌进行排序,删除重复值。删除任何一手牌中第10高的牌之后的所有牌(3手牌*每手3张牌,再加上1):没有人能出一张低于这个值的牌。 为了会计目的,按照牌面值建立一个目录,显示哪些手牌持有每个值。例如,给定玩家A、B、C和以下手牌:
A [1, 1, 1, 6, 4, 12, 7, 11, 13, 13, 9, 2, 2]
B [13, 2, 3, 1, 5, 5, 8, 9, 11, 10, 5, 5, 9]
C [13, 12, 11, 10, 6, 7, 2, 4, 4, 12, 3, 10, 8]
我们将对手牌进行排序和去重。2是C手牌中第10高的牌,因此我们删除所有2及以下的值。然后建立目录。
A [13, 12, 11, 9, 7, 6]
B [13, 11, 10, 9, 8, 5, 3]
C [13, 12, 11, 10, 8, 7, 6, 4, 3]
Directory:
13 A B C
12 A C
11 A B C
10 B C
9 A B
8 B C
7 A B
6 A C
5 B
4 C
3 B C
使总和最大化,取目录中前9个值的总和。如果达到这个值,请立即停止,因为您已找到最优解。
设定一个高起始目标:按顺序循环手牌,取剩余手牌中可用的最高点数牌。在本例中,循环A-B-C,则为
13, 11, 12, 9, 10, 8, 7, 5, 6 => 81 //注意:由于我选择的值 //这恰好提供了一个最优解。 //它在大部分桥牌问题空间中都是如此。
记下每个手牌已出的牌数;当一个手牌出完3张牌时,以某种方式将其淘汰:在选择代码中进行检查或从目录的局部副本中删除它。
当您遍历选择列表时,任何时候只要剩余的牌不足以达到目前为止最佳总分数,就剪枝搜索。例如,如果您在抽出7张牌后的总分数为71,而剩下的最高牌是5,则停止搜索:您无法用5+4达到81。
这能让你动起来吗?