Learning F#: Case study with branch and bound – PART I
OpCoast © 2016
Part I: 0-1 Knapsack
Before delving into the more general branch and bound algorithm in F#, let’s try one to implement the simpler 0-1 Knapsack problem using branch and bound techniques. This problem mimics the idea of a person trying to pack a limited weight (or space) bag (knapsack) with items such that their total value (utility) is maximized. Mathematically, it is formulated as:
0-1 Knapsack definition
Here, there are variables each denoted as . Likewise there are weights associated with each item and utility (value) settings for each, denoted and respectively. Note that the terminology used here differs from the Wiki link above in that variable, weight and utility indices go from 0 to -1 to be more in line with computer array indexing and the value is denoted with instead of
0-1 Knapsack has a pseudo-polynomial solution approach, but here we are focused on applying a branch and bound approach. Later we will look at other problem types.
Short Description of Branch and Bound
Before getting back to the case of the knapsack problem it will help to understand how branch and bound works. Consider the following search tree diagram:
Branch and Bound Illustration
Deciding on the selection of each variable can be viewed as a tree structure, where each branch is a variable selection and each node represents the setting (or non-setting) of each variable. At the top of the tree, all of the variables are unset. In the drawing, we have a case of three variables (indexed from 0 to 2). At each level of the tree one variable is decided (set). As an example, if you follow the red line from the top to the bottom, the variables are set to = 0, = 1, and = 1. At the bottom level, all of the variables are set and therefore a value of the knapsack and a weight of the knapsack can be computed (see Eq 1). At any intermediate node other than the bottom layer, at least one variable remains to be set. There are 2 nodes at the bottom layer, that is all the combinations of variables, in the figure =3 so there are 8 in that case. And there are 2n+1-1 nodes in the whole graph for any variables.
Notice that the nodes at the bottom are labeled with ‘U’ and the rest as ‘G’. This is meant to imply that nodes with complete settings have a particular utility while the rest (the ‘G’s) have only estimates; we will later be using a function named hence the G moniker, but that is getting ahead of ourselves.
Programming
A first step is deciding how to represent the problem domain in code or the type system. We see that we have three arrays to represent, each with a cardinality of . The weights and utilities could be either integers or floating point values, let’s choose the more inclusive floating point (float
in F#). The variables of our domain can be either zero or one, or in the case of branch and bound unset. This leads us to determine how to represent (the type) the variables of our problem. We have decided to type the variables as DiscreteVar
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: |
(* this is setting for a variable of the problem *)type ZeroOneVarSetting = | Unset (* this is a discriminated union type in F# *) | One | Zero(* this is a variable of the problem. Each var has a Name and mutable v*)type DiscreteVar = { (* this is a record type in F# *) Name : string; mutable Setting : ZeroOneVarSetting; // discrete value setting } |
I assume that you know the basics of F#
We used a discriminated union type to capture the possible settings for a variable as either Unset, Zero or One, makes sense. Then, the DiscreteVar
is a record type with a Name and Settings that use this type. To define a problem instance, we might use something like:
Arrays are denoted with [| |]
while lists are denoted as [ ]
, each with elements separated with semicolons.
1: 2: 3: 4: 5: 6: 7: |
let vars = [|{Name = "food"; Setting = Unset}; {Name = "tent"; Setting = Unset}; {Name = "gps"; Setting = Unset }; {Name = "map"; Setting = Unset} |] let weights = [|5.0; 14.5; 1.0; 0.5 |] let utilities = [| 8.0; 5.0; 3.0; 3.0 |] let weightLimit = 20.0 |
Notice that each of vars, weights and utilities are array types. Another choice might be to make them lists. F# arrays allow for modification of an element, while lists do not. If we want to change an element of a list we need to extract the elements leading up to the element to be changed, concatenate this with the new element and then concatenate the rest and assign this to a new variable (see topic). Array sizes are fixed, but that works since once we define a problem set (as above) the size of these arrays doesn’t need to change. We make the setting for the variable mutable since we think we need to change it later. These are our ideas at this point anyway.
The approach throughout is to start with something and refine it as we go along.
Notice that we never defined the type of element that vars
contains. F# deduces that the type of element is a record type and furthermore that the type is DiscreteVar
based on seeing that these record names match in these record expressions (see topic). OK, let’s write a function to determine the total utility.
1: 2: 3: 4: 5: 6: |
let utility u v = let products = Array.map2 (fun uElem vElem -> uElem * vElem) u v Array.sum products let tv = utility utilities vars |
Line 1 defines a function named utility
which takes two arguments named and
. At this point we haven’t declared what types
and
are, and the compiler deduces their type from the body of the function and from subsequent uses. In fact, if you put that code into F#, the utility definition is fine until you add the invocation at line 5. Before adding line 5, the compiler thinks of the function as:
Type Deduction
That is, it knows that and
are arrays, given their appearance as arguments to the
Array.map2
function; it also knows that the array elements must allow the (*)
operator so it chooses int
as the array element type sans other information. Recall that Array.map2
takes as arguments a function that accepts two arguments and produces a value; the two arrays which must be of the same length. However, when we add the line 5 content, an error arises:
Type Mis-Deduction
Now, the compiler sees that the argument is a float array, and then sees that it cannot apply the
(*)
operator to a float and a DiscreteVar
(since is a
DiscreteVar
array in this instance). So, its important to realize that function and data type deduction ‘looks’ at the whole content, including code that comes later. Function arguments that are not explicitly typed are generic arguments (think ala C++ templates but without the template keywording); these will prove very useful as they allow definition of functions that are flexible in their argument types. But enough about general F# typing for now, let’s fix our problem.
TIP: You can explicitly type function arguments to force their interpretion within the function body, and then fix invocations. Once all that works you can try to remove these declarations to make the code neater and more flexible — although sometimes F# does need type ‘hints’
So how are we going to develop utility
? Let’s re-write utility
such that it accepts an array of floats and an array of DiscreteVar
like so …
1: 2: 3: 4: 5: 6: 7: 8: |
let utility u v = let products = Array.map2 (fun uElem vElem -> uElem * match vElem.Setting with | One -> 1.0 | _ -> 0.0 ) u v Array.sum products let tv = utility utilities vars printfn "%f" tv |
In the mapping function, the second argument is still a DiscreteVar
but we use pattern matching to derive a float value from the discriminated union type. In this case, the pattern One
produces a 1.0 and otherwise a 0.0 results since Unset
and Zero
will match the default pattern. The result is that
products
is an array of floats such that it is the pairwise product of the array and
1.0
whereever vElem.Setting
is One
. Each member of this result (which is a float array
) is then summed using the Array.sum
method to produce a float result (remember that the final expression of a function is its return value). If we run this and examine the result as is we get 0.0, of course this is because all of the variables are Unset
and thus a multicand of 0.0 is produced for each vElem
in the expression.
We next realize that multiplying weights and the variables is the same function, so let’s rename it multiplySum
. We also don’t need the intermediate value products
and instead pipe (|>
the result of the pairwise multiplication to the Array.sum. We could apply this function using either utility
or weight
as the first argument.
1: 2: 3: 4: 5: 6: 7: |
let multiplySum x v = Array.map2 (fun xElem vElem -> xElem * match vElem.Setting with | One -> 1.0 | _ -> 0.0 ) x v |> Array.sum printfn "%f" (multiplySum utilities vars) printfn "%f" (multiplySum weights vars) |
The printfn
function examines the format string and makes sure that the number and type of arguments match the wildcard patterns
In the multiplySum printfn
statements, you must use parens to make the function application with its arguments into a single argument for the print, otherwise its three arguments. Of course, both of these would still produce 0.0.
We have a sense of some of the code that we may use, but then notice that the arrays for variables, weights and utility be the same length. Furthermore the next incremental change is then realizing that weight and utility are really factors of a knapsack item. Therefore, we adjust the definitions and get to this:
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: |
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; mutable Setting : ZeroOneVarSetting; // discrete value setting } 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 = 20.0 let multiplySumWeight v = Array.map (fun vElem -> vElem.Weight * match vElem.Setting with | One -> 1.0 | _ -> 0.0 ) v |> Array.sum let multiplySumUtility v = Array.map (fun vElem -> vElem.Utility * match vElem.Setting with | One -> 1.0 | _ -> 0.0 ) v |> Array.sum printfn "%f" (multiplySumWeight vars) printfn "%f" (multiplySumUtility vars) |
OK, the item definitions seem better suited to the domain at hand — using the type system hand-in-hand with the problem domain is called domain driven design. One concept from this idea is to use the type system to disallow invalid states or variable settings. We see this in small part in the ZeroOneVarSetting
as the only choices are one of the three discriminants. If instead we used, say, an int
type to represent the state of our variable, assuming that the value would be -1 for Unset, 0 for Zero and 1 for One, what would the meaning be if the int took on a value of 7 or -4? In the F# discriminated union approach, there is simply no opportunity for such erroneous or unexpected settings.
Unfortunately however, the two functions for getting the total weight and utility seem redundant varying only in which record member is used in the multiplication. Let’s revisit this again, this time creating a two argument function multiplyEither
with a first argument of boolean that selects Utility if true and Weight otherwise.
1: 2: 3: 4: 5: 6: |
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 |
The idiomatic or canonical approach to leveraging functional style is often discussed in regard to F# code. See this and this for more
We then create our multiplySumWeight
as multiplyEither true
and multiplySumUtility false
. This demonstrates an important feature of functional programming, namely partial function application. multiplySumWeight
is multiplyEither
with the first argument of true ‘baked in’; it only has one argument now the DiscreteVar
array. That is the type for multiplySumWeight
is DiscreteVar [] -> float
, the type for multiplyEither
is bool -> DiscreteVar [] -> float
. Notice that we never told multiplyEither
what type its argument is, a Boolean type (bool) is deduced from its use as the test in the if expression (and the partial applications that use actual arguments of true and false). There are other variations for doing these accumulating products that could be used too, but for now we will leave it here.
Download This Code
With some of the functionality we need to address the problem, on to page 2