Tuesday, November 20 2012
Common-Lisp

Recent Articles

CL Happy Numbers

A while ago I stumbled upon Happy Numbers as explained in programming praxis, and offered an implementation of them in SQL and in Emacs Lisp. Yeah, I know. Why not, though?

Today I'm back on that topic and as I'm toying with Common Lisp I though it would be a good excuse to learn me some new tricks. As you can see from the earlier blog entry, last time I did attack the digits problem quite lightly. Let's try a better approach now.

(defun digits (n)
  "return the list of the digits of N"
  (nreverse
   (loop for x = n then r
      for (r d) = (multiple-value-list (truncate x 10))
      collect d
      until (zerop r))))

As you can see I wanted to use that facility I like very much, the for x = n then r way to handle first loop iteration differently from the next ones. But I've been hinted on #lisp that there's a much better way to write same code:

(defun integer-digits (integer)
  "stassats version"
  (nreverse
   (loop with remainder
      do (setf (values integer remainder) (truncate integer 10))
      collect remainder
      until (zerop integer))))

That code runs about twice as fast as the previous one and is easier to reason about. It's using setf and the form setf values, something nice to discover as it seems to be quite powerful. Let's see how to use it, even if it's really simple:

CL-USER> (integer-digits 12304501)
(1 2 3 0 4 5 0 1)

Let's move on to solving the Happy Numbers problem though:

(defun sum-of-squares-of-digits (integer)
  (loop with remainder
     do (setf (values integer remainder) (truncate integer 10))
     sum (* remainder remainder)
     until (zerop integer)))

(defun happy? (n &optional seen)
  "return true when n is a happy number"
  (let* ((happiness (sum-of-squares-of-digits n)))
    (cond ((eq 1 happiness)      t)
	  ((memq happiness seen) nil)
	  (t
	   (happy? happiness (push happiness seen))))))

(defun find-happy-numbers (limit)
  "find all happy numbers from 1 to limit"
  (loop for n from 1 to limit when (happy? n) collect n))

And here's how it goes:

CL-USER> (find-happy-numbers 100)
(1 7 10 13 19 23 28 31 32 44 49 68 70 79 82 86 91 94 97 100)

CL-USER> (time (length (find-happy-numbers 1000000)))
(LENGTH (FIND-HAPPY-NUMBERS 1000000))
took 1,621,413 microseconds (1.621413 seconds) to run.
       116,474 microseconds (0.116474 seconds, 7.18%) of which was spent in GC.
During that period, and with 4 available CPU cores,
     1,431,332 microseconds (1.431332 seconds) were spent in user mode
       145,941 microseconds (0.145941 seconds) were spent in system mode
 185,438,208 bytes of memory allocated.
 1 minor page faults, 0 major page faults, 0 swaps.
143071

Of course that code is much faster than the one I wrote before both in SQL and Emacs Lisp, the reason being that instead of writing the number into a string with (format t "~d" number) then subseq to get them one after the other, we're now using truncate.

Happy hacking!

Update

It turns out that to solve math related problem, some maths hindsight is helping. Who would have believed that? So if you want to easily get some more performances out of the previous code, just try that solution:

(defvar *depressed-squares* '(0 4 16 20 37 42 58 89 145)
  "see http://oeis.org/A039943")

(defun undepressed? (n)
  "same as happy?, using a static list of unhappy sums"
  (cond ((eq 1 n) t)
	((member n *depressed-squares*) nil)
	(t
	 (let ((h (sum-of-squares-of-digits n)))
	   (undepressed? h)))))

(defun find-undepressed-numbers (limit)
  "find all happy numbers from 1 to limit"
  (loop for n from 1 to limit when (undepressed? n) collect n))

Time to compare:

CL-USER> (time (length (find-happy-numbers 1000000)))
(LENGTH (FIND-HAPPY-NUMBERS 1000000))
took 1,938,048 microseconds (1.938048 seconds) to run.
       290,902 microseconds (0.290902 seconds, 15.01%) of which was spent in GC.
During that period, and with 4 available CPU cores,
     1,778,021 microseconds (1.778021 seconds) were spent in user mode
       140,862 microseconds (0.140862 seconds) were spent in system mode
 185,438,208 bytes of memory allocated.
 3,320 minor page faults, 0 major page faults, 0 swaps.
143071

CL-USER> (time (length (find-undepressed-numbers 1000000)))
(LENGTH (FIND-UNDEPRESSED-NUMBERS 1000000))
took 1,036,847 microseconds (1.036847 seconds) to run.
         5,372 microseconds (0.005372 seconds, 0.52%) of which was spent in GC.
During that period, and with 4 available CPU cores,
     1,018,708 microseconds (1.018708 seconds) were spent in user mode
        16,982 microseconds (0.016982 seconds) were spent in system mode
 2,289,152 bytes of memory allocated.
143071
CL-USER>