Part II: Generalized Branch and Bound

Armed with success for completing the 0-1 knapsack using a branch and bound approach, we now tackle the general form of branch and bound — and get to a very functional solution. So what about the 0-1 knapsack solver keeps it from being a generalized branch and bound solver? Well firstly, the variable definition includes the Weight and Utility fields which are specific to knapsack. The discrete variables only have states Unset, Zero and One which is fine assuming that the variables are binary. That is a discrete variable can also be one that takes on any value from a set of possibilities (possibly infinite as in ‘positive integers’), not just 0 or 1. Also, the branching part of the code makes the same assumption, substituting in Zero or One for Unset variables. This also effected the queue update in that we decided if none, zero or one, or both variable updates should be queued. The function used to evaluate the objective function (that is maximize the knapsack utility, the multiplySumUtility call) was explicitly invoked in the code. And finally the upper bound estimator (estimator) was also explicit in the routine. So there are lot’s of things that make this routine specific to the 0-1 knapsack — but its a good learning experience before trying to create a general solver (assuming our bottom-up approach) — and it does follow the general method of branch and bound.

Let’s start making some changes.

The first change is to make the objective function and estimator functions parameters of the branch and bound function. Functions can be arguments since functions are first-class objects in F# (F# is not the only language where this is true). We’d like to get to a branch and bound function that starts to look like this, and its invocation:

1: 
2: 
3: 
4: 
5: 
let BandB f g vars initLowestValue =
    (* TBD *)
    
(* invoke *)
let (solutionUtility, solutionVars) = BandB fknap gknap vars -1.0    

That is the objective function (f) and upperbound estimator (g) are to be passed in, assuming that they operate as being applied to vars. We already know the function f here for 0-1 knapsack, its just multiplySumUtility (or is it?), but we don’t have g yet. Well, g is estimator with the weight limit for a particular problem built in, we can use partial function application for this. Let’s see where this stands so far:

  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: 
 77: 
 78: 
 79: 
 80: 
 81: 
 82: 
 83: 
 84: 
 85: 
 86: 
 87: 
 88: 
 89: 
 90: 
 91: 
 92: 
 93: 
 94: 
 95: 
 96: 
 97: 
 98: 
 99: 
100: 
101: 
102: 
103: 
104: 
105: 
106: 
107: 
108: 
109: 
110: 
111: 
112: 
113: 
114: 
115: 
116: 
117: 
118: 
119: 
120: 
121: 
122: 
123: 
124: 
125: 
126: 
127: 
128: 
129: 
130: 
131: 
132: 
open System.Collections.Generic

(* Part II page 1 *)
type ZeroOneVarSetting =
    | Unset                         (* this is a discriminated union type in F# *)
    | One
    | Zero

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

let BandB f g vars initLowestValue =
    let mutable bestUtility = initLowestValue;
    let mutable queue = [vars]
    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 unsetVarIndex 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 unsetVarIndex = unsetVarIndex curVars 
            if not (unsetVarIndex = -1) then

                let varArrayZero = Array.copy curVars 
                                   |> Array.mapi (fun index var -> if index = unsetVarIndex then {var with Setting = Zero} else var)
                let varArrayOne  = Array.copy curVars 
                                   |> Array.mapi (fun index var -> if index = unsetVarIndex then {var with Setting = One} else var)

                let gEstimateZero = g varArrayZero
                let gEstimateOne = g 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, just take the rest!
                        
            else
                let util = f curVars         
                numberOfEvals <- numberOfEvals + 1
                if util > bestUtility then  (* NOT GOOD AS WE LOST THE WEIGHT LIMIT *)
                    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)

(* Some of this might be Visual Studio specific, you may need to adjust for other platforms *)
[<EntryPoint>]
let main argv = 
    (* DEFINE vars and functions for 0-1 knapsack *)

                 (* These are deduced to be DiscreteVar record types *)
    let vars = [|{Name = "food"; Setting = Unset; Weight = 5.0; Utility = 8.0}; 
                 {Name = "tent"; Setting = Unset; Weight = 14.5; Utility = 5.0}; 
                 {Name = "gps"; Setting = Unset; Weight = 1.0; Utility = 3.0}; 
                 {Name = "map"; Setting = Unset; Weight = 0.5; Utility = 3.0} |]
    let weightLimit = 16.0

    let multiplySumEither b v = 
        Array.map (fun vElem -> (if b then vElem.Utility else vElem.Weight) * match vElem.Setting with
                                                                              | One -> 1.0
                                                                              | _ -> 0.0 ) v |> Array.sum
    let multiplySumWeight = multiplySumEither false
    let multiplySumUtility = multiplySumEither true

    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  
                                       | (_,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

    let gknap = estimator weightLimit   // this is new

    let (solutionUtility, solutionVars) = BandB multiplySumUtility gknap vars -1.0
    printfn "Best Utility = %f" solutionUtility
    for var in solutionVars do
        printfn "%A" var

    System.Console.ReadKey() |> ignore  // don't close the output window until a key is hit
    0
  

Notice the appearance of the now-generalized objective function f on line 72 and of the upper bound estimator on lines 55 and 56. These are defined in the invocation at line 125. This is not a bad change, but we see the issue on line 74. This line used to only take the improved value for f if the weight of f was within wlimit. But we removed wlimit as an argument baking that into the g function. wlimit is a parameter of the 0-1 knapsack but not of general branch and bound optimization problems so we don’t want to add it back to the argument list of BandB, but what if we baked that into f just as we did for g?

This idea will be changed later, but this shows additional usage of partial function application

Well, we don’t want f to be acceptable if the weight limit is not met, so what if we re-created a new f that returns -infinity (or the smallest negative number) if the weight limit is not met? So our new portion of the main code would be adapted as:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
let gknap = estimator weightLimit   // this is new
let knapLimited wlimit vars =
    let w = multiplySumWeight vars
    if w <= wlimit then (multiplySumUtility vars) else System.Double.NegativeInfinity

let fknap = knapLimited weightLimit

let (solutionUtility, solutionVars) = BandB fknap gknap vars -1.0
printfn "Best Utility = %f" solutionUtility
for var in solutionVars do
    printfn "%A" var   

The knapLimited function does just what we thought, and we then create fknap using it with the particular weight limit for our problem instance baked in. However, when we run this we still have a problem … recall that the estimator for 0-1 knapsack required the variables in decreasing order of utility/weight ratio. The user variables as defined are not in such an order. We could fix this by sorting the var array before passing it to BandB but this doesn’t seem right. The general BandB routine should not require variables in a particular order, although it is perfectly fair that the f and g functions for a particular problem instance type would.

1: 
2: 
let varsSorted = (Array.sortBy (fun elem -> -elem.Utility / elem.Weight) vars)
let (solutionUtility, solutionVars) = BandB fknap gknap varsSorted -1.0  

Nevertheless, the above will fix the problem, but let’s revisit this later; on to page 2