曲棍球游戏池算法

8
这是一个小有趣的项目,我开始尝试最大化赢得我们办公室冰球比赛的机会。我正在尝试找到在最大薪资帽下能给我带来最多积分的20名球员。
例如,假设原始数据由以下信息组成:
  1. 球员姓名
  2. 位置(前锋、后卫、守门员)
  3. 本赛季预计得分
  4. 薪资
现在我想要在X薪资帽下选出使我获得最多积分的20名球员。以后,在第二阶段,我想做同样的事情,但在这种情况下,我只需要12名前锋、6名后卫和2名守门员。
现在,显而易见的方法是简单地遍历所有可能的组合,然而,虽然这种方法可以实现目标,但对于500名球员来说,可能性太多了。我可以添加一些智能筛选器,将500名球员减少到前50名前锋、前30名后卫和前15名守门员,但仍然会非常缓慢。
我想知道是否有其他算法来实现这个目标。这只是为了好玩,并不是重要的商业请求。但如果你有任何想法,请让我知道。
我的第一次尝试是使用了背包算法和其他来源的帮助。它似乎只使用薪资作为参数就可以工作。但我很难弄清楚如何添加20名球员团队参数。它是用.Net实现的,但应该很容易转换成Java。
我在考虑做一个单独的循环,以找出最好的由20名球员组成的团队,而不考虑薪资,然后将两个列表进行比较,直到找到在两个列表中都排名最高的团队。不确定这样是否可行。
namespace HockeyPoolCalculator
{
public class ZeroOneKnapsack
{

protected List<Item> itemList = new List<Item>();
protected int maxSalary = 0;
protected int teamSize = 0;
protected int teamSalary = 0;
protected int points = 0;
protected bool calculated = false;

public ZeroOneKnapsack() { }

public ZeroOneKnapsack(int _maxSalary)
{
setMaxSalary(_maxSalary);
}

public ZeroOneKnapsack(List<Item> _itemList)
{
setItemList(_itemList);
}

public ZeroOneKnapsack(List<Item> _itemList, int _maxSalary)
{
setItemList(_itemList);
setMaxSalary(_maxSalary);
}

// calculte the solution of 0-1 knapsack problem with dynamic method:
public virtual List<Item> calcSolution()
{
int n = itemList.Count;

setInitialStateForCalculation();
if (n > 0 && maxSalary > 0)
{
List<List<int>> playerList = new List<List<int>>();
List<int> salaryList = new List<int>();

//initialise list
playerList.Add(salaryList);
for (int j = 0; j <= maxSalary; j++)
salaryList.Add(0);
// Loop through players
for (int i = 1; i <= n; i++)
{
List<int> prev = salaryList;
playerList.Add(salaryList = new List<int>());
for (int j = 0; j <= maxSalary; j++)
{
if (j > 0)
{
int wH = itemList.ElementAt(i - 1).getSalary();
// Is the players salary more than the current calculated salary? If yes, then keep current max points, else get the highest amount between previous max points at that salary and new max points.
salaryList.Add((wH > j)?prev.ElementAt(j): Math.Max(prev.ElementAt(j),itemList.ElementAt(i - 1).getPoints() + prev.ElementAt(j - wH)));
}
else
{
salaryList.Add(0);
}
} // for (j...)
} // for (i...)
points = salaryList.ElementAt(maxSalary);

for (int i = n, j = maxSalary; i > 0 && j >= 0; i--)
{
int tempI = playerList.ElementAt(i).ElementAt(j);
int tempI_1 = playerList.ElementAt(i - 1).ElementAt(j);
if ((i == 0 && tempI > 0)||(i > 0 && tempI != tempI_1))
{
Item iH = itemList.ElementAt(i - 1);
int wH = iH.getSalary();
iH.setInKnapsack(1);
j -= wH;
teamSalary += wH;
}
} // for()
calculated = true;
} // if()
return itemList;
}

// add an item to the item list
public void add(String name, int Salary, int value)
{
if (name.Equals(""))
name = "" + (itemList.Count() + 1);
itemList.Add(new Item(name, Salary, value));
setInitialStateForCalculation();
}

// add an item to the item list
public void add(int Salary, int value)
{
add("", Salary, value); // the name will be "itemList.size() + 1"!
}

// remove an item from the item list
public void remove(String name)
{
for (int pointer = 0; pointer <= itemList.Count-1; pointer++)

{
itemList[pointer].getName().Equals("");

if (itemList.ElementAt(pointer).getName().Equals(itemList.ElementAt(pointer).getName()))
{
itemList.Remove(itemList.ElementAt(pointer));
}
}
setInitialStateForCalculation();
}

// remove all items from the item list
public void removeAllItems()
{
itemList.Clear();
setInitialStateForCalculation();
}

public int getPoints()
{
if (!calculated)
calcSolution();
return points;
}

public int getSolutionSalary() { return teamSalary; }
public bool isCalculated() { return calculated; }
public int getMaxSalary() { return maxSalary; }

public void setTeamSize(int _teamSize)
{
teamSize = _teamSize;
}

public int getTeamSize()
{
return teamSize;
}

public void setMaxSalary(int _maxSalary)
{
maxSalary = Math.Max(_maxSalary, 0);
}

public void setItemList(List<Item> _itemList) {
if (_itemList != null) {
itemList = _itemList;
foreach (Item item in _itemList) {
item.checkMembers();
}
}
}

// set the member with name "inKnapsack" by all items:
private void setInKnapsackByAll(int inKnapsack) {
foreach (Item item in itemList)
if (inKnapsack > 0)
item.setInKnapsack(1);
else
item.setInKnapsack(0);
}

// set the data members of class in the state of starting the calculation:
protected void setInitialStateForCalculation()
{
setInKnapsackByAll(0);
calculated = false;
points = 0;
teamSalary = 0;
teamSize = 0;
}

} 
}

感谢您的帮助!

第一阶段你完全不关心位置吗?比如说,你不想要至少一个守门员? - Witness Protection ID 44583292
5个回答

3

不幸的是,由于这个问题是NP-hard,你不应该期望找到一个好的解决方案。除非P = NP,否则就没有多项式时间算法,而穷举搜索可能是最好的算法之一(尽管你可以使用一些启发式方法来加速它)。

为了证明这个问题是NP难的,我们将展示如何在多项式时间内将背包问题归约到它。给定任何由集合S = {(weight1, value1), (weight2, value2), ... , (weightn, valuen)}和重量限制k组成的子集求和问题实例,我们可以通过创建一组薪资为权重、期望得分为价值的球员来构建一个您的曲棍球问题的实例。然后我们尝试找到薪水不超过k的最大重量的球员组合,这与在原始背包问题中不超过目标重量的最大总和相同。
正如您发布的代码所显示的那样,有一种伪多项式时间算法可以解决背包问题。假设薪水较低(或者您可以将它们归一化为小数),您可能会有效地使用它。
虽然很难找到一个多项式时间算法来得到精确答案,但如果您可以接受近似最优解,那么有一个多项式时间算法可以近似解决背包问题。有关详细信息,请查看这些笔记,其中详细介绍了两种算法。有趣的是,它们依赖于您似乎正在使用的伪多项式时间算法,因此可能很容易实现?抱歉让您对数学问题的美好解决方案感到失望... NP-hard问题往往会这样做。 :-(

我认为这里的忧虑是不必要的。解决方案质量的瓶颈肯定在于预测的质量,而不是在于团队优化所需的时间/误差。请考虑,我真的怀疑他能否在预测中获得3位数字的准确性,然后如果他将问题离散化到4个有效数字,他可以在时间10^显著性*联盟球员数~= 10^4 * 500 = 500万次计算内近似解决它。 - Rob Neuhaus
如果您有时间和兴趣,能否解释一下为什么@mike的解决方案不起作用。如果它能够起作用,那么这就不是NP难问题了,对吗? - Unapiedra
当然!他所描述的算法是一种贪心算法,在扫描过程中总是选择每个美元最昂贵的球员。然而,你可以构造出这样的例子:这个球员需要在最优解中,而将他排除在外总是会让你处于更糟糕的情况。试着想一下这种情况可能是什么。 - templatetypedef

3

enter image description here

在图表上绘制球员,X轴表示得分,Y轴表示薪水,原点处为零。右下方的球员将是价格便宜,得分高的理想选择,左上方的球员将是价格昂贵,得分低的不理想选择,位于对角线上的球员将是等价的(每个点的成本相同)。从以原点为锚点的X轴逆时针扫过Y轴,形成一个不断增长的饼状切片,直到20个球员在切片内或者总薪资达到限额。如果你达到了 $ 上限但不足20个球员,请删除切片内最左上角的球员并继续操作。如果你达到了20个球员但没有达到上限,你可以删除切片中的低得分球员,为即将进入切片的高得分球员腾出位置,这会导致你的总成本每个点不必要地增加,但由于这只是虚假货币,你不是真正的所有者,所以可以尝试一下。


这是一个不错的贪心算法,但是除非你刚刚找到了一个证明 P = NP 的算法,否则无法保证它能给出最优解! - templatetypedef
没有人声称这是最优解,只是一种攻击问题的简单方法,极有可能和其他任何方法一样好。(即使选择最优解也不能保证他会赢得比赛,因为球员过去的表现不能保证他们未来的成功。)请注意,除了被抢先选几名球员外,这种方法很快就变成了按每分最低成本顺序选择的简单方法,这可能和其他任何方法一样好。 - Witness Protection ID 44583292

2
一种有趣的解决这种问题的方法是使用遗传算法。实际上,我就是用它来管理我的曲棍球池的!
如果您感兴趣,可以在此处查看整个Scala代码,但其中的核心部分是:
def GA(nbIter: Int, popSize: Int, tournamentSize: Int) = {
  def loop(iter: Int, pop: Seq[LineUp]): LineUp = {
    val best = pop.maxBy(_.fitness)
    println("\nBest of generation " + iter + best)
    if (iter == nbIter)
      best
    else {
      val newPop = for {
        _ ← Stream.continually()
        x = tournament(pop, tournamentSize).head
        y = (x.mutants take 5) maxBy (_.fitness)
      } yield y
      loop(iter + 1, best +: newPop.take(popSize - 1))
    }
  }
  val initialPop = LineUp.randomStream.take(popSize)
  loop(0, initialPop)
} 

它首先生成一个随机的有效阵容集合(考虑薪资限制和位置限制)。对于每个迭代,它通过使用锦标赛选择爬山算法的组合来生成新的人群。 "适应度" 简单地定义为具有最低薪资作为平局解决者的阵容的预期得分数量。

当然,这只是一种启发式方法,但如果您的球员列表不太大,并且您可以让算法运行一段时间,那么您很有可能会找到一个相当优化的解决方案。

0
问题可能很困难,但您可以利用守门员不参与进攻的事实(更正式地说,所选实体属于不同类别),以提高运行时间。
此外,您可能希望找到问题的近似解。使用该值来限制最优解并搜索它。

0

你可以将它轻松地表述为整数线性规划(ILP)。解决这些问题也是NP难的,但问题实例似乎不是很大,因此它可能是可解的(例如一个求解器是lpsolve)。即使由于问题规模而无法完美解决,也存在良好的整数线性规划启发式算法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接