Archive for the ‘memoize’ Tag

projecteuler12...または、返り値を記憶する関数

 projecteuler問題12 は、自然数を加算して得られる三角数において、約数の数が最初に500を超えるものを求めるもの。
 そのために、三角数を newLISP 組込関数 factor で因数分解し、そこから、約数をすべて求めます。
 その際、約数の組み合わせは projecteuler4(続き) で使った binary counting 利用します 。
 ということで、コードは、

(define-macro (memoize mem-func func) 
  (set (sym mem-func mem-func) 
    (letex (f func  c mem-func) 
      (lambda () 
        (or (context c (string (args))) 
            (context c (string (args)) (apply f (args))))))))
(define (make-sublist lst)
  (clean nil? (map (fn(x) (if x $idx)) lst))
)
(memoize combination (lambda (n)
  (let (res)
    (for (x 1 (-- (pow 2 n)))
      (let (lst (bits x true))
        (push (if (< (length lst) n) (append lst (dup nil (- n (length lst)))) lst)
              res -1))
    )
  (map make-sublist res))))
(memoize combination2 (lambda (n)
  (if (= n 0) '()
    (let (res (combination2 (-- n)))
      (push (list n) res -1)
      (dolist (x res (= (x 0) n))
         (push (append x (list n)) res -1))
      res))))
(define (divisors2 num , (res '(1)))
  (let (numbers (factor num))
    (if (= num 1) 1
        numbers
      (let (sublist (combination2 (length numbers)))
        (dolist (x sublist)
          (push (apply * (select numbers x)) res -1))
        (unique res)))))
(let (i 2 sum 1 res nil)
  (until res
    (let (x (divisors2 (++ sum i)))
	   (if (> (length x) 500) (setq res (list i (x -1))) (++ i))))
  res)

 binary counting で作った組み合わせは何度も使いますから、その度に計算するのは無駄。そこで、マクロ memoize の登場です。このマクロは、引数と答えを対で記憶する関数をつくるもので、newLISP の Code Snippets Create memoizing Functions として載っています。
 このマクロを使って関数を作ると、初めての引数には実際に計算し、一度計算した引数には保存してある値を返します。何度も同じ引数を使う時は、大幅に実行時間を短くできます。
 こんな風に、便利で役に立つマクロや関数の例が newLISP のホームページマニュアルコード例に載っているのが newLISP の魅力の一つ。
 さて、このコードを実行すると、

(12375 76576500)
> 

 こんな感じで、出てきます。
 つまり、答えは 12375段目の 76576500

 以上、如何でしょうか?

projecteuler4(続き)...または、組み合わせの求め方

 さて、問題4 の続きです。
 三桁の数同士を掛け算して、最大の回文数を求めるために、最初に回文数を用意します。

(setq digit 3
       max-num (pow 10 digit)
       numbers (sequence (pow 10 (- digit 1)) (- max-num 1))
       strings1 (map string numbers)
       strings2 (map reverse strings1)
       palindromes (map int (map append strings1 strings2)))

 palindromes の中に回文数が小さい順に入っています。それを newLISP 組込関数 reverse を使って順を逆にして、大きい順から一個一個、三桁の数同士の掛け算になるかどうかを確認します。
 そのために、例えば 906609 を newLISP 組込関数 factor で因数分解すると、

> (factor 906609)
(3 11 83 331)
> 

 このままでは、三桁の数同士の掛け算になるかどうか判りません。4つの数値を組み合わせて、二つの掛け算にする必要があります。
 その組み合わせは、

((3 11 83) (331))
((3 11 331) (83))
((3 11) (83 331))
((3 83 331) (11))
((3 83) (11 331))
((3 331) (11 83))
((3) (11 83 331))

 の7通り、リスト (3 11 83 331) の位置で表すと、

(0 0 0 1)
(0 0 1 0)
(0 0 1 1)
(0 1 0 0)
(0 1 0 1)
(0 1 1 0)
(0 1 1 1)

 と、こんな感じ。お気づきでしょうか、この組み合わせ方を binary counting と言うそうです。ここまで来れば、あとは実装するだけ。それが、前回紹介したコードになります。実行時間は早いはず、わずか 94 個目で解答にたどり着くわけですから(笑)。

 以上、如何でしょうか?

newLISP で On Lisp する...第5章(その1)

(この blog は、“short short story または 晴耕雨読な日々”からの引越してきたもの。スクリプトは、newLISP V10.2.1 以降で動作するように書き直しています。)
defun 等の newLISP組込関数に無い関数は、特に断らない限り、newlisp-utility.lsp に定義してあります。

 今回から、第5章 返り値としての関数
 まず、Common Lisp は進化する では、スクリプト例がいくつかありますが、“On Lisp”本書の主張は、

“レキシカル・スコープなら、それだけで、実行時にクロージャを生成できる。”

ということです。
決して、

“レキシカルスコープと違って、ダイナミック・スコープでは実行時にクロージャを生成できないし、出来たとしても、内部変数が、最終的に呼び出された環境に左右される”

 ではありません。なぜなら、Common Lisp からすればダイナミック・スコープといえる newLISP でも、実行時にクロージャを生成可能だからです。それは、第2章(その3)で make-adder の例 を実証しました。また、そこでは、内部状態を変化させられるクロージャも可能であることも示しました。
 ということは、newLISP は、レキシカル・スコープ?そんな訳はありません。こう言い換えるべきでしょう、

“ダイナミック・スコープでも、レキシカル・スコープのようなクロージャを生成できる言語もある。newLISP のように。”

 冗談(笑)は、さておき、スクリプトを実装しましょう。(defun は newlisp-utility.lsp に定義してあります)

(defun joiner (obj)
  (case (type-of obj)
    ("list" append)
    ("integer" +)
    ("float" add)))
(defun joinEx ()
  (apply (joiner (args 0)) (args)))

 newLISP には、組込join があるので、joinEx に改名してあります。また、newLISP には、typecase は無いので、前に紹介したこと のある ArtfulCode ユーティリティ の type-of と組込<a href="http://www.newlisp.org/downloads/newlisp_manual.html#case"case を組み合わせています。
 動作は、

> (joinEx 1 2 3)
6
> (joinEx 1.1 2.2 3.3)
6.6
> (joinEx '(1) '(2 3) '(4 5 6))
(1 2 3 4 5 6)

 ダイナミックスコープの下でできることだから、当たり前。
 make-adder は、第2章(その3)で試しているので、次の関数complement を

(defun complement (f)
  (letex (func f)
    (fn () (not (apply func (args))))))

 もちろん、所望の動作をします(remove-if、evenp、oddp、numberp は newlisp-utility.lsp に定義してあります)。

> evenp
(lambda (num) (= (& num 1) 0))
> oddp
(lambda (num) (= (& num 1) 1))
> remove-if
clean
> (remove-if oddp '(1 2 3 4 5 6))
(2 4 6)
> (remove-if (complement oddp) '(1 2 3 4 5 6))
(1 3 5)
> numberp
number?
> (remove-if (complement numberp) '(1 a 2 b 3 c))
(1 2 3)
> (setq e? (complement oddp))
(lambda () (not (apply (lambda (num) (= (& num 1) 1)) (args))))
> (map e? '(1 2))
(nil true)
> (setq s? (complement numberp))
(lambda () (not (apply number? (args))))
> (map s? '(a 1))
(true nil)

 引数の関数が、組込であれ、自作であれ、クロージャ内に束縛されているのを見ることができます。つまり、Common Lisp 同様、newLISP も抽象化のための強力な道具を手にしています。

 次は、直交性 。newLISP も、直交的なプログラミング言語だといえると思います。最初は、等価で破壊的な関数を返すオペレータを newLISP で定義した例です。newLISP には、ハッシュがありませんので、代わりに context のハッシュ的な使い方を使っています。

(define *equivs*:*equivs*)
(defun !! (f)
  (or  (*equivs* (string "'" f)) f))
(defun def! (f f!)
  (*equivs* (string "'" f) '())
  (setf (*equivs* (string "'" f)) f!))

 ! 単品は、newLISP では、組込関数なので、!! に名前を変えてあります。関数def! の一行目は、context 内に、シンボルを用意しておかないと、setf が変更できる対象がないというエラーになるからです。
 動作は、破壊的関数delete-if がないので、用意します。

(define-macro (delete-if)
  (let (_sym (symbol? (args 1)))
    (letex (_fn (args 0)
            _lst (args 1))
      (if _sym
          (setf _lst (clean _fn _lst))
        (clean _fn _lst)))))

そして、

> (def! remove-if delete-if)
(lambda-macro () 
 (let (_sym (symbol? (args 1))) 
  (letex (_fn (args 0) _lst (args 1)) 
   (if _sym 
    (setf _lst (clean _fn _lst)) 
    (clean _fn _lst)))))
> (setq lst (sequence 0 9))
(0 1 2 3 4 5 6 7 8 9)
> (remove-if evenp lst)
(1 3 5 7 9)
> lst
(0 1 2 3 4 5 6 7 8 9)
> ((!! remove-if) evenp lst)
(1 3 5 7 9)
> lst
(1 3 5 7 9)
> ((!! remove-if) evenp (sequence 0 9))
(1 3 5 7 9)
> (remove-if evenp (sequence 0 9))
(1 3 5 7 9)

 所望の動作をしています。newLISP では、破壊と非破壊の両方の関数がある場合が少ないので、そういう用途で使うことは、あまりないでしょうけど(笑)。

 関数の値のメモ化 の、関数memoize は、newLISP の解説書 “Code Patterns in newLISP” にあります(“Speed up with memoization” の項)。ただし、紹介されているコードは、関数を定義するマクロです。本章に合わせてクロージャを返す関数に再定義します。
(gensym は newlisp-utility.lsp に定義してあります)

(defun memoize (func) 
  (let (ctx (gensym))
    (context ctx)
    (letex (f func  c ctx) 
      (fn () 
        (or (context c (string (args))) 
            (context c (string (args)) (apply f (args))))))))

もちろん、context を使っています。そして、動作例です。

> (setq slowid (memoize (fn (x) (sleep 5000) x)))
(lambda () (or (context gensym3 (string (args))) (context gensym3 (string (args)) 
   (apply (lambda (x) (sleep 5000) x) (args)))))
> (time (slowid 1))
5000
> (time (slowid 1))
0

newLISP の場合、組込sleep の引数と組込time の戻り値の単位は、共に、ミリ秒です。

切りが良いので、関数を合成する は、次回に。

以上、如何でしょうか?