Before implementing a full branch and bound method, let’s start by first developing an exhaustive searcher, that is one that simply elaborates all possible variable settings, and selects the best solution from those. Rather than use a recursive approach, we will use a queueing approach since that is what we’ll need for the branch and bound later. Consider this code:
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: |
let exhaustiveSearcher vars initLowestValue = let mutable bestUtility = initLowestValue; let mutable queue = [vars] // queue is a list of vars, where vars are an array of DiscreteVar (assume all Unset) let mutable numberOfQueues = 1 // we just queued the start, so 1 (* find an unset var *) let findUnsetVarIndex vars = Array.findIndex (fun var -> var.Setting = Unset) vars // will raise exception if not found (* any var unset ? *) let anyUnsetVars vars = Array.fold (fun acc var -> acc || var.Setting = Unset) false vars // will raise exception if not found while not queue.IsEmpty do match queue with | queueHead :: restOfQueue -> if anyUnsetVars queueHead then let varIndex = findUnsetVarIndex queueHead // queueHead is an array of DiscreteVar let tvar = Array.get queueHead varIndex let zeroVar = { tvar with Setting = Zero } // change the unset var to One and Zero let varArrayZero = Array.copy queueHead // copy the array so we can change element Array.set varArrayZero varIndex zeroVar let oneVar = { tvar with Setting = One } let varArrayOne = Array.copy queueHead // copy the array so we can change element Array.set varArrayOne varIndex oneVar queue <- varArrayZero :: varArrayOne :: restOfQueue numberOfQueues <- numberOfQueues + 2 // added two the queue else // all vars have a setting! evaluate it let util = multiplySumUtility queueHead if util > bestUtility then bestUtility <- util queue <- restOfQueue // we've processed queueHead, take it off the queue () | _ -> () // queue isempty, do nothing printfn "Total settings queued = %d" numberOfQueues bestUtility |
In F# mutable variables are reassigned using the <-
syntax. ref variables are reassigned using the :=
syntax
The function takes as arguments the DiscreteVar
array and an initial value which is presumed to be less than all possible valid utility evaluations; it is used to initialize bestUtility
. Since we will be altering bestUtility, queue
, and numberOfQueues
during the algorithm, they are defined as mutable
(i.e., changeable). We next define two useful functions, findUnsetVarIndex
and anyUnsetVars
each taking an argument of DiscreteVar
array. findUnsetVarIndex
searches the var array to find the index of a variable using the Array.find
function that is Unset
, it will throw an exception if there is no such element found. The anyUnsetVars
method returns true if there are any vars in the input array that are Unset
. Starting with a value of false
the Array.fold
function applies the given (lambda, unnamed) function to the accumulated value (acc) and each element; in this case the boolean value of testing each elem for Unset
.
The empty match pattern at the end of the pattern match is unnecessary as the loop conditional test ensures that the queue is not empty, but all patterns must be matched
Next comes the main loop involving the queue. While the queue is not empty, we use pattern matching to get the head of the queue queueHead
and the remainder of the queue restOfQueue
(otherwise we do nothing as in the default pattern match). We first test the queueHead
(which is an array of DiscreteVar
) to see if any of its member variables are Unset
, if so we are effectively taking the unset variable and creating two copies of the array, one of which changes the unset variable to Zero
and the other to One
. These new arrays, along with the restOfQueue
become the new queue. If, on the otherhand, all the variables are set, we use our multiplySumUtility
function from the last page to evaluate the setting. If this utility value is better than the one we currently have we update the current best found. Being able to change an element using Array.set
is why we chose to use arrays, rather than lists, for our variable input.
If we apply the exhaustiveSearcher
function like so:
1: 2: 3: 4: 5: 6: 7: 8: 9: |
let vars = [|{Name = "food"; Setting = Unset; Weight = 5.0; Utility = 8.0}; (* These are deduced to be DiscreteVar record types *) {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 totalUtility = exhaustiveSearcher vars -1.0 printfn "Best utility (ignores weight restriction): %f" totalUtility |
We will get the following output:
1: 2: |
Total settings queued = 31 Best utility (ignores weight restriction): 19.000000 |
First of all, we notice that the total utility is 19.0, which is the sum of all the utilities — makes sense as there is nothing in the elaborator that limits the best value in regard to weights, so it finds the solution where every value is set to One
— bingo maximum utility. We also note that the queue count is 31 for n = 4, which is exactly 2n+1 – 1 which is what it should be. We have queued all the nodes in the search tree.
We are not very focussed on algorithmic efficiency here, except where it provides an opportunity to explore F#.
Let’s make some improvements, first we want to get the actual solution, not just the total utility of the maximum. Secondly notice that both findUnsetVarIndex
and anyUnsetVars
are basically running through the vars, one determining if all the vars are unset and the other finding an index for an unset variable. They seem like they could be combined into a single fucntion, but we need to return two values from the function. Returning multiple values is done using a tuple.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: |
(* return tuple of: (any-var-unset, index-of-unset-var) *) let unsetVar vars = let mutable index = 0 // we can't use Array.find since when all the vars are set it would throw exception let mutable retValue = (false, -1) for var in vars do // still go through the whole array after finding the one we want. match var.Setting with | Unset -> retValue <- if not (fst retValue) then (true, index) else retValue | _ -> () // do nothing index <- index + 1 retValue |
In this code, we created the function unsetVar
which returns the tuple of ‘are any vars unset’ (bool) and ‘index of unset var’ (int). It works by iterating through the vars matching any that have a setting of Unset. If a var does, and its the first one found then we set our return tuple to (true, index)
. Note that just as in the Array.fold and Array.findIndex methods before, we scan through the whole array everytime, even after finding what we want. If we did this in a more traditional way, we would exit the loop with (true, index)
as soon as we found it. We could also just return a single int value where we interpret -1 as Unset variable not found. And we could do this with the Array.findIndex function as well as long as we catch the exception for no unset varaibles in the array. We will consider this issue later, but at least now we only go through the variable array once and see how a tuple might be used as a return value.
Next is the revised code that makes use of this new function, and also declares the bestVars
mutable variable to hold the best setting found. In the revised code for the while loop below, the let
statement on line 22 is used to capture the return tuple from unsetVar
, placing each into a local variable. These are then used just as before.
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: |
let exhaustiveSearcher wlimit vars initLowestValue = let mutable bestUtility = initLowestValue; let mutable queue = [vars] // queue is a list of vars, where vars are an array of DiscreteVar (assume all Unset) let mutable numberOfQueues = 1 // we just queued the start, so 1 let mutable bestVars = [| |] // best var settings found. (* return tuple of: (any-var-unset, index-of-unset-var) *) let unsetVar vars = let mutable index = 0 // we can't use Array.find since when all the vars are set it would throw exception let mutable retValue = (false, -1) for var in vars do // still go through the whole array after finding the one we want. match var.Setting with | Unset -> retValue <- if not (fst retValue) then (true, index) else retValue | _ -> () // do nothing index <- index + 1 retValue while not queue.IsEmpty do match queue with | queueHead :: restOfQueue -> let (anyUnset,varIndex) = unsetVar queueHead if anyUnset then let tvar = Array.get queueHead varIndex // change the unset var to One and Zero let zeroVar = { tvar with Setting = Zero } let varArrayZero = Array.copy queueHead // copy the array so we can change element Array.set varArrayZero varIndex zeroVar let oneVar = { tvar with Setting = One } let varArrayOne = Array.copy queueHead // copy the array so we can change element Array.set varArrayOne varIndex oneVar queue <- varArrayZero :: varArrayOne :: restOfQueue numberOfQueues <- numberOfQueues + 2 // added two the queue else // all vars have a setting! evaluate it let util = multiplySumUtility queueHead let wt = multiplySumWeight queueHead if util > bestUtility && wt <= wlimit then bestUtility <- util bestVars <- queueHead queue <- restOfQueue // we've processed queueHead, take it off the queue | _ -> () // queue isempty, do nothing printfn "Total settings queued = %d" numberOfQueues (bestUtility, bestVars) |
Another change was also made. On line 40, the computed weight for the assignment is computed (using our old function) and the new best utility total is not updated unless thus weight is less than or equal to the weight limit, which is a new argument to the exhaustiveSearcher
. Now we still exhuastively search all possible combinations of variables, but only select the best one that is feasible, that is makes the weight limit. You might also notice that the functions multiplySumUtility
and multiplySumWeight
are used in the searcher in a somewhat non-modular way, that is they are assumed to exist in the environment. This is not necessarily an issue, but we will be revisiting this later as well.
Don’t try to run this exhaustive searcher with more than 8 to 12 variables, it will get mighty slow
If we execute this code, using the following:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: |
let vars = [|{Name = "food"; Setting = Unset; Weight = 5.0; Utility = 8.0}; (* These are deduced to be DiscreteVar record types *) {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 totalUtility,bestVarSetting = exhaustiveSearcher weightLimit vars -1.0 printfn "Best utility (ignores weight restriction): %f" totalUtility for var in bestVarSetting do printfn "%A " var |
We get the following. This is a best knapsack that is under the 16.0 unit weight limit, the food, gps and map are selected, no tent.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: |
Total settings queued = 31 Best utility (ignores weight restriction): 14.000000 {Name = "food"; Weight = 5.0; Utility = 8.0; Setting = One;} {Name = "tent"; Weight = 14.5; Utility = 5.0; Setting = Zero;} {Name = "gps"; Weight = 1.0; Utility = 3.0; Setting = One;} {Name = "map"; Weight = 0.5; Utility = 3.0; Setting = One;} |