What you want to do is create all lists of size k given a list of size N, this is Combinatorics- a fun part of Mathematics, which is used in gambling! To make sure your function works, check that your len(possible_combinations) = n!/ (k! (N-k)!). Oh look, it uses those fun factorials! So if you have a list of 7 ingredients and you want to find all possible sublists of 4 ingredients, you get: (7654321)/ ((4321)(321)) = (765)/(321)= 75 = 35 unique sublists!
The Algorithmic Word
A look into how we can use algorithms to generate a list of Last Word cocktail variations.
Table of Contents
I really enjoy making mixed drinks. One I've gotten quite familiar with during these Corona filled times is something called The Last Word. It's a prohibition era cocktail attributed to the Detroit Athletic Club (apparently they did a lot of creative drinking there) which contains Green Chartreuse, Gin, Maraschino Liqueur and Lime Juice in equal parts. It tastes good and looks cool, what more can you ask for? The best part about The Last Word is that you can create countless variations. All you need to do is swap out one of the ingredients with something else. There's a lot of lists out there but they are a little bit limited for my liking. What if we could programmatically create our own list of variations using an algorithm?
If you're not convinced watch this great video by Greg from How to Drink where he explains it better.
During this experiment I'm going to just include generic names such as
Gin instead of
Gordons Gin, I'm also going to pair this list down for demonstration purposes in the code examples. You'll be able to modify the example yourself if you'd like to create your own tailored list.
For this first attempt we're going to create a function that given an array of ingredients, it will return every 4 item variation it can find. We're going to need to utilize a series of loops and recursion to make this possible. I'll be writing this using Python as it handles this type of thing pretty well.
ingredients = [ 'Green Chartreuse', 'Yellow Chartreuse', 'Dark Rum', 'Lemon Juice', 'Gin', 'Lime Juice', 'Benedictine' ] possible_combinations =  def f(ingredients, drink=): if len(drink) == 4: possible_combinations.append(drink) return else: for i in range(len(ingredients)): f(ingredients[i+1:], drink+[ingredients[i]]) f(ingredients) print(possible_combinations)
My good friend, Ph.D, and mathematical wizard, Lara Langdon, has some insight into the mathematical formulas behind what's happening here.
Awesome! The reason we don't end with duplicates is due to how the function is utilizing recursion within the
for loop. On the initial loop it's going to get the first ingredient in the
ingredients array and then call the function again after offsetting the
ingredients array by its current index. The list of ingredients will get smaller each time as each loop will fire off another series of loops which perform the same offset operation. The example below represents a series of loops, you'll see how each loop has a base, and the last item is shifted until there's no more possible 4 item combinations left.
# 'A' is the base [ A B C D E F ] ^ ^ ^ ^ 1 2 3 4 [ A B C D E F ] ^ ^ ^ ^ 1 2 3 4 [ A B C D E F ] ^ ^ ^ ^ 1 2 3 4 # 'B' is the base [ B C D E F ] ^ ^ ^ ^ 1 2 3 4 [ B C D E F ] ^ ^ ^ ^ 1 2 3 4 # 'C' is the base [ C D E F ] ^ ^ ^ ^ 1 2 3 4
Using a series of
Green Chartreuse as the base, which is the first item in the
ingredients array. The first three items remain the same all the way down, and only the last item is changed. Once it's done it will remove
Green Chartreuse and create the next drink using the next index as its base, in this example it will be
['Yellow Chartreuse', 'Dark Rum', 'Lemon Juice', 'Gin', 'Lime Juice', 'Benedictine'] ['Dark Rum', 'Lemon Juice', 'Gin', 'Lime Juice', 'Benedictine'] ['Lemon Juice', 'Gin', 'Lime Juice', 'Benedictine'] ['Gin', 'Lime Juice', 'Benedictine'] # Drink created! Green Chartreuse, Yellow Chartreuse, Dark Rum, Lemon Juice ['Lime Juice', 'Benedictine'] # Drink created! Green Chartreuse, Yellow Chartreuse, Dark Rum, Gin ['Benedictine'] # Drink created! Green Chartreuse,Yellow Chartreuse, Dark Rum, Lime Juice  # Drink created! Green Chartreuse, Yellow Chartreuse, Dark Rum, Benedictine
While the previous algorithm works, it discards the fact that each ingredient in a Last Word plays a part in making the drink what it is. The first algorithm allows certain variations which are undesirable, I'd be surprised if anyone wants to willingly drink something that comprises of Vodka, Whiskey, Gin and Rum in equal parts.
Let's try and make it a little smarter. For this second experiment we'll update our function so it creates unique variations across multiple arrays. One array will contain liquors, another will have liqueurs, and another miscellaneous ingredients. When the function runs it will create all unique variations possible across these arrays, but it won't pull more than the specified amount from each, allowing us to keep some form of composition in the results. In mathematics this is called a Cartesian Product.
Oh, well here we’ve changed the problem to be about the Cartesian Product. This is from set theory, another branch of mathematics! The Cartesian product of lists A and B, is a new list L=AxB which contains elements of the shape (a, b), where little a is from A, and little b is from B. L is a cool list too- it has all possible combinations of (a, b), and has size len(A) * len(B). You can do this with 3 lists too, A,B and C, and L=AxBxC, the Cartesian product of the three lists will have len(L)=len(A)*len(B)*len(C) and so on! Math is fun! To check your function works, multiply the lengths of each list together!
Thanks, Lara! You can find the updated function below.
# Ingredient lists are shortened for demonstration purposes. liquors = [ 'Dark Rum', 'Vodka', 'Gin', 'Whisky', ] liquers = [ 'Green Chartreuse', 'Yellow Chartreuse', 'Benedictine' ] misc = [ 'Lime Juice', 'Lemon Juice', ] ingredient_categories = [liquors, liquers, liquers, misc] possible_combinations =  def f(categories, drink=, start=0): if len(drink) == len(categories): possible_combinations.append(drink) return for ingredient in categories[start]: f(categories, drink+[ingredient], start+1) f(ingredient_categories) print(possible_combinations)
Similar to the previous function the operations are performed in a similar way. The function is recursively called until each item has appeared in the results. The primary difference is that the pointing occurs over multiple arrays as opposed to a single one. When the
drink array has the same length as the
ingredients array the function is returned and the loop is exited.
The example below illustrates how the function moves across multiple arrays to formulate every possible variation.
[ A B ] [ A B ] [ A B ] [ A B ] ^ ^ ^ ^ 1 2 3 4 [ A B ] [ A B ] [ A B ][ A B ] ^ ^ ^ ^ 1 2 3 4 [ A B ] [ A B ] [ A B ] [ A B ] ^ ^ ^ ^ 1 2 3 4 [ A B ] [ A B ] [ A B ] [ A B ] ^ ^ ^ ^ 1 2 3 4 [ A B ] [ A B ] [ A B ] [ A B ] ^ ^ ^ ^ 1 2 3 4 [ A B ] [ A B ] [ A B ] [ A B ] ^ ^ ^ ^ 1 2 3 4 [ A B ] [ A B ] [ A B ] [ A B ] ^ ^ ^ ^ 1 2 3 4 [ A B ] [ A B ] [ A B ] [ A B ] ^ ^ ^ ^ 1 2 3 4 # 'B' is now the base, and so on... [ A B ] [ A B ] [ A B ] [ A B ] ^ ^ ^ ^ 1 2 3 4
As a result we have ourselves a somewhat adequate list of Last Word variations. Depending on the amount of possible ingredient choices you provide you will end up with a lot of results as this compounds quite fast. In my tests with only a handful of possible ingredients the array length was over
I've created a Last Word variant generator using an expanded ingredient list from the examples. I can't promise that all of these variations will taste good though. Each ingredient should be added in equal parts (typically an ounce each) over ice and shaken.
- Green Chartreuse
- Maraschino Liquer
- Lime Juice
The last Word
The glass color and name of the drink are randomly generated. If you'd like to see the list of all available outputs you can do so here.