(The fact that this turned out to be a long story is ridiculous, but perhaps it’ll be useful to someone else)
The idea here is intuitive: given say
2, we can immediately come up with the following orderings:
012 021 102 120 201 210
The problem here is to find the millionth permutation of
The first way is to cheat and work it out with pen-and-paper, upon which you soon realize that there are cycles for each set of numbers. E.g. for two numbers we would have 2 possible orderings (
10), for three we have the ones shown above, etc, and this is because for
n numbers we have
n! combinations. Right, that’s obvious. But the next step is to see that within each block of
n!, there are
n blocks of size
(n-1)!, where the first digit is the same, which means we have a straightforward way of reducing the problem of size
n to a problem of size
As an example, to find the fifth permutation in the example above, we would see that it involves two cycles of two digits each, and then halfway through a single cycle. So we can come up with the first digit being
2, and the next digit being the second of the remaining digits (i.e.
1), and we’re finally left with
(Note: Argh … This sentence is wrong! But I didn’t realize that until later)
So I wrote the code for this, and I used Ocaml because I’m a complete n00b at it.
let rec fact n = if n = 0 then 1 else n * fact (n - 1) let perm_total = 1_000_000;; let get_perm total index = let fact_index = fact index in total / (fact_index), total mod fact_index let rec get_all_perms total index_size = let rec get_next_perm total index_size perm_list = if index_size = 0 then perm_list else match get_perm total index_size with | p, new_total -> get_next_perm new_total (index_size - 1) (p :: perm_list) in List.rev (get_next_perm total index_size ) let rec digits_list n lst = if n = 0 then (0 :: lst) else digits_list (n - 1) (n :: lst) let remove_digit digit lst = List.filter (fun x -> x <> digit) lst let rec get_next_perm_digit perm_list digit_list pdlist = if List.length perm_list = 0 then pdlist else let p = List.hd perm_list in let d = List.nth digit_list p in get_next_perm_digit (List.tl perm_list) (remove_digit d digit_list) (d :: pdlist) let rec get_perm_digits perm_list = let digits = digits_list (List.length perm_list)  in List.rev (get_next_perm_digit perm_list digits ) let euler24 = get_perm_digits (get_all_perms perm_total 9);;
Yes, the names are terrible. Many of the started out as nested functions which I pulled into the top-level to test separately. Anyway, the point is … the answer was wrong.
Ok, I thought, I must’ve screwed up in the Ocaml-ness of my solution. So I rewrote it thusly:
(defun fact (n) (if (= n 1) 1 (* n (fact (1- n))))) (defparameter *total* 1000000) (defun get-perm (total index) (let ((f (fact index))) (truncate total f))) (defun get-next-perm (total index-size perm-list) (if (= index-size 0) (cons 0 perm-list) (multiple-value-bind (p new_total) (get-perm total index-size) (get-next-perm new_total (1- index-size) (cons p perm-list))))) (defun get-all-perms (total index-size) (reverse (get-next-perm total index-size '()))) (defun digits-list (n) (loop for i from 0 to n collect i)) (defun get-next-perm-digit (perm-list digit-list p-d-list) (if (null perm-list) p-d-list (let* ((p (first perm-list)) (d (nth p digit-list))) (get-next-perm-digit (rest perm-list) (remove d digit-list) (cons d p-d-list))))) (defun get-perm-digits (perm-list) (let ((digits (digits-list (1- (length perm-list))))) (reverse (get-next-perm-digit perm-list digits '())))) (defun euler-24 () (get-perm-digits (get-all-perms *total* 9)))
The same answer popped out – which means I was successful at my translation – but it was still the wrong answer. In the initial few seconds of denial, I refreshed the Project Euler page and tried again. No luck.
Now it was time for desperate measures, so I came up with this brute force solution:
(defun list->number (list) (reduce (lambda (x y) (+ (* x 10) y)) list)) (defun number->list (n) (do ((tempn n (floor (/ tempn 10))) (digits '() (cons (mod tempn 10) digits))) ((= tempn 0) digits))) (defun has-digits (n digit-list) (let ((nlist (number->list n))) (not (set-difference digit-list nlist)))) (defun brute-force-next-permutation (digit-list) (let ((n (list->number digit-list))) (do ((trynum (1+ n) (1+ trynum))) ((has-digits trynum digit-list) (number->list trynum)) (format t "Trying ~A~%" trynum))))
…. which wasn’t quite right either, since it skipped the leading zero in our lists. This one seemed to work:
(defun add-num (digit-list) (let ((sum (1+ (first digit-list)))) (if (< sum 10) (cons sum (rest digit-list)) (cons (mod sum 10) (add-num (rest digit-list)))))) (defun next-number (digit-list) (let ((revlist (reverse digit-list))) (nreverse (add-num revlist)))) (defun brute-force-next-permutation (digit-list) (do ((trynum (next-number digit-list) (next-number trynum))) ((not (set-difference digit-list trynum)) trynum))) (defun brute-force-nth-permutation (digit-list n) (do ((i 1 (1+ i)) (trynum digit-list (brute-force-next-permutation trynum))) ((= i n) trynum)))
So I set that running with
(brute-force-nth-permutation '(0 1 2 3 4 5 6 7 8 9) 1000000), and it looked like it was clearly going to take a while.
At this point I was really depressed, since I couldn’t figure out what was wrong with the initial approach which was so straightforward … and then I realized that I was probably off by one.
So I took the answer submitted earlier, and entered the next permutation. Nope. Ok, what about the previous one? That worked!
(Note: To circle back to the ‘trivial’ example at the top of the post: I didn’t catch myself talking about the fifth permutation as being 0,1,2,3,4,5 – so I was really talking about the sixth permutation …)
Later, the (inefficient) brute-force computation terminated in
1668 seconds, and happily, the answers matched. I tried the previous code with a small modification:
(defun euler-24 () (get-perm-digits (get-all-perms (1- *total*) 9)))
… and it gave the same answer (and obviously, so did the Ocaml version). So a happy ending to this story, but a painful reminder of the fact that there are only two hard problems in computer science: naming, caching, and off-by-one errors.