10

Can't call it a problem on Stack Overflow apparently, however I am currently trying to understand how to integrate constraints in the form of item groups within the Knapsack problem. My math skills are proving to be fairly limiting in this situation, however I am very motivated to both make this work as intended as well as figure out what each aspect does (in that order since things make more sense when they work).

With that said, I have found an absolutely beautiful implementation at Rosetta Code and cleaned up the variable names some to help myself better understand this from a very basic perspective.

Unfortunately I am having an incredibly difficult time figuring out how I can apply this logic to include item groups. My purpose is for building fantasy teams, supplying my own value & weight (points/salary) per player but without groups (positions in my case) I am unable to do so.

Would anyone be able to point me in the right direction for this? I'm reviewing code examples from other languages and additional descriptions of the problem as a whole, however I would like to get the groups implemented by whatever means possible.

<?php

function knapSolveFast2($itemWeight, $itemValue, $i, $availWeight, &$memoItems, &$pickedItems)
{
    global $numcalls;
    $numcalls++;

    // Return memo if we have one
    if (isset($memoItems[$i][$availWeight]))
    {
        return array( $memoItems[$i][$availWeight], $memoItems['picked'][$i][$availWeight] );
    }
    else
    {
        // At end of decision branch
        if ($i == 0)
        {
            if ($itemWeight[$i] <= $availWeight)
            { // Will this item fit?
                $memoItems[$i][$availWeight] = $itemValue[$i]; // Memo this item
                $memoItems['picked'][$i][$availWeight] = array($i); // and the picked item
                return array($itemValue[$i],array($i)); // Return the value of this item and add it to the picked list

            }
            else
            {
                // Won't fit
                $memoItems[$i][$availWeight] = 0; // Memo zero
                $memoItems['picked'][$i][$availWeight] = array(); // and a blank array entry...
                return array(0,array()); // Return nothing
            }
        }   

        // Not at end of decision branch..
        // Get the result of the next branch (without this one)
        list ($without_i,$without_PI) = knapSolveFast2($itemWeight, $itemValue, $i-1, $availWeight,$memoItems,$pickedItems);

        if ($itemWeight[$i] > $availWeight)
        { // Does it return too many?
            $memoItems[$i][$availWeight] = $without_i; // Memo without including this one
            $memoItems['picked'][$i][$availWeight] = array(); // and a blank array entry...
            return array($without_i,array()); // and return it
        }
        else
        {
            // Get the result of the next branch (WITH this one picked, so available weight is reduced)
            list ($with_i,$with_PI) = knapSolveFast2($itemWeight, $itemValue, ($i-1), ($availWeight - $itemWeight[$i]),$memoItems,$pickedItems);
            $with_i += $itemValue[$i];  // ..and add the value of this one..

            // Get the greater of WITH or WITHOUT
            if ($with_i > $without_i)
            {
                $res = $with_i;
                $picked = $with_PI;
                array_push($picked,$i);
            }
            else
            {
                $res = $without_i;
                $picked = $without_PI;
            }

            $memoItems[$i][$availWeight] = $res; // Store it in the memo
            $memoItems['picked'][$i][$availWeight] = $picked; // and store the picked item
            return array ($res,$picked); // and then return it
        }   
    }
}

$items = array("map","compass","water","sandwich","glucose","tin","banana","apple","cheese","beer","suntan cream","camera","t-shirt","trousers","umbrella","waterproof trousers","waterproof overclothes","note-case","sunglasses","towel","socks","book");
$weight = array(9,13,153,50,15,68,27,39,23,52,11,32,24,48,73,42,43,22,7,18,4,30);
$value = array(150,35,200,160,60,45,60,40,30,10,70,30,15,10,40,70,75,80,20,12,50,10);

## Initialize
$numcalls = 0;
$memoItems = array();
$selectedItems = array();

## Solve
list ($m4, $selectedItems) = knapSolveFast2($weight, $value, sizeof($value)-1, 400, $memoItems, $selectedItems);

# Display Result 
echo "<b>Items:</b><br>" . join(", ", $items) . "<br>";
echo "<b>Max Value Found:</b><br>$m4 (in $numcalls calls)<br>";
echo "<b>Array Indices:</b><br>". join(",", $selectedItems) . "<br>";

echo "<b>Chosen Items:</b><br>";
echo "<table border cellspacing=0>";
echo "<tr><td>Item</td><td>Value</td><td>Weight</td></tr>";

$totalValue = 0;
$totalWeight = 0;

foreach($selectedItems as $key)
{
    $totalValue += $value[$key];
    $totalWeight += $weight[$key];

    echo "<tr><td>" . $items[$key] . "</td><td>" . $value[$key] . "</td><td>".$weight[$key] . "</td></tr>";
}

echo "<tr><td align=right><b>Totals</b></td><td>$totalValue</td><td>$totalWeight</td></tr>";
echo "</table><hr>";

?>
  • 2
    Can you please Clearly define the problem the desired end result? This would help understand the code in a timely manner instead of figuring it out manually. – Traveller Nov 06 '15 at 10:07
  • You should really try to be more active if you set a bounty to a question. – Traveller Nov 06 '15 at 14:12
  • Did You Try using info from [this] (http://stackoverflow.com/questions/29729609/knapsack-with-selection-from-distinct-groups?rq=1) post? – Traveller Nov 06 '15 at 15:34
  • The desired end result is to essentially have the items belong to groups and a number of spots for each group. This is being used to determine the optimal fantasy football lineups, where in my case there will be one QB, 2 RB, 3 WR, 1 TE, 1 DEFENSE. Each of the items (players) have a position and will need to fit that position. –  Nov 06 '15 at 23:07
  • To add a bit more information, in this particular case, each of the "items" will essentially be players, the weight would be their salary and value would be the points they score. The above script works perfectly fine except that I need to figure out how to break down players into groups. Without that, I may end up with 5 players that are QB where only 1 is allowed. –  Nov 06 '15 at 23:38

2 Answers2

3

That knapsack program is traditional, but I think that it obscures what's going on. Let me show you how the DP can be derived more straightforwardly from a brute force solution.

In Python (sorry; this is my scripting language of choice), a brute force solution could look like this. First, there's a function for generating all subsets with breadth-first search (this is important).

def all_subsets(S):  # brute force
    subsets_so_far = [()]
    for x in S:
        new_subsets = [subset + (x,) for subset in subsets_so_far]
        subsets_so_far.extend(new_subsets)
    return subsets_so_far

Then there's a function that returns True if the solution is valid (within budget and with a proper position breakdown) – call it is_valid_solution – and a function that, given a solution, returns the total player value (total_player_value). Assuming that players is the list of available players, the optimal solution is this.

max(filter(is_valid_solution, all_subsets(players)), key=total_player_value)

Now, for a DP, we add a function cull to all_subsets.

def best_subsets(S):  # DP
    subsets_so_far = [()]
    for x in S:
        new_subsets = [subset + (x,) for subset in subsets_so_far]
        subsets_so_far.extend(new_subsets)
        subsets_so_far = cull(subsets_so_far)  ### This is new.
    return subsets_so_far

What cull does is to throw away the partial solutions that are clearly not going to be missed in our search for an optimal solution. If the partial solution is already over budget, or if it already has too many players at one position, then it can safely be discarded. Let is_valid_partial_solution be a function that tests these conditions (it probably looks a lot like is_valid_solution). So far we have this.

def cull(subsets):  # INCOMPLETE!
    return filter(is_valid_partial_solution, subsets)

The other important test is that some partial solutions are just better than others. If two partial solutions have the same position breakdown (e.g., two forwards and a center) and cost the same, then we only need to keep the more valuable one. Let cost_and_position_breakdown take a solution and produce a string that encodes the specified attributes.

def cull(subsets):
    best_subset = {}  # empty dictionary/map
    for subset in filter(is_valid_partial_solution, subsets):
        key = cost_and_position_breakdown(subset)
        if (key not in best_subset or
            total_value(subset) > total_value(best_subset[key])):
            best_subset[key] = subset
    return best_subset.values()

That's it. There's a lot of optimization to be done here (e.g., throw away partial solutions for which there's a cheaper and more valuable partial solution; modify the data structures so that we aren't always computing the value and position breakdown from scratch and to reduce the storage costs), but it can be tackled incrementally.

David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
  • Apart from exponential complexity of brute force approach, there is no use of mysterious "item groups" that Brett struggles with. – Alex Blex Nov 06 '15 at 14:55
  • @AlexBlex 1. I'm not proposing to use brute force. 2. The item groups are hidden in the function `cost_and_position_breakdown`. – David Eisenstat Nov 06 '15 at 15:42
0

One potential small advantage with regard to composing recursive functions in PHP is that variables are passed by value (meaning a copy is made) rather than reference, which can save a step or two.

Perhaps you could better clarify what you are looking for by including a sample input and output. Here's an example that makes combinations from given groups - I'm not sure if that's your intention... I made the section accessing the partial result allow combinations with less value to be considered if their weight is lower - all of this can be changed to prune in the specific ways you would like.

function make_teams($players, $position_limits, $weights, $values, $max_weight){
  $player_counts = array_map(function($x){
                     return count($x);
                   }, $players);
  $positions = array_map(function($x){ 
                 $positions[] = []; 
               },$position_limits);
  $num_positions = count($positions);
  $combinations = [];
  $hash = [];
  $stack = [[$positions,0,0,0,0,0]];

  while (!empty($stack)){
    $params = array_pop($stack);
    $positions = $params[0];
    $i = $params[1];
    $j = $params[2];
    $len = $params[3];
    $weight = $params[4];
    $value = $params[5];

    // too heavy
    if ($weight > $max_weight){
      continue;

    // the variable, $positions, is accumulating so you can access the partial result
    } else if ($j == 0 && $i > 0){

      // remember weight and value after each position is chosen
      if (!isset($hash[$i])){
        $hash[$i] = [$weight,$value];

      // end thread if current value is lower for similar weight
      } else if ($weight >= $hash[$i][0] && $value < $hash[$i][1]){
        continue;

      // remember better weight and value
      } else if ($weight <= $hash[$i][0] && $value > $hash[$i][1]){
        $hash[$i] = [$weight,$value];
      }
    }

    // all positions have been filled
    if ($i == $num_positions){
      $positions[] = $weight;
      $positions[] = $value;

      if (!empty($combinations)){
        $last = &$combinations[count($combinations) - 1];
        if ($weight < $last[$num_positions] && $value > $last[$num_positions + 1]){
          $last = $positions;
        } else {
          $combinations[] = $positions;
        }
      } else {
        $combinations[] = $positions;
      }

    // current position is filled
    } else if (count($positions[$i]) == $position_limits[$i]){
      $stack[] = [$positions,$i + 1,0,$len,$weight,$value];

    // otherwise create two new threads: one with player $j added to
    // position $i, the other thread skipping player $j
    } else {
      if ($j < $player_counts[$i] - 1){
        $stack[] = [$positions,$i,$j + 1,$len,$weight,$value];
      }
      if ($j < $player_counts[$i]){
        $positions[$i][] = $players[$i][$j];
        $stack[] = [$positions,$i,$j + 1,$len + 1
                   ,$weight + $weights[$i][$j],$value + $values[$i][$j]];
      }
    }
  }
  return $combinations;
}

Output:

$players = [[1,2],[3,4,5],[6,7]];
$position_limits = [1,2,1];
$weights = [[2000000,1000000],[10000000,1000500,12000000],[5000000,1234567]];
$values = [[33,5],[78,23,10],[11,101]];
$max_weight = 20000000;

echo json_encode(make_teams($players, $position_limits, $weights, $values, $max_weight));

/*
[[[1],[3,4],[7],14235067,235],[[2],[3,4],[7],13235067,207]]
*/
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61