← Back to posts

Simple combinatorics in Clojure

I've recently encountered this seemingly straightforward problem in a coding kata Best Travel, which requires finding all combinations of n elements in a list. It turned out that I still have some ways to go to think in functional way.

Problem statement

For a given list lst, find all possible combinations of size n, so that each element in the resultant list comes from the original list, but do not repeat. Also, the order of elements in resultant list does not matter.

So, as a simple example: given list (1 2 3), and size of 2, the output should be ((1 2) (1 3) (2 3)).

As an example with duplicate elements in original list: given list (1 1 2 3), and size of 3, the output should be ((1 1 2) (1 1 3) (1 2 3) (1 2 3)).

First Attempt

The immediate solution that came to me was list comprehension. As I was practicing Clojure, I thought of for function. Also, I had a faling test, which asserts 3 elements, so I wrote a solution for 3 first.

(defn for-comb [coll]
  (let [possibilities (for [x coll
                            y coll
                            z coll
                            :while (and (not= x y) (not= y z))]
                        (list x y z))]
    (->> possibilities
         (map sort)
         (distinct))))

(for-comb '(1 1 2 3))
;; ((1 1 2) (1 1 3) (1 2 3) (2 2 3))

Ok, this works, then I moved on to generalize for n elements. Hmmm how would I do that? What should be in the for binding?

Second Attempt

The for approach seemed quite difficult to be generalized. (Or maybe I'm not aware of a good solution using for yet.) Next best thing, is to use index.

(defn vec-remove
  [pos coll]
  (into (subvec coll 0 pos) (subvec coll (inc pos))))

(defn index-comb [coll n]
  (for [i (range n)]
    ;; what do I do now)
  ))

At this point, it already points to using recursion. I would need to get one element out of the list, then recurse to get the next element from the rest of the list.

Third Attempt

In 2023, the easiest way for me to continue would be to ask ChatGPT. So here is the solution it gave me:

(defn combinations [coll size]
  (cond
    (= size 0) '(())
    (empty? coll) '()
    :else (concat (map (fn [x] (cons (first coll) x))
                       (combinations (rest coll) (dec size)))
                  (combinations (rest coll) size))))

(combinations [1 1 2 3] 3)
;; ((1 1 2) (1 1 3) (1 2 3) (1 2 3))

Brilliant, so let's see how it works.

It does this by recursively generating combinations from the rest of the list, both with and without the first item, and concatenates these.

The base cases are when size is 0 (return a list with an empty list) and when the collection is empty (return an empty list).

So, for the example (1 2 2 3), it contains two possible sets of solutions. First, solutions containing the first 1. Second, solutions that do not contain 1. To find the first set, it takes the 1, and rest of the list (2 2 3), then try to find 2 element combinations of (2 2 3) and append 1 to each combination. To find the second set, it just repeats the process of the same function but with one element fewer. Eventually, both solutions converge onto the base case.

clojure.math.combinatorics solution

There is also an official combinatorics library that already solves this problem. You can find the actual library solution here

Final words

After looking at my original thought process and the recursion solution, the recursion makes perfect sense!

HomeBlogContactGithubReading