Learning F#: Case study with branch and bound – Part I (page 3)

But before we get back to coding, let’s understand a bit more about branch and bound. In the figure, we have a three-variable problem (binary in that each variable has two set positions, for our 0-1 Knapsack these are 0 and 1 of course). Application of branch and bound to this problem dates back many years. In Part II, we will develop branch and bound more generally but applying it first to the 0-1 knapsack will aid our incremental development in F#.

Branch and Bound Illustration

Suppose that the function g can tell you (at least estimate) what the best possible solution (max in our case) might be for any solution combination of variables given the ones that are set already. For example, in the figure we see that the value of g once we have selected v0 to be 0 is G=79. Suppose further that via our exploration of the variable settings we have determined a solution with utility of U=90 (following the red line here, this would be for settings of v0 = 1, v1 = 0, and v2 = 0). Since we have already found a solution with a utility of 90, there is no reason to explore the branch starting at the node where g is 79 as there are no solutions there that can be better than 79. This assumes of course that our g estimate is good, namely that it doesn’t underestimate the potential solutions for any subsequent variable settings. The tighter g is to the actual best solution within its sub-space the better, but the method works as long as the estimator does not underestimate.

This is the basis for search savings in branch and bound, if we have estimators that can give what the best possible solution is for subsets of variables (at least an estimator that doesn’t underestimate possible solutions) then once we start finding actual solutions we don’t need to explore spaces that cannot be any better. As reference, this web video walks through the branch and bound procedure for 0-1 knapsack; we will move on with coding.

This wiki page gives a generic (pseudo code) version of branch and bound, although this version assumes that we want to minimize the objective function not maximize as we are for our knapsack. We will see later that handling either maximizing or minimizing is not a problem, but for now let’s focus on maximizing.

Utility estimate of a knapsack with partial variable settings

So in order for our branch and bound routine to work, we need to estimate the best value that a partial variable setting would have. In the figure above, this would be the ‘G’ values at each node. Recall that each node is a setting of variables such that each has one of the Unset, Zero or One value. In the example, we somehow determined that after setting v0 = 0 that G=79 (this is an arbitrary number for demonstration). How can we get such an estimate for our problem? We already have a function multiplySumUtility that computes the utility of a knapsack given selections, and a function that determines the weight of such as well.

Without getting into the details, our estimator can work like this:

  1. Sort the variables in order of best utility to weight ratio
  2. Determine the utility of the variables that have already been assigned, call this assignedUtility
  3. Determine the weight of the assigned variables, and set remainingWeight to the weight limit less this weight
  4. For the Unset variables var (in order according to step 1), and while the remainingWeight is > 0:
    • if var.Weight <= remainingWeight, add var.Weight to unassignedUtilityEstimate and reduce remainingWeight by this weight
    • otherwise take a fraction of this items value: let weightFrac = remainingWeight / var.Weight; add weightFrac*var.Utility to unassignedUtilityEstimate; set remainingWeight = 0

There are other forms of knapsack, like fractional and unbounded, but aren’t considering them now

This estimates the utility of a node as: the sum of the existing utility for variables that have been assigned plus the best possible value of those that have not. This is done by adding the best possible choices in terms of utility to weight ratio including a fractional one at the end. Of course in our real knapsack we can only choose or not choose an item, but this works as our estimator, which cannot be less that the actual utility for the node. That is, for these settings this is the best that could possibly be obtained for any sub-node in the search tree.

So let’s consider the F# coding for this:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
let estimator wlimit vars =
    // assume vars are sorted in decreasing utility/weight ratio
    let assignedUtility = multiplySumUtility vars
    let mutable unassignedUtilityEstimate = 0.0
    let mutable remainingWeight = wlimit - multiplySumWeight vars
    Array.iter (fun var -> if remainingWeight > 0.0 then do
                             if var.Weight <= remainingWeight then do
                               remainingWeight <- remainingWeight - var.Weight
                               unassignedUtilityEstimate <- unassignedUtilityEstimate + var.Utility
                             else do
                               let frac = var.Weight/remainingWeight
                               unassignedUtilityEstimate <- unassignedUtilityEstimate + frac * var.Utility
                               remainingWeight <- 0.0
                             ) vars

    assignedUtility + unassignedUtilityEstimate  

This seems to be on track, we basically use the Array.iter to go through each var, in decreasing order of utility to weight ratio (assumed), an adjust the mutable values. But, this doesn’t work — basically saying that mutable variables cannot be used in a closure. Well, the closure is the unnamed function we used as the Array.iter argument and of course the mutable variables are those we defined. We can fix this using ref variables instead as so:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
let estimator wlimit vars =
    // assume vars are sorted in decreasing utility/weight ratio
    let assignedUtility = multiplySumUtility vars // value from set vars
    let unassignedUtilityEstimate = ref 0.0
    let remainingWeight = ref (wlimit - multiplySumWeight vars)
    Array.iter (fun var -> if !remainingWeight > 0.0 && var.Setting = Unset then do
                             if var.Weight <= !remainingWeight then do
                               remainingWeight := !remainingWeight - var.Weight
                               unassignedUtilityEstimate := !unassignedUtilityEstimate + var.Utility
                             else do
                               let frac = var.Weight / !remainingWeight
                               unassignedUtilityEstimate := !unassignedUtilityEstimate + frac * var.Utility
                               remainingWeight := 0.0
                             ) vars

    assignedUtility + !unassignedUtilityEstimate   

Ref variables can be used in closures, recall these are assigned using the := and accessed using the ! operator as seen. But is this the right way to go? Idiomatic F# tries to stay away from ref variables (although of course sometimes they are required and the best way to do things), so if we need ref variables we should at least think about other ways to do what we need. What if we used an Array.fold to ‘accumulate’ a result across the elements, like:

Is this version clearer in terms of easily seeing what the routine does?

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
let estimator wlimit vars =
    // assume vars are sorted in decreasing utility/weight ratio
    let assignedUtility = multiplySumUtility vars
    let remainingWeight = wlimit - multiplySumWeight vars
    let (leftOverWeight,unassignedUtilityEstimate) = 
        Array.fold (fun acc var -> match acc,var.Setting with
                                   | (_,accUtil),Zero   // we can't use _,accUtil,Zero here - that's a 3-tuple 
                                   | (_,accUtil),One
                                   | (0.0,accUtil),_ -> (fst acc, accUtil)
                                   | (accWt,accUtil),varSetting -> 
                                       if accWt >= var.Weight then
                                          (accWt - var.Weight, accUtil + var.Utility)
                                       else 
                                          let frac = var.Weight / accWt
                                          (0.0, accUtil + frac * var.Utility)
                   ) (remainingWeight,0.0) vars

    assignedUtility + unassignedUtilityEstimate  

F# note: The fst function returns the first element of a tuple

The Array.fold function uses a base element, here the tuple (remainingWeight,0.0) and applies the given function to each element threading an accumulator through the result (called acc here) [yes, this gives the same result as before]. The tuple is a pair such that the first part is the remainingWeight (or ‘leftOverWeight’ as it adjusts) and the second is the estimated value. Now our function uses pattern matching to determine the next accumulator return value. The idea of the pattern is: if the variable is not Unset (e.g. Zero or One) or if the accumulator’s first value (the leftOverWeight) is zero, then just pass along the acc as is, we don’t want to change it — we are only adding contributions here from un-set variables. Otherwise, apply our calculation for adjusting each of the leftOverWeight and estimated utility value.

F# note: the order of pattern matching matters

Notice that in the pattern match we had to be careful to match each of the acc and var.Setting elements properly. For example, the parens in (_,accUtil),Zero are needed so that the parenthetical part matches acc and the Zero matches var.Setting, if we tried to use _,accUtil,Zero it would be an error since that is a 3-tuple and not a tuple of a 2-tuple and a DiscreteVar variable — which is what acc,var.Setting is.

0-1 Knapsack branch and bound

Finally, with our estimator in place and some adjustments to the exhuastive search code we’ll do here, we can tackle branch and bound.

F# note: Here is a handy operator reference

We still need to determine variables that are unset, so for the purposes of covering more F#, let’s revisit the unsetVar function from before. That returned a tuple of ‘any-unset, index-of-unset’ basically. As was hinted, instead let’s just return an int such that if it is -1 then we interpret that as no vars are unset. And let’s explore using Array.findIndex, but make sure we capture the exception thrown:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
open System.Collections.Generic

(* OTHER CODE *)

(* return index of unset (first) var, or -1 if all are set (to One or Zero) *)
let unsetVar vars =
    try
        Array.findIndex (fun var -> var.Setting = Unset) vars
    with
        | :? KeyNotFoundException -> -1 

As in other computer languages, F# uses a try/catch approach (using keywords try and with however). The new unsetVar routine uses the Array.findIndex with a predicate (condition) of looking for an Unset variable. It will throw an exception of KeyNotFoundException if not found which is caught in the with clause. Note that the System.Collections.Generic library must be included (this is done with the open keyword) in order to bring the KeyNotFoundException name into scope. The :? operator returns true if the argument matches and otherwise false, so if a KeyNotFoundException exception is raised it will be caught. Our new unsetVar routine is simpler and problably more readily understood than before.

Since we used a queueing style, we can readily adapt our exhaustive searcher to a branch and bound solver. We just need to incorporate the estimator function such that it prevents partial solutions that are already worse than a solution we have from going on the queue. Here it is, with our new unsetVar function.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
44: 
45: 
46: 
47: 
48: 
49: 
50: 
51: 
52: 
53: 
54: 
55: 
56: 
57: 
58: 
59: 
60: 
61: 
let knapsack01 wlimit vars initLowestValue =
// make sure that vars are sorted in utility/weight descending order [Note: minus sign]
let varsSorted = Array.sortBy (fun elem -> -elem.Utility / elem.Weight) vars   
// same as exhaustive
let mutable bestUtility = initLowestValue;
let mutable queue = [varsSorted]
let mutable bestVars = [| |]     // best var settings found, nothing so far.
// these are only Informational, not needed in the routine
let mutable numberOfQueues = 1   // we just queued the start, so 1
let mutable numberOfEvals = 0    // number of times we evaluated the function with all vars set
(* return index of unset (first) var, or -1 if all are set (to One or Zero) *)
let unsetVar vars =
try
Array.findIndex (fun var -> var.Setting = Unset) vars
with
| :? KeyNotFoundException -> -1
while not queue.IsEmpty do
match queue with
| curVars :: restOfQueue -> 
let varIndex = unsetVar curVars 
if not (varIndex = -1) then
let mutable newQueue = restOfQueue  // if the 'g' funcs pass later, we'll add to the queue, 
// otherwise its just the rest of the queue
let tvar = Array.get curVars varIndex
// change the unset var to One and Zero
let zeroVar = { tvar with Setting = Zero }
let varArrayZero = Array.copy curVars  // copy the array so we can change element
Array.set varArrayZero varIndex zeroVar
let gEstimateZero = estimator wlimit varArrayZero
if gEstimateZero > bestUtility then 
newQueue <- varArrayZero :: newQueue
numberOfQueues <- numberOfQueues + 1
let oneVar = { tvar with Setting = One }
let varArrayOne = Array.copy curVars  // copy the array so we can change element
Array.set varArrayOne varIndex oneVar
let gEstimateOne = estimator wlimit varArrayOne
if gEstimateOne > bestUtility then 
newQueue <- varArrayOne :: newQueue
numberOfQueues <- numberOfQueues + 1
queue <- newQueue   // set the queue to the new queue
else
let util = multiplySumUtility curVars
let wt = multiplySumWeight curVars
numberOfEvals <- numberOfEvals + 1
if util > bestUtility && wt <= wlimit then
bestUtility <- util
bestVars <- curVars
queue <- restOfQueue // we've processed curVars, take it off the queue
| _ -> ()  // queue isempty, do nothing
printfn "Total settings queued = %d, total number of evals: %d" numberOfQueues numberOfEvals
(bestUtility, bestVars) 

Running this, we get the same solution as our exhaustive solver. However, now we’ve only visited 16 nodes (versus 31 before) and needed to evaluate 4 full settings (versus all 16 combinations before).

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
Total settings queued = 16, total number of evals: 4
Best Utility = 14.000000
{Name = "map";
Weight = 0.5;
Utility = 3.0;
Setting = One;}
{Name = "gps";
Weight = 1.0;
Utility = 3.0;
Setting = One;}
{Name = "food";
Weight = 5.0;
Utility = 8.0;
Setting = One;}
{Name = "tent";
Weight = 14.5;
Utility = 5.0;
Setting = Zero;}  

Our code is getting a bit complex and probably should be tested and timed for improvements, but that is a subject for Part III. Let’s take a look at other improvements. The code concerning adapting the queue is a bit convoluted, are there better ways to handle this? We have seen pattern matching against discriminated unions (to determine which setting applies), and for given values, but there is another option active patterns. An active pattern uses code execution to determine which case applies.

Take a look at the code on lines 22-30 below. This is an active pattern definition that takes a 3-tuple as an argument and matches to four cases, TakeBoth, TakeZero, TakeOne or TakeNeither. The if expression decides which pattern to match based on the argument. In this case, for example, if both gZero (this will be the estimate for the Zero case in the body of the code) and gOne are greater than the bestSoFar then TakeBoth is matched. A similar explanation applies to the other cases, and the discussion of its application follows.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
44: 
45: 
46: 
47: 
48: 
49: 
50: 
51: 
52: 
53: 
54: 
55: 
56: 
57: 
58: 
59: 
60: 
61: 
62: 
63: 
64: 
65: 
66: 
67: 
68: 
69: 
70: 
71: 
72: 
73: 
74: 
75: 
76: 
let knapsack01 wlimit vars initLowestValue =
// make sure that vars are sorted in utility/weight descending order [Note: minus sign]
let varsSorted = Array.sortBy (fun elem -> -elem.Utility / elem.Weight) vars   
// same as exhaustive
let mutable bestUtility = initLowestValue;
let mutable queue = [varsSorted]
let mutable bestVars = [| |]     // best var settings found, nothing so far.
// these are only Informational, not needed in the routine
let mutable numberOfQueues = 1   // we just queued the start, so 1
let mutable numberOfEvals = 0    // number of times we evaluated the function with all vars set
(* return index of unset (first) var, or -1 if all are set (to One or Zero) *)
let unsetVar vars =
try
Array.findIndex (fun var -> var.Setting = Unset) vars
with
| :? KeyNotFoundException -> -1
(* active patterns *)
let (|TakeBoth|TakeZeroOnly|TakeOneOnly|TakeNeither|) (gZero, gOne, bestSoFar) = 
if gZero > bestSoFar && gOne > bestSoFar then
TakeBoth
else if gZero > bestSoFar then
TakeZeroOnly
else if gOne > bestSoFar then
TakeOneOnly
else
TakeNeither
while not queue.IsEmpty do
match queue with
| curVars :: restOfQueue -> 
let varIndex = unsetVar curVars 
if not (varIndex = -1) then
let tvar = Array.get curVars varIndex
// change the unset var to One and Zero
let zeroVar = { tvar with Setting = Zero }
let varArrayZero = Array.copy curVars  // copy the array so we can change element
Array.set varArrayZero varIndex zeroVar
let gEstimateZero = estimator wlimit varArrayZero
let oneVar = { tvar with Setting = One }
let varArrayOne = Array.copy curVars  // copy the array so we can change element
Array.set varArrayOne varIndex oneVar
let gEstimateOne = estimator wlimit varArrayOne
// update the queue
queue <- match (gEstimateZero, gEstimateOne, bestUtility) with
| TakeBoth -> 
numberOfQueues <- numberOfQueues + 2
varArrayOne :: varArrayZero :: restOfQueue
| TakeZeroOnly -> 
numberOfQueues <- numberOfQueues + 1
varArrayZero :: restOfQueue
| TakeOneOnly -> 
numberOfQueues <- numberOfQueues + 1
varArrayOne :: restOfQueue
| _ -> restOfQueue // Neither case
else
let util = multiplySumUtility curVars
let wt = multiplySumWeight curVars
numberOfEvals <- numberOfEvals + 1
if util > bestUtility && wt <= wlimit then
bestUtility <- util
bestVars <- curVars
queue <- restOfQueue // we've processed curVars, take it off the queue
| _ -> ()  // queue isempty, do nothing
printfn "Total settings queued = %d, total number of evals: %d" numberOfQueues numberOfEvals
(bestUtility, bestVars)  

??? I think the active pattern version might be a bit clearer, but it relies on also understanding the active pattern. Is there still a better way?

Lines 51-61 show use if the active pattern, here the queue is updated depending on which case (active pattern) applies. Perhaps this is more readily understandable than the previous approach?

Also to circle back to an earlier decision, we made the Setting field of DiscreteVar mutable with the idea that we would need to change it, but we never did. Note that we create copies of an unset var (called tvar in the code) and use the with clause to change the setting to a new DiscreteVar. So we never actually update the Setting in place, so here is the minor update:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
type DiscreteVar = 
{                               (* this is a record type in F# *)
Name : string;
Weight : float;
Utility : float;
Setting : ZeroOneVarSetting;         // discrete value setting
}