amb 接受一些参数,它会从这些参数里“不确定”的选一个出来。选 择的标准是:要让整个程序得到 有效的结果。
amb 跟 LISP 一样古老,但是它却强大得难以置信。使用它,我们可 以轻而易举的写出需要大量回溯才能解决的问题。它可以被作为一种 通用的回溯机制。
在后面我们会看到如何用 amb 轻而易举的解决:
amb 的功能就是从它的参数里选出一个来让整个程序得到“有效的结 果”。“有效的结果”这个概念很模糊,什么叫做有效的结果?
为了定义“有效的结果”,我们首先定义一下叫做“无效的结果”, 或者叫做“失败的结果”。
(amb)
没有参数的 amb 被定义为是一个 失败。
看看下面这个表达式:
(if (amb #f #t) 1 (amb))
后面那个 (amb) 显然是失败,那么第一个 amb 应该选择哪一个参数 作为输出呢?如果它选 #f, 那么 if 判断条件为假,就会执行 (amb),导致整个表达式“失败”。
所以,为了避免失败,第一个 amb 不能选择 #f, 它只能选择 #t。 我们的表达式返回值是 1.
再来看一个例子:
(let ((x (list (amb 2 1 -2 5 8 18) (amb 9 8 2 4 14 20)))) (assert (> (car x) (cadr x))) (display x))
x 是由 list 从两个 amb 的结果构造的 list. 这个表达式中间有一个断言,说 (car x) 必须 (cadr x). 那么那两个 amb 分别应该返回什么呢?我们可以从这个表达式的返 回结果看到:
(5 2)
第一个 amb 返回了 5, 第二个 amb 返回了 2. 这就叫做“有效的结 果”。
先别在你的 Scheme 解释器里敲上面的例子,它还没有定义呢! 别急,现在我们来看看 amb 用 Scheme 如何实现。
如果你真的着急,可以跳到 SchemeAmb.
amb-fail 是最近一个失败的分支设置的函数。如果执行没有参数的 (amb) 就会转到这个 amb-fail.
这个例子里,我们把 amb-fail 被初始化为打印 "amb tree exhausted"。
(define amb-fail '*) (define initialize-amb-fail (lambda () (set! amb-fail (lambda () (error "amb tree exhausted"))))) (initialize-amb-fail)
我们用 R5RS 的 syntax-rules 来实现 amb 操作符:
(define-syntax amb (syntax-rules () ((amb alt ...) (let ((prev-amb-fail amb-fail)) (call/cc (lambda (sk) (call/cc (lambda (fk) (set! amb-fail (lambda () (set! amb-fail prev-amb-fail) (fk 'fail))) (sk alt))) ... (prev-amb-fail)))))))
有些不容易看懂,实际上它的功能就是把
(amb #f #t)
这样的输入,转换成:
(let ((prev-amb-fail amb-fail)) (call/cc (lambda (sk) ; branch 1 (call/cc (lambda (fk) (set! amb-fail (lambda () (set! amb-fail prev-amb-fail) (fk 'fail))) (sk #f))) ; branch 2 (call/cc (lambda (fk) (set! amb-fail (lambda () (set! amb-fail prev-amb-fail) (fk 'fail))) (sk #t))) (prev-amb-fail))))
表达式先把 amb-fail 的值保存在局部变量 prev-amb-fail 里,这 样当整个 amb 表达式失败时,它可以通过 prev-amb-fail 通知上 一个 amb 表达式改变它的值。
整个 amb 表达式的 continuation 存放在 sk 里。对于每一个参数, 使用了一个 call/cc 得到它的 continuation. 并且保存在 fk 里。 我们把这些参数对应的 call/cc 暂且叫做 分支 好了。看上面的 "; branch 1" 和 "; branch 2".
当某一个分支得到一个值,它就通过整个 amb 的 continuation(sk) 把这个值返回出去。这样 amb 就返回一个值。
每一个分支在第一次执行时,有两项工作:
第一,把当前的 amb-fail 设置为一个函数。这个 内部函数 的作 用就是把 amb-fail 的值恢复到进入 amb 以前的值:
(lambda () (set! amb-fail prev-amb-fail) (fk 'fail))
第二,立即通过 amb 表达式的 continuation(sk) 返回自己的分支 的值。从而引起 amb 表达式中途返回。
注意,每一个分支执行时都会引起 amb 立即返回。后面的分支都还 没有执行!
(if (amb #f #t) 1 (amb))
就用最开头的那个最简单的例子,这样容易理解:
(let ((prev-amb-fail amb-fail)) (call/cc (lambda (sk) ; branch 1 (call/cc (lambda (fk) (set! amb-fail (lambda () (set! amb-fail prev-amb-fail) (fk 'fail))) (sk #f))) ; branch 2 (call/cc (lambda (fk) (set! amb-fail (lambda () (set! amb-fail prev-amb-fail) (fk 'fail))) (sk #t))) (prev-amb-fail))))
第一个 amb 被展开,就成了上面那个样子。#f 和 #t 是两个分支。 然后 #f 对应的分支将被运行。这个分支的 call/cc 把 amb-fail 绑定到自己的内部函数,然后马上使用
(sk #f)
返回分支的值。
接着 if 得到这个值,从而引起第二个没有参数的 (amb) 的执行。 这就是一个“失败”。(amb) 的执行没有参数,所以没有分支。它被 展开成:
(let ((prev-amb-fail amb-fail)) (call/cc (lambda (sk) (prev-amb-fail))))
它马上就会执行最下面的
(prev-amb-fail)
而 prev-amb-fail 在进入这个 (amb) 的时候被绑定到了 amb-fail, 也就是最近一个失败函数。这里 amb-fail 其实就是第一个 amb 表 达式的 #f 分支设置的。
所以,我们将执行 #f 的分支设置的 amb-fail 函数。这就是 #f 分 支的内部函数,它先把 amb-fail 的值设置成 prev-amb-fail 也就 是进入 (amb #f #t) 以前的值,然后使用 (fk 'fail) 返回 'fail 到分支的 continuation.
接着 (amb #f #t) 的第二个分支开始执行。它在设置好 amb-fail 为自己的内部函数之后,返回了 #t 给 if. 那么 if 就会返回 1. 使得整个 if 表达式没有“失败”。
我们可以为 amb 设计一些辅助函数,使用它们我们可以清晰的表达 经常用到的信息。由于我的代码里多次使用这些函数,以后我们用到 这些函数时就不再列出代码。
(define number-between (lambda (lo hi) (let loop ((i lo)) (if (> i hi) (amb) (amb i (loop (+ i 1)))))))
这个函数是用来方便的构造一个 amb 数字选择的。比如
(number-between 1 8)
就相当于
(amb 1 2 3 4 5 6 7 8)
如果是 (number-between 1 100) 就可以省去你打很多数字了 :)
(define assert (lambda (pred) (if (not pred) (amb))))
我们可以用 assert 来插入一个断言。这样可以使程序的表达更加清晰明确。
(define-syntax apply-amb (syntax-rules () ((apply-amb ls) (eval `(amb ,@ls) (interaction-environment)))))
当我们需要把 amb 作用于一个从别处返回的列表时,可以用这个宏。
(define-syntax bag-of (syntax-rules () ((bag-of e) (let ((prev-amb-fail amb-fail) (results '())) (if (call/cc (lambda (k) (set! amb-fail (lambda () (k #f))) ;<-----+ (let ((v e)) ;amb-fail will be modified by e | (set! results (cons v results)) ;| (k #t)))) ;| (amb-fail)) ;so this amb-fail may not be ---+ (set! amb-fail prev-amb-fail) (reverse! results)))))
amb 每次只返回一个结果。所以如果想得到所有可以使得程序“不失 败”的结果。你需要多次调用 (amb)。为了一次性得到所有结果,你 可以用 bag-of.
bag-of 接受一个参数,这是一个表达式,这个表达式里面可以调用 amb,它返回一个“有意义的结果”。
用来判断一个list里的元素是不是没有重复。
(define (distinct? . ls) (let loop ((lst (car ls))) (let ((first (car lst)) (rest (cdr lst))) (cond ((null? rest) #t) ((member first rest) #f) (else (loop rest))))))
用来从一个list里删除一个元素。
(define (del n ls) (let ((ls (reverse (reverse ls)))) (cond ((null? ls) ls) ((eqv? n (car ls)) (cdr ls)) (else (let loop ((l (cdr ls)) (last ls)) (cond ((null? l) ls) ((equal? n (car l)) (set-cdr! last (cdr l)) ls) (else (loop (cdr l) l))))))))
我们先举一个简单的例子示意一下我们上面的方便函数怎么用:
(define (prime? n) (call/cc (lambda (return) (do ((i 2 (+ i 1))) ((> i (sqrt n)) #t) (if (= (modulo n i) 0) (return #f)))))) (define gen-prime (lambda (hi) (let ((i (number-between 2 hi))) (assert (prime? i)) i)))
其实这里就只是定义了一个函数 prime?,它可以判断一个数是不是 素数。然后我们定义了一个函数 gen-prime,它说:“ 从 2 到 hi 取一个数,它必须是一个素数。 ”
我们用 (gen-prime 20) 就能返回 20 以内的第一个素数。如果我们 要得到下一个素数,就调用 (amb)。不断调用 (amb) 就得到后面的 素数,直到超过 20,我们就会看到 "amb tree exhausted".
如果用
(bag-of (gen-prime 20))
我们就能一次性得到所有 20 以内的素数在一个 list 里。
这是一个用 amb 解决的 n-皇后问题。
(define (debug e) #f) (define (n-queens n) (call/cc (lambda (return) (let place-queens ((i 0) (rows '())) (when (< i n) (let ((try-place (number-between 1 n))) ;start to place queen No.i (debug `("considering queen " ,i " on row " ,try-place "\n")) (do ((placed-idx 0 (+ 1 placed-idx))) ;ensure no two queens conflict ((>= placed-idx (length rows))) (debug `("checking queen on column " ,placed-idx)) (let* ((r (list-ref rows placed-idx)) (condition (and (not (= r try-place)) (not (or (= (+ placed-idx r) (+ i try-place)) (= (- placed-idx r) (- i try-place))))))) (if condition (debug " ... OK!\n") (debug " ... conflict!\n")) (assert condition))) (debug `("putting queen " ,i " on row " ,try-place "\n")) (debug `("places: " ,(append rows (list try-place)) "\n")) (place-queens (+ 1 i) (append rows (list try-place)))) ) (return rows)))))
其实程序的大部分回溯都由 number-between 包办了。在放置第 i 个皇后时,你需要做的只是:让 number-between 帮你取一个数,作 为第 i 列皇后放置的行数。然后说:“ 这个皇后不能与已经放好 的任何一个皇后在同一条横线上,或者在同一条对角线上。 ” amb 就会自动帮你找到答案。魔法!
我在代码里加入了一些 debug 语句,但是 debug 先被定义为什么也 不干。这样处理 8 个皇后的时候会快一些。执行:
(n-queens 8)
就得到一个结果。再执行 (amb) 就得到下一个结果,再下一个结果……
执行
(bag-of (n-queens 8))
就得到了“八皇后问题”的所有 92 个解。
如果你把 debug 重新定义为
(define debug (lambda (e) (cond ((list? e) (for-each display e)) ((string? e) (display e)))))
就能显示这个过程中,amb 为你考虑了什么。不过显示 debug 信息 时,最好使用 4 个皇后,因为 8 个皇后的信息量实在太大了,会看 头晕的 :P
n-皇后其实不大能展示 amb 的威力。你可能觉得用 C 实现 n-皇后 也挺容易?那么就看看下面几个……
我一直想写一个凑 24 程序,可就是懒得动手。现在有了 amb,我花 了 10 分钟就写出了一个程序可以得到所有结果。也许方法有点笨, 但是我真的只花了 10 分钟!
后来我又花了一个小时就把所有看起来重复的解都去掉了。比如我认 为: (* 2 (+ 2 (+ 3 7))) 和 (* 2 (+ 2 (+ 7 3))) 是一样的。 这样在 bag-of 时可以减少一些没有意义的重复。
(define (get-24 . numbers) (let* ((index '(0 1 2 3)) (ai (apply-amb index)) (bi (apply-amb index)) (ci (apply-amb index)) (di (apply-amb index))) (assert (distinct? (list ai bi ci di))) (let* ((a (list-ref numbers ai)) (b (list-ref numbers bi)) (c (list-ref numbers ci)) (d (list-ref numbers di))) (let* ((ops '('+ '- '* '/)) (op1s (apply-amb ops)) (op1 (eval op1s (interaction-environment))) (op2s (apply-amb ops)) (op2 (eval op2s (interaction-environment))) (op3s (apply-amb ops)) (op3 (eval op3s (interaction-environment)))) ; (for-each display `(,a " " ,b " " ,c " " ,d " " ; ,op1s " " ,op2s " " ,op3s "\n")) (let ((exp (amb (when (not (or (and (eq? op2 /) (= (op3 c d) 0)) (and (eq? op1 /) (= (op2 b (op3 c d)) 0)) (and (memq op3 (list + * /)) (< c d)) (and (memq op2 (list + * /)) (< b (op3 c d))) (and (memq op1 (list + * /)) (< a (op2 (op3 c d)))))) `(,op1s ,a (,op2s ,b (,op3s ,c ,d)))) (when (not (or (and (eq? op3 /) (= 0 b)) (and (eq? op2 /) (= 0 c)) (and (eq? op1 /) (= 0 d)) (and (memq op3 (list + * /)) (< a b)) (and (memq op2 (list + * /)) (< (op3 a b) c)) (and (memq op1 (list + * /)) (< (op2 (op3 a b) c) d)))) `(,op1s (,op2s (,op3s ,a ,b) ,c) ,d)) (when (not (or (and (eq? op3 /) (= 0 c)) (and (eq? op2 /) (= 0 (op3 b c))) (and (eq? op1 /) (= 0 (op2 a (op3 b c)))) (and (memq op3 (list + * /)) (< b c)) (and (memq op2 (list + * /)) (< a (op3 b c))) (and (memq op1 (list + * /)) (< (op2 a (op3 b c)) d)))) `(,op1s (,op2s ,a (,op3s ,b ,c)) ,d)) (when (not (or (and (eq? op3 /) (= 0 c)) (and (eq? op2 /) (= 0 (op3 b c))) (and (eq? op1 /) (= 0 (op2 (op3 b c) d))) (and (memq op3 (list + * /)) (< b c)) (and (memq op2 (list + * /)) (< (op3 b c) d)) (and (memq op1 (list + * /)) (< a (op2 (op3 b c) d))))) `(,op1s ,a (,op2s (,op3s ,b ,c) ,d))) (when (not (or (and (eq? op2 /) (= (op2 a b) 0)) (and (eq? op1 /) (= (op3 c d) 0)) (and (memq op3 (list + * /)) (< c d)) (and (memq op2 (list + * /)) (< a b)) (and (memq op1 (list + * /)) (< (op2 a b) (op3 c d))))) `(,op1s (,op2s ,a ,b) (,op3s ,c ,d)))))) (assert (eqv? 24 (eval exp (interaction-environment)))) exp )))))
原理很简单,选4个数,选3个操作符,选5种可能的表达式树,然后 把操作符和数字按表达式树组合起来。
选数的时候先选4个不重复的 index,然后到参数list里取出数。这 样可以解决参数重复的问题。帮助函数 distinct? 可以判断一个 list 里的成员是否有 equal? 意义上的重复。
选操作符时可以重复。因为一个操作符可以多次使用。
构造表达式树时,要求 * + / 三种操作符的左边的参数必须大于或 等于右边的参数,这样可以减少重复。
然后断言:“表达式结果必须是24。”
看到了吗?我只是简单的描述了一下,amb 就为我找到了答案!
运行:
(get-24 1 3 6 12)
结果是:
(* (* 6 1) (/ 12 3))
执行 (amb) 就得到下一个解。
(* (/ 6 1) (/ 12 3))
我们可以用
(bag-of (get-24 1 3 6 12))
得到所有的解。
其实上面的“凑24” 可以推广一下,我们可以用一个程序来生成那 些表达式树,这样我们就可以解决用任意数目的输入数凑足任何一个 数,用任何操作符。实现如下:
(define (get-it numbers operators target) (let loop ((rest numbers)) (let ((ai (number-between 0 (- (length rest) 1))) (bi (number-between 0 (- (length rest) 1)))) (assert (distinct? (list ai bi))) (let ((a (list-ref rest ai)) (b (list-ref rest bi))) (let* ((op (apply-amb operators)) (subexp (list op a b))) (if (and (memv op '(+ *)) (real? a) (real? b)) (assert (> (eval (cadr subexp) (interaction-environment)) (eval (caddr subexp) (interaction-environment))))) (if (memv op '(+ *)) (cond ((and (pair? a) (eqv? op (car a)) (not (pair? b))) (set! subexp `(,@a ,b))) ((and (pair? b) (eqv? op (car b)) (not (pair? a))) (set! subexp `(,@b ,a))) ((and (pair? a) (pair? b) (eqv? op (car a)) (eqv? op (car b))) (set! subexp (append a (cdr b)))))) (if (eq? op '/) (assert (not (= 0 (eval (caddr subexp) (interaction-environment)))))) (if (= 2 (length rest)) (begin (assert (= target (eval subexp (interaction-environment)))) subexp) (loop (cons subexp (del a (del b rest)))) ))))))
这个函数 get-it 接受三个参数。第一个是允许使用的数字,第二个 是允许使用的操作符(必须是二元操作符),第三个参数是要得到什么 结果。
你发现其实这个程序虽然强大很多,反而比上面的 get-24 还要短小。 实际上它的原理就是自底向上构造一个表达式树,然后断言这个表达 式的值为 target.
我们的帮助函数 del 是用来从一个 list 里去掉一个元素的。
比如我们可以这样使用:
(bag-of (get-it '(1 3 6 12) '('+ '- '* '/) 24))
这就相当 get-24 对于参数 1 3 6 12。
我们还可以自己定义一些操作符,比如“平方和”符号 "++":
(define (++ a b) (+ (* a a) (* b b)))
然后我用
(get-it '(2 8 4 3 6 12) '('+ '- '* '/ '++) 100)
就可以求得用这5种操作符对这6个数进行操作,所有能得到 100 的 表达式。
我们甚至可以使用分数数甚至复数!
(get-it '(3 5 10 7) '('+ '- '* '/ '++) 12.5) (get-it '(1+2i 5 2 3-3i) '('+ '- '* '/ '++) 27+9i)
下面两个例子是从 Teach Yourself Scheme in Fixnum Days 抄来的例子。实际上我就是从 这本书里得知的 amb。
这个程序解决了对欧洲地图的 4-着色。不是证明四色定理哈!
用 amb 为每个国家选一个颜色,然后根据邻接矩阵判断是否有颜色 冲突。就是这么简单。
(define choose-color (lambda () (amb 'red 'yellow 'blue 'white))) (define color-europe (lambda () ;choose colors for each country (let ((p (choose-color)) ;Portugal (e (choose-color)) ;Spain (f (choose-color)) ;France (b (choose-color)) ;Belgium (h (choose-color)) ;Holland (g (choose-color)) ;Germany (l (choose-color)) ;Luxemb (i (choose-color)) ;Italy (s (choose-color)) ;Switz (a (choose-color)) ;Austria ) ;construct the adjacency list for ;each country: the 1st element is ;the name of the country; the 2nd ;element is its color; the 3rd ;element is the list of its ;neighbors' colors (let ((portugal (list 'portugal p (list e))) (spain (list 'spain e (list f p))) (france (list 'france f (list e i s b g l))) (belgium (list 'belgium b (list f h l g))) (holland (list 'holland h (list b g))) (germany (list 'germany g (list f a s h b l))) (luxembourg (list 'luxembourg l (list f b g))) (italy (list 'italy i (list f a s))) (switzerland (list 'switzerland s (list f i a g))) (austria (list 'austria a (list i s g)))) (let ((countries (list portugal spain france belgium holland germany luxembourg italy switzerland austria))) ;the color of a country ;should not be the color of ;any of its neighbors (for-each (lambda (c) (assert (not (memq (cadr c) (caddr c))))) countries) ;output the color ;assignment (for-each (lambda (c) (display (car c)) (display " ") (display (cadr c)) (newline)) countries)))))) (color-europe)
得到第一个结果需要一些时间,以后每次按以下 (amb) 就显示另一 个结果。如果你喜欢,可以把这些代码改一改然后用 bag-of 得到所 有结果。嗯……大概有 2592 个吧…… 不过要有耐心哦!建议用 scsh 来运行这个程序。
这个问题来自 J A H Hunter 写的 Mathematical Brain-Teasers。
有一个部落叫 Kalotan,这里的人有一个很奇怪的特点,那就是男性 从来只说真话;女性从来不会连续说两句真话,也不会连续说两句假 话。
有一天,一个人类学家来到这个部落。遇到一对(异性)夫妇和他们的 小孩 Kibi。人类学家问 Kibi:“你是男孩还是女孩?”
Kibi 说了一句 Kalotan 语。人类学家听不懂,于是转向 Kibi 的父 母询问答案(他们会说英语)。于是其中一个(parent1)对他说: “Kibi 说他是男孩。” 另一个(parent2)对他说:“Kibi 是个女孩。 Kibi 撒谎了。”
请你判断 parent1, parent2 和 Kibi 各自的性别。
如果写一个 Scheme 程序,不但立即就可以解决这个问题。还可以帮 助我们分析这个问题。程序如下:
(define (distinct? . ls) (let loop ((lst (car ls))) (let ((first (car lst)) (rest (cdr lst))) (cond ((null? rest) #t) ((member first rest) #f) (else (loop rest)))))) (define (xor a b) (or (and a (not b)) (and b (not a)))) (define solve-kalotan-puzzle (lambda () (let ((parent1 (amb 'm 'f)) (parent2 (amb 'm 'f)) (kibi (amb 'm 'f)) (kibi-self-desc (amb 'm 'f)) (kibi-lied? (amb #t #f))) ;; Parent1 and parant2 must have distinct sex. (assert (distinct? (list parent1 parent2))) ;; If kibi is a boy, then he will never tell a lie. (assert (if (eqv? kibi 'm) (not kibi-lied?))) (assert (if kibi-lied? (xor (and (eqv? kibi-self-desc 'm) (eqv? kibi 'f)) (and (eqv? kibi-self-desc 'f) (eqv? kibi 'm))))) (assert (if (not kibi-lied?) (xor (and (eqv? kibi-self-desc 'm) (eqv? kibi 'm)) (and (eqv? kibi-self-desc 'f) (eqv? kibi 'f))))) ;; If parent1 is male, ;; parent1 told the truth, ;; parent2 told a truth and a lie, ;; but we don't know which is the truth. (assert (if (eqv? parent1 'm) (and (eqv? kibi-self-desc 'm) (xor (and (eqv? kibi 'f) (eqv? kibi-lied? #f)) (and (eqv? kibi 'm) (eqv? kibi-lied? #t)))))) ;; If parent1 is female, ;; we can't know whether parent1 told the truth, ;; because he(she) said only one sentence, ;; but parent2 must told us all truth. (assert (if (eqv? parent1 'f) (and (eqv? kibi 'f) (eqv? kibi-lied? #t)))) ;; Output the results. (newline) (display "Kibi said its sex is ") (display kibi-self-desc) (display ".\n") (if kibi-lied? (display "Kibi lied.\n") (display "Kibi told the truth.\n")) (display "The sex of parent1, parent2 and Kibi is: ") (display (list parent1 parent2 kibi)) (newline)))) (solve-kalotan-puzzle)
我们用变量 parent1, parent2, kibi 分别表示三个人的 性别。用 kibi-self-desc 表示 Kibi 自称的性别。用 kibi-lied? 表示 Kibi 是否撒谎。
这里有两个帮助函数 distinct? 和 xor。distinct? 可以判断一个 list 里的元素是否没有重复。xor 是异或,当且仅当它只有一个参 数为真时为真。
其它的部分在程序里已经相当明了,不需要多解释了。
执行
(solve-kalotan-puzzle)
就能看到三个人的性别,和对另外一些事实的判断。如果你对这个结 果的唯一性表示怀疑,可以用
(bag-of (solve-kalotan-puzzle))
来看看是不是只有一个答案。
我们可以另外定义两个宏,用来得到一个 amb 系统的最大值或者最 小值:
(define-syntax min-of (syntax-rules () ((_ e cost) (let ((prev-amb-fail amb-fail) (results '())) (if (call/cc (lambda (k) (set! amb-fail (lambda () (k #f))) (let ((v e)) (cond ((null? results) (set! results (list v))) ((< (cost v) (cost (car results))) (set! results (list v))) ((= (cost v) (cost (car results))) (if (not (member v results)) (set! results (cons v results))))) (k #t)))) (amb-fail)) (set! amb-fail prev-amb-fail) (reverse! results))))) (define-syntax max-of (syntax-rules () ((_ e cost) (let ((prev-amb-fail amb-fail) (results '())) (if (call/cc (lambda (k) (set! amb-fail (lambda () (k #f))) (let ((v e)) (cond ((null? results) (set! results (list v))) ((> (cost v) (cost (car results))) (set! results (list v))) ((= (cost v) (cost (car results))) (if (not (member v results)) (set! results (cons v results))))) (k #t)))) (amb-fail)) (set! amb-fail prev-amb-fail) (reverse! results)))))
min-of 和 max-of 都接受两个参数,一个是用来生成结果的表达式, 和一个用来衡量结果费用的函数。它的返回值是一个list,里面是达 到最小(最大)值的所有解。
比如,我们可以这样用:
(define (f1) (* (amb 34 23 12 3 8 34 45 94 32 18) (amb 3 8 42 45 64 47 68 19 10 2))) (min-of (f1) (lambda (x) x))
这样我们就可以求得 f1 里的两个 amb 可能的最小乘积。
这两个函数可以作为通用的离散优化函数。比如我们可以用 max-of 来解决装箱问题(bin-pack).
(define (bin-pack objs volume) (let pack ((in-bag '()) (out-of objs)) (call/cc (lambda (return) (let ((next (apply-amb out-of))) (if (<= (apply + (cons next in-bag)) volume) (begin (pack (cons next in-bag) (del next out-of))) (return in-bag)))))))
我们的帮助函数 del 是用来从一个 list 里去掉一个元素的。
bin-pack 接受两个参数,第一个是一些物体的重量,第二个是我们 的箱子(行包)的容积。
每次运行就会得到一个不超过容积的解,比如:
(bin-pack (list 48 102 180 23 3 45 201 19 29 34 55 82 24) 300)
就会得到 (102 48).
我们可以用 max-of 得到最大可能的装箱:
(max-of (bin-pack (list 48 102 180 23 3 45 201 19 29 34 55 82 24) 300) (lambda (l) (apply + l)))
结果是 ((55 19 45 3 23 102 48)). 总重 295.