Project Euler – Problem 93

The problem: Problem 93

Analysis

This problem was straight-forward to solve and only required the use of some standard algorithms. A quick analysis shows that a brute-force method suffices to find the solution. Let X =\{a, b, c, d\} be a subset of \{0, 1, 2, 3, 4, 5, 6, 7, 8, 9\}. There are \binom{10}{4} such subsets. Let p be a permutation on X. Clearly, there are 4! such permutations. For each p we simply compute all possible ways of evaluating the expression p(a)\,x\,p(b)\,y\,p(c)\,z\,p(d) using all possible ways of adding parentheses, where x, y, z \in \{+, -, \ast, /\}. Since, x, y, z can each be selected in 4 ways and there are exactly 5 ways to add parentheses to the above expression, it follows that we have 4! \cdot 4^3 \cdot 5 = 7680 expressions involving a, b, c, d. Overall, we will end up evaluating \binom{10}{4} \cdot 7680 = 1612800 expressions, which a brute-force algorithm is capable of computing in a reasonable amount of time.

Algorithm

So here’s the algorithm:

largest = 0, ret = {}
for each 4-subset, X, of {0,1,2,3,4,5,6,7,8,9} do
  for each permutation, p, on X do
    for x, y, z in {+,-,*,/} do
      compute all expressions, p[X[0]] x p[X[1]] y p[X[2]] z p[X[3]] using all 5 possible parenthesizations
    end-for
  end-for
  determine n, such that n + 1 was not the result of one of the expressions evaluated in lines 3-7 and 1, 2, 3, ..., n was a result of one of the expressions
  if n > largest then
    largest = n
    ret = X
  end-if
end-for
return ret

To clarify how it works, let’s suppose that X =\{1, 2, 3, 4\}, p = \left(\begin{array}{cccc} 1 & 2 & 3 & 4\\ 2 & 3 & 1 & 4 \end{array}\right), x = \ast, y = - and z = +. Then we will have to compute the 5 expressions:

\begin{array}{ccc} ((1 \ast 2) - 3) + 4 & = & 3\\ (1 \ast 2) - (3 + 4) & = & - 5\\ (1 \ast (2 - 3)) + 4 & = & 3\\ 1 \ast ((2 - 3) + 4) & = & 3\\ 1 \ast (2 - (3 + 4)) & = & -5. \end{array}

Finally, after computing all 7680 expressions involving 1, 2, 3 and 4, we determine n. In this case n = 28, since 29 is never computed as a result of one of the expressions and 1, 2, 3, \ldots, 28 are results of at least one of the expressions.

Implementation Details

I solved this problem in Lisp. The function solve implements the
algorithm.

(defun solve ()
  (do ((largest 0) (ret nil)
       (s (vector 0 1 2 3) (next-subset s)))
      ((null s) (values largest ret))
    (let ((n (perform-evaluations s)))
      (if (> n largest) (setf ret s largest n)))))

The function perform-evaluations implements lines 3-8 of the
algorithm. It returns n as described in line 8.

(defun perform-evaluations (s)
  (do ((seen (make-array (1+ max-result)))
       (p s (next-permutation p)))
      ((null p) (largest-consecutive seen)) ; returns n
    (dolist (f ops)
      (dolist (g ops)
        (dolist (h ops)
          (let ((a (svref p 0)) (b (svref p 1)) (c (svref p 2)) (d (svref p 3)))
            (dolist (x (parens a f b g c h d))
              (if (positive-integerp x) (setf (aref seen (1- x)) t)))))))))

parens is a macro that produces a list containing the 5 parenthesizations of the expression it is given.

(defmacro parens (a f b g c h d)
  `(list
    (funcall ,h (funcall ,g (funcall ,f ,a ,b) ,c) ,d)
    (funcall ,g (funcall ,f ,a ,b) (funcall ,h ,c ,d))
    (funcall ,h (funcall ,f ,a (funcall ,g ,b ,c)) ,d)
    (funcall ,f ,a (funcall ,h (funcall ,g ,b ,c) ,d))
    (funcall ,f ,a (funcall ,g ,b (funcall ,h ,c ,d)))))

So, for example,

> (parens 1 '* 2 '- 3 '+ 4)
(3 -5 3 3 -5)

However, notice that parens fails whenever one of the parenthesizations yields a division by zero.

> (parens 1 '+ 2 '+ 3 '/ 0)
error: division by zero

> (parens 4 '/ 3 '- 2 '+ 1)
error: division by zero

To circumvent this problem I decided to implement 4 new functions add,sub, mul and div such that whenever one of their arguments is nil the result is nil. Also, using div to divide by zero yields nil instead of an error. So,

> (parens 1 'add 2 'add 3 'div 0)
(nil nil nil nil nil)

> (parens 4 'div 3 'sub 2 'add 1)
(1/3 -5/3 5 2 nil)

The code for add, sub, mul and div is shown below.

(defun add (a b) (perform '+ a b))
(defun sub (a b) (perform '- a b))
(defun mul (a b) (perform '* a b))
(defun div (a b) (perform '/ a b))

(defun perform (op a b)
  (cond ((or (null a) (null b)) nil)
        ((and (eql op '/) (zerop b)) nil) ; avoid division by zero
        (t (funcall (symbol-function op) a b))))

Finally, the functions next-subset and next-permutation do exactly what their names imply. These are interesting algorithms in their own right and I will try to explain how they can be implemented in another post.

To solve the problem simply call solve and wait in anticipation (about 17 secs) for the answer.

> (time (solve))

Run time: 16.48103 secs.
(the answer has been removed, solve it yourself)
About these ads
Tagged , , ,

2 thoughts on “Project Euler – Problem 93

  1. Mohd Ajami says:

    Thanks dude, sometimes Brute-forcing is the easiest way to do it. Made it in Java, but the overall time for all sub(algorithms) doesn’t come less than 1 minute.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: