Learning F#: Case study with branch and bound – PART II

Learning F#: Case study with branch and bound – PART II

OpCoast © 2016

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 () and upperbound estimator () are to be passed in, assuming that they operate as being applied to vars. We already know the function here for 0-1 knapsack, its just multiplySumUtility (or is it?), but we don’t have yet. Well, 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    | Zerotype 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 = curVars                                    |> Array.mapi (fun index var -> if index = unsetVarIndex then {var with Setting = Zero} else var)                let varArrayOne  = 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 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 if the weight of was within wlimit. But we removed wlimit as an argument baking that into the 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 just as we did for

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

Well, we don’t want to be acceptable if the weight limit is not met, so what if we re-created a new 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 newlet knapLimited wlimit vars =    let w = multiplySumWeight vars    if w <= wlimit then (multiplySumUtility vars) else System.Double.NegativeInfinitylet fknap = knapLimited weightLimitlet (solutionUtility, solutionVars) = BandB fknap gknap vars -1.0printfn "Best Utility = %f" solutionUtilityfor 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  

Download code so far

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