曲棍球池算法

时间:2011-09-17 01:50:22

标签: knapsack-problem

这是一个有趣的项目,我已经开始尝试并最大化我赢得办公室曲棍球池的机会。我正在努力寻找最佳方式来选出20名球员,这些球员将在最高工资帽中获得最高分。

例如,假设原始数据由

组成
  1. 玩家名称
  2. 位置(前锋,前卫,守门员)
  3. 本赛季的预测分数
  4. 薪水。
  5. 现在我希望20名球员能够在X工资帽中获得最高分。后来,作为第2阶段,我想做同样的事情但是在这种情况下,我只想要12名前锋,6名防守队员和2名守门员。

    现在显而易见的方法是简单地进行所有可能的组合,但是虽然这将起作用,但是对于500名玩家来说这不是一个有效的选项,这将有太多可能的组合。我可以添加一些智能过滤器,将500名玩家减少到前50名前锋,前30名防守队员和前15名守门员,但是,这将是一个非常缓慢的过程。

    我想知道是否还有其他算法来实现这一点。这只是为了娱乐而不是重要的业务请求。但如果您对如何继续有一些想法,请告诉我。

    我的第一次尝试是在其他来源的帮助下使用背包算法。它似乎只与Salary一起作为参数。我正在努力弄清楚如何添加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;
    }
    
    } 
    }
    

    感谢您的帮助!

5 个答案:

答案 0 :(得分:3)

不幸的是,您不应期望找到这个问题的良好解决方案,因为它是NP-hard。除非P = NP,否则没有任何多项式时间算法,并且穷举搜索可能是最好的算法之一(尽管您可能会使用一些启发式算法来加速它)。

要看到这个问题是NP难的,我们将展示如何在多项式时间内将knapsack problem减少到它。给定子集和问题的任何实例由一组S = {(weight 1 ,value 1 )组成,(weight 2 ,value < sub> 2 ),...,(weight n ,value n )}和权重限制k,我们可以通过以下方式构建你的曲棍球问题的实例创建一组玩家,其薪水是权重,其预期点是价值。然后我们尝试找到薪水不超过k的玩家的最大权重组合,然后与原始背包问题中不超过目标体重的最大总和相同。

正如你发布的代码所示,有一个伪多项式时间算法来解决背包问题。假设工资很低(或者你可以将它们标准化为小数字),你可以使用它来达到良好的效果。

虽然有一个多项式时间算法来确定答案是值得怀疑的,但如果你对近似最优解有好处,那么有一个多项式时间算法来近似解决背包问题。有关详细信息,请查看these notes,其中详细介绍了两种算法。有趣的是,它们依赖于您似乎正在使用的伪多项式时间算法,所以它们可能很容易实现?

很抱歉打破你对数学的好解决方案的希望...... NP难题往往会这样做。 : - (

答案 1 :(得分:3)

enter image description here

在图表上绘制玩家,X轴是点,Y轴是工资,原点是零。右下角球员将是理想的廉价高得分手,左上角球员将是不受欢迎的昂贵的低得分手,对角线上的球员将是相等的(每点相同的成本)。从X水平逆时针扫描原始锚定半径到Y垂直,形成一个不断增长的扇形切片,直到切片内的20个玩家或切片内的总工资达到上限。如果你达到$ cap而不是20个玩家,删除切片内的“最左上角”玩家并继续。如果你达到20个玩家而不是上限,你可以从切片中删除一个低分数,为即将进入切片的得分较高的玩家腾出空间,使你的每点总成本不必要地上升,但因为这是有趣的钱并且你不是真正的主人,去吧。

答案 2 :(得分:2)

解决此类问题的有趣方法是使用genetic algorithm。实际上,我为自己的曲棍球池做到了这一点!

如果你很好奇,你可以看到the whole Scala code here,但它的核心是:

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)
} 

首先生成有效阵容的随机集合(尊重工资上限和位置限制)。对于每次迭代,它会使用tournament selectionhill climbing的组合生成新的种群。 “健身”简单地定义为具有最低薪水作为平局的阵容的预期分数。

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

答案 3 :(得分:0)

问题可能很难,但你可以使用这样一个事实:你的守门员不会在进攻中打球(更正式:你选择的实体属于不同的类别),以改善你的运行时间。

此外,您可能希望找到问题的近似解决方案。使用该值绑定最佳解决方案并搜索它。

答案 4 :(得分:0)

您可以轻松地将其表述为ILP。解决它们也是NP-Hard,但问题实例似乎并不那么大,所以它可能是可解的完美(一个求解器就是lpsolve)。即使由于问题的大小而无法解决问题,也存在ILP的良好启发式。