Cuối cùng, người ta mong muốn nhận được một số lời giải tùy theo yêu cầu của người sử
dụng. Ta xây dựng hàm some-solutions dựa theo cách xây dựng hàm a-solution
nhưng khi tìm thấy một lời giải, máy yêu cầu người sử dụng trả lời có tiếp tục tìm lời giải khác
không. Vị từ other-solution? dùng để duy trì quá trình tìm kiếm. Sau khi đã đưa ra hết
các lời giải tìm thấy, hàm trả về #f.
121 trang |
Chia sẻ: huongthu9 | Lượt xem: 510 | Lượt tải: 0
Bạn đang xem trước 20 trang tài liệu Giáo trình Lập trình hàm và Lập trình Logic - Phan Huy Khánh, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
gọi để thực hiện tính toán L hệt
công việc của bốn hàm trên nhưng được viết lại như sau (dòng tiếp theo là ví dụ áp dụng) :
; Hàm (sum h L )
(list-it (lambda (x y)
(+ (h x) y)) L 0)
(list-it (lambda (x y) (+ (sqrt x) y)) ’(1 2 3 4 5) 0)
--> 8.38233
; Hàm (product h L)
(list-it (lambda (x y)
(* (h x) y)) L 1)
(list-it (lambda (x y) (* (sqrt x) y)) ’(1 2 3 4 5) 1)
--> 10.9545
; Hàm (myappend L1 L2)
(list-it cons L1 L2)
(list-it cons ’(a b c) ’(1 2))
--> ’(a b c 1 2)
; Hàm (mymap h L)
(list-it (lambda (x y)
(cons (h x ) y)) L ’())
(list-it (lambda (x y)
(cons (sqrt x ) y)) ’(1 2 3 4 5) ’())
--> ’(1 . 1.41421 1.73205 2 . 2.23607)
(map sqrt ’(1 2 3 4 5))
--> ’(1 . 1.41421 1.73205 2 . 2.23607)
Chú ý khi áp dụng, cần cung cấp tham đối thực sự cho các tham đối là hàm h.
5. Định nghĩa các hàm fold
Trong chương 1, ta đã định nghĩa hàm foldr «bao qua phải» dùng để tính toán tích luỹ
kết quả trên các phần tử của một danh sách. Sau đây ta định nghĩa trong Scheme hai hàm
fold là foldl (left) và foldr (right) cùng nhận vào một hàm f hai tham đối : một phần tử
xuất phát a và một danh sách L, để trả về kết quả là áp dụng liên tiếp (luỹ kế) hàm f cho a và
với mọi phần tử của L. Hai hàm khác nhau ở chỗ hàm foldl lấy lần lượt các phần tử của L từ
trái qua phải, còn hàm foldl lấy lần lượt các phần tử của L từ phải qua trái :
(define (foldl f a L)
(if (null? L)
a
(foldl f (f a (car L)) (cdr L))))
(define (foldr f a L)
(if (null? L)
a
(f (car L) (foldr f a (cdr L)))))
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 95
(foldl cons 0 ’(1 2 3 4 5))
--> ’(((((0 . 1) . 2) . 3) . 4) . 5)
(foldr cons 0 ’(1 2 3 4 5))
--> ’(1 2 3 4 5 . 0)
III.11.6. Tham đối hoá từng phần
Như đã trình bày ở chương 1 về nguyên lý lập trình hàm, kỹ thuật tham đối hóa từng phần
(currying) cho phép dùng biến để truy cập đến các hàm có số tham biến bất kỳ f(x1, ... , xn).
Chẳng hạn, hàm hai biến f(x, y) được xem là hàm một biến x trả về giá trị là hàm y :
x → (y → f(x, y))
Giả sử xét hàm max tìm số lớn nhất trong thư viện Scheme, với n=2 :
(max (* 2 5) 10)
--> 10
Sử dụng kỹ thuật Currying, ta viết :
(define (curry2 f)
(lambda (x) (lambda (y) (f x y))))
(((curry2 max) (* 2 5)) 10)
--> 10
Với n=3, ta cũng xây dựng tương tự :
(define (curry3 f)
(lambda (x)
(lambda (y)
(lambda (z) (f x y z)))))
((((curry3 max) (* 2 5)) 10) (+ 2 6))
--> 10
Từ đó ta có thể xây dựng cho hàm n đối bất kỳ. Ưu điểm của kỹ thuật Currying là có thể
đặc tả một hàm ngay khi mới biết giá trị của tham số thứ nhất, hoặc các tham số đầu tiên cho
các hàm có nhiều hơn hai tham đối.
Đối với hàm một biến, ưu điểm của kỹ thuật Currying là người ta có thể tổ hợp tuỳ ý các
hàm mà không quan tâm đến các tham đối của chúng.
III.11.7. Định nghĩa đệ quy cục bộ
Trong chương trước, ta đã làm quen với khái niệm hàm bổ trợ để định nghĩa các hàm
Scheme. Hàm bổ trợ có thể nằm ngay trong hàm cần định nghĩa, được gọi là hàm cục bộ.
Người ta có thể sử dụng dạng hàm letrec trong thư viện Scheme để định nghĩa đệ quy một
hàm cục bộ. Chẳng hạn ta cần xây dựng một danh sách các số tự nhiên 0..n với n cho trước
như sau :
; iota : number −> list(number)
; hoặc iota : n −> (0 1 2 ... n)
(define (iota n)
(if (zero? n )
’(0)
(append (iota (- n 1)) (list n))))
(iota 10)
--> '(0 1 2 3 4 5 6 7 8 9 10)
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 96
Định nghĩa trên đây đúng đắn, tuy nhiên việc sử dụng hàm ghép danh sách append làm
tốn bộ nhớ, do luôn luôn phải thêm các phần tử mới vào cuối một danh sách. Vì vậy ý tưởng
cải biên là làm ngược lại vấn đề : xây dựng hàm cho phép nhận một số m để trả về danh sách
các số tự nhiên từ m đến n là (m m+1 ....n). Thay vì sử dụng hàm ghép danh sách, ta sử dụng
cons để xây dựng hàm bổ trợ như sau :
(define (m-to-n m n)
(if (< n m)
’()
(cons m (m-to-n (+ m 1) n))))
(m-to-n 3 10)
--> ’(3 4 5 6 7 8 10)
Do hàm m-to-n không dùng ở đâu khác, nên cần đặt bên trong hàm iota. Sau khi định
nghĩa, cần gọi với tham đối m=0 :
(define (iota n)
(define (m-to-n m n)
(if (< n m)
’()
(cons m (m-to-n (+ m 1) n))))
(m-to-n 0 n))
(iota 10)
--> '(0 1 2 3 4 5 6 7 8 9 10)
Giả sử thay vì định nghĩa hàm cục bộ m-to-n, ta sử dụng dạng let để tạo ra lần lượt các
số tự nhiên kể từ m. Tuy nhiên không cần dùng đến tham biến n vì n đã được cấp bởi hàm
iota :
(define (iota n)
(let ((m-to-n (lambda (m)
(if (< m n)
’()
(cons m (m-to-n (+ m 1)))))))
(m-to-n 0)))
(iota 10)
--> ’()
Ta thấy kết quả sai vì lời gọi m-to-n trong hàm chỉ dẫn đến thực hiện let một lần mà
không thực hiện gọi đệ quy. Để khắc phục, ta sử dụng dạng letrec, là dạng let đặc biệt để
tạo ra lời gọi đệ quy. Cú pháp của letrec giống hệt let cũng gồm phần liên kết và phần
thân :
(letrec ((v1 e1 ) ... (vk eN)) s)
Các biến vi, i=1..N, được gán giá trị ei để sau đó thực hiện phần thân s là một biểu
thức nào đó. Tuy nhiên mỗi phép gán biến có thể «nhìn thấy» lẫn nhau, nghĩa là khi tính biểu
thức ei thì có thể sử dụng các biến vi với i, j tuỳ ý. Nghĩa là không giống hoàn toàn let,
các biến cục bộ v1, ... , vN đều được nhìn thấy trong tất cả các biểu thức e1, ... , ek. Tuy nhiên,
cần chú ý rằng mỗi biểu thức ej được tính mà không cần tính giá trị của biến vj, khi ej là một
biểu thức lambda.
Nhờ ngữ nghĩa này, dạng letrec thường được sử dụng để định nghĩa các thủ tục đệ quy
tương hỗ (mutually recursive).. Chẳng hạn, các vị từ odd? và even? là trường hợp điển hình
cho các hàm thừa nhận định nghĩa đệ quy tương hỗ. Sau đây là ví dụ sử dụng letrec để định
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 97
nghĩa tương hỗ hai thủ tục cục bộ kiểm tra một số nguyên là chẵn (even) hay lẻ (odd) mà
không sử dụng hai hàm thư viện của Scheme là even? và odd? :
(letrec
((local-even? (lambda (n)
(if (= n 0)
#t
(local-odd? (- n 1)))))
(local-odd? (lambda (n)
(if (= n 0)
#f
(local-even? (- n 1))))))
(list (local-even? 27) (local-odd? 27)))
--> ’(#f #t)
Bây giờ hàm iota được định nghĩa lại như sau :
(define (iota n)
(letrec
((m-to-n (lambda (m)
(if (< n m)
’()
(cons m (m-to-n (+ m 1 )))))))
(m-to-n 0)))
(iota 10)
−−> ’(0 1 2 3 4 5 6 7 8 9 10)
Sử dụng phối hợp dạng letrec và lambda như sau :
(lambda (x1 ... xN)
(define f1 e1)
...
(define fN eN)
s)
⇔
(lambda (x1 ... xN)
(letrec (f1 e1)
...
(fN eN))
s))
III.12. Xử lý trên các hàm
III.12.1. Xây dựng các phép lặp
Trong mục trước, ta đã định nghĩa hàm list-it sử dụng kỹ thuật tích luỹ kết quả để tạo
ra một cấu trúc lập trình giống nhau áp dụng cho nhiều hàm khác nhau. Sau đây, ta sẽ xây
dựng các hàm lặp mới là append-map, map-select, every và some để mở rộng thư
viện các hàm của Scheme (vốn chỉ có hai thủ tục lặp có sẵn là map và for-each).
1. Hàm append-map
Khi một hàm f nhận tham đối là các phần tử của một danh sách để trả về các giá trị là danh
sách, người ta cần nối ghép (concatenation) các danh sách này. Ta xây dựng hàm append-
map như sau :
(define (append-map f L)
(apply append (map f L)))
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 98
Áp dụng hàm append-map, ta có thể xây dựng hàm flatting để làm phẳng («cào
bằng») một danh sách phù hợp khác rỗng theo nguyên tắc : ghép kết quả làm phẳng các phần
tử của danh sách ở mức thứ nhất, kết quả làm phẳng của nguyên tử là danh sách (thu gọn về
nguyên tử) :
(define (flatting s)
(if (list? s)
(append-map flatting s)
(list s)))
(flatting ’(a (b c) ((d (e))) "yes" ()))
−−> ’(a b c d e "yes")
(flatting ’a)
−−> ’(a)
(flatting 10)
−−> ’(10)
(flatting ’())
−−> ’()
2. Hàm map-select
Nhiều khi, người ta chỉ muốn áp dụng hàm map cho một số phần tử của một danh sách
thoả mãn một vị từ p? nào đó mà thôi, nghĩa là sử dụng map có lựa chọn. Ta xây dựng hàm
map-select với ý tưởng như sau : sử dụng append-map và gán giá trị ’() cho các phần
tử không thoả mãn vị từ, và do vậy các giá trị rỗng này không đưa vào danh sách kết quả cuối
cùng khi hàm trả về.
(define (map-select f L p?)
(append-map
(lambda (x)
(if (p? x)
(list (f x))
’()))
L))
(map-select (lambda (x)(/ 1 x))
’(a 3 0 5 7 9)
(lambda (x)
(and (number? x) (not (zero? x)))))
−−> ’(1/3 1/5 1/7 1/9)
(map-select sqrt '(1 2 3 4 5) odd?)
−−> ’(1. 1.73205 2.23607)
3. Các hàm every và some
Khi các đối số của phép hội lôgic and là các giá trị của hàm f nào đó, ta có thể định nghĩa
hàm every để mở rộng and như sau :
(every f ’(e1 ... eN)) = (and (f e1) ... (f eN))
Tuy nhiên ta không thể định nghĩa every một cách trực giác là áp dụng phép and cho
danh sách các giá trị của f :
(define (every f L)
(apply and (map f L)))
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 99
Bởi vì hàm apply không nhận and làm tham đối. Trong Scheme, and không phải là hàm
mà là một dạng đặc biệt. Ta định nghĩa every theo cách đệ quy truyền thống như sau :
(define (every f L)
(if (null? L)
#t
(and (f (car L))
(every f (cdr L)))))
(every even? '(0 2 4 6 8))
−−> #t
(every number? '(1 3 a 5))
−−> #f
Một cách tương tự, ta xây dựng hàm some để mở rộng phép tuyển lôgic or bằng cách thay
thế and bởi or có dạng:
(some f ’(e1 ... eN)) = (or (f e1) ... (f eN))
Hàm some như sau :
(define (some f L)
(if (null? L)
#f
(or (f (car L))
(some f (cdr L)))))
(some number? ’(1 3 a 5))
−−> #t
(some odd? ’(0 2 4 6 8))
−−> #f
III.12.2. Trao đổi thông điệp giữa các hàm
Sau khi định nghĩa các hàm và cài đặt các kiểu dữ liệu, người sử dụng có thể thao tác trực
tiếp trên dữ liệu bằng cách sử dụng kỹ thuật lập trình truyền thông điệp (message
transmission). Thay vì xem dữ liệu như là một giá trị kiểu đơn vị (một số nguyên, một bộ đôi,
một danh sách, v.v ... ), ta có thể xem dữ liệu như là một hàm nhận các thông điệp và thực hiện
tính toán tuỳ thuộc vào thông điệp đã nhận. Người ta gọi đây là hàm bẻ ghi (switch function).
Những hàm thấy được từ bên ngoài chỉ còn là một lời gọi với một thông điệp đặc biệt. Ta xét
lại ví dụ về xử lý các số hữu tỷ ở chương trước. Ta đã khai báo các hàm tạo số hữu tỷ, xác định
tử số và mẫu số như sau :
functions
create-rat : integer × integer → rational
numer : rational → integer
denom : rational → integer
Bây giờ ta định nghĩa lại hàm create-rat có dạng như sau :
create-rat : (integer × integer) → (symbol → Integer)
Hàm create-rat sẽ tạo ra một hàm nhận vào một thông điệp (kiểu ký hiệu) để trả về
một số nguyên, tuỳ theo nội dung thông điệp cho biết đó là tử số hay mẫu số :
(define (create-rat x y) ; precondition: y ≠ 0
(define g (gcd x y)) ; gcd là ước số chung lớn nhất của x, y
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 100
(define N (quotient x g))
(define D (quotient y g))
(lambda (message)
(cond
((eq? message ’numerator) N)
((eq? message ’denominator) D)
(else (error ”unknown message” message)))))
((create-rat 9 15) ’numerator)
--> 3
((create-rat 9 15) ’denominator)
--> 5
((create-rat 9 15) ’denom)
--> *** Error: "unknown message" (denom)
(create-rat 9 15)
--> *** Error: ’#{Procedure 6700 (unnamed in create-rat)}
Ta tiếp tục định nghĩa hai hàm numer và denom sẽ gọi đến hàm create-rat :
; numer : (integer × integer) → integer
(define (numer R)
(R ’numerator))
; denom : (integer × integer) → integer
(define (denom R)
(R ’denominator))
(numer (create-rat 9 15))
--> 3
(denom (create-rat 9 15))
--> 5
Với cách xây dựng các hàm xử lý các số hữu tỷ trên đây, người sử dụng chỉ có thể gọi đến
một hàm nhờ một thông điệp message : lỗi sai do một lời gọi hàm nào đó, vô tình hay cố ý,
đều không thể xảy ra. Trong trường hợp một thông điệp cần các tham đối bổ sung, hàm bẻ ghi
sẽ dẫn về một hàm cần phải thực hiện với những tham đối này.
Nếu cần thêm một bộ kiểm tra =rat, chỉ cần thêm mệnh đề sau đây vào điều kiện kiểm tra
message của hàm create-rat (trước else) trên đây :
((eq? message ’=rational)
(lambda(R) (= (* N (denom R)) (* D (numer R)))))
Lúc này, ta có thể viết hàm =rat trả về một hàm một tham đối là số hữu tỷ cần so sánh
như sau :
(define (=rat R1 R2)
((R1 ’=rational) R2)) ; áp dụng hàm trả về với R2
(=rat (create-rat 1 3) (create-rat 1 3))
--> #t
(=rat (create-rat 2 3) (create-rat 3 2))
--> #f
Kỹ thuật lập trình xem dữ liệu như là một hàm nhận các thông điệp để thực hiện tính toán
thường khó sử dụng, đòi hỏi người lập trình phải quản lý tốt ý đồ xử lý theo nội dung thông
điệp. Tất cả những gì là dữ liệu đều có thể tạo ra hàm nhờ lambda nhưng rất khó nhận biết
được những thông điệp nào sẽ thoả mãn hàm. Chẳng hạn sau đây là một ví dụ sử dụng bộ đôi
nhưng thay đổi hai hàm car là cdr :
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 101
(define (cons x y)
(lambda (m)
(cond
((eq? m ’car) x)
((eq? m ’cdr) y)
(else (error "unknown message")))))
(define (car D) (D ’car))
(define (cdr D) (D ’cdr))
(cdr (cons ’tom ’jerry))
--> ’jerry
(car (cons '’tom '’jerry))
--> ’tom
(car ’(1 2 3))
--> ERROR: attempt to call a non-procedure
III.12.3. Tổ hợp các hàm
Từ khái niệm hàm bậc cao, ta có thể định nghĩa các hàm tổ hợp (function composition) như
sau :
; gºf : (T1 → T2) → T3
Hoặc tổ hợp trực tiếp ngay trong tên hàm :
(define (compofg x ) (g (f x))
chẳng hạn :
(define (cacddddr L)
(car (cddddr L)))
(cacddddr ’(a b c d e f))
−−>’e
Hoặc sử dụng lambda để tạo ra dạng một «lời hứa» :
(define (compos g f)
(lambda (x) (g (f x))))
chẳng hạn :
((compos car cddddr) ’(a b c d e f g ))
−−> ’e
(define fifth
(compos car cddddr))
(fifth ’(a b c d e f g ))
−−> ’e
Nhờ khái niệm bậc của hàm, nhiều sai sót có thể được phát hiện. Giả sử ta xét các hàm map
và cdr làm việc với dữ liệu kiểu danh sách :
map : (T1 → T2) × List(T1) → List(T2)
cdr : List(T) → List(T)
Nếu áp dụng hàm map là hàm bậc 2 có cdr làm tham đối sẽ gây ra lỗi :
(map cdr (list 1 2 3))
--> ERROR: cdr: Wrong type in arg1 1
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 102
Trong định nghĩa hàm cdr, danh sách có kiểu List(T), nên danh sách (1 2 3) phải
có kiểu List(Integer). Như vậy, để áp dụng được hàm map, cần phải có :
T1 = List(T) = Integer
nhưng điều này là không thể xảy ra. Tương tự, từ định nghĩa hàm sau đây :
f : (Number → T1) → T2
ta định nghĩa hàm :
(define (f g) (g 2))
thì lời gọi sau đây là sai :
(f f)
--> ERROR: Wrong type to apply: 2
Tuy nhiên những lời gọi sau đây lại cho kết quả đúng :
(f list)
--> '(2) ; tạo danh sách một phần tử
(f sqrt)
--> 1.41421 ; tính căn bậc hai một số nguyên
Ví dụ sau là một định nghĩa hàm sử dụng một hàm khác làm tham đối nhưng thực tế, tham
đối này chẳng đóng vai trò gì trong việc tính toán ở thân hàm, chỉ có mặt cho «phải phép» xét
về mặt cú pháp :
(define (f g n)
(if (= n 0)
1
(* n (g g (- n 1)))))
(define (mystery n) (f f n))
(mystery 5)
--> 120
(f f 5)
--> 120
Người đọc có thể nhìn nhận ra ngay mystery là hàm tính giai thừa ! Xét về phép toán,
định nghĩa trên tỏ ra hợp lý (tính đúng giai thừa). Tuy nhiên, mọi ngôn ngữ có định kiểu mạnh
sẽ từ chối định nghĩa này, vi không thể xác định được bậc của hàm f và vai trò của nó trong
thân hàm. Một cách tương tự, người đọc có nhận xét gì khi thay đổi lại định nghĩa hàm tính
giai thừa như sau :
(define (fac g h n)
(if (= n 0)
1
(* n (h h h (- n 1)))))
Có thể thay đổi dòng cuối cùng thành (g g g ... ) ?
III.12.4. Các hàm có số lượng tham đối bất kỳ
Khi gọi thực hiện một hàm, bộ diễn dịch Scheme đặt tương ứng các tham đối hình thức với
các tham đối thực sự. Sau đây là các dạng định nghĩa hàm có nhiều tham đối tuỳ ý và dạng sử
dụng phép tính lambda tương đương.
Dạng định nghĩa 1 :
(define (f x y z) ... )
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 103
tương đương với :
(define f (lambda (x y z) ... )
Ví dụ với lời gọi :
(f 1 (+ 1 2) 4)
các tham đối hình thức lần lượt được nhận giá trị : x=1, y=3, z=4.
Dạng định nghĩa 2 :
(define (g . L) ... )
(chú ý g là tên hàm, có dấu cách trước và sau dấu chấm) tương đương với :
(define g (lambda L ... ))
Ví dụ với lời gọi :
(g 1 2 3 4 5)
tham đối hình thức là danh sách L được lấy giá trị L = ’(1 2 3 4 5).
Dạng định nghĩa 3 :
(define (h x y . z) ... )
tương đương với :
(define h (lambda (x y . z) ... )
Ví dụ với lời gọi :
(h 1 2 3 4)
các tham đối hình thức được lấy giá trị như sau : ((x y . z) được so sánh với (1 2 3
4), từ đó, x=1, y=2, z=(3 4)
Trong 3 dạng định nghĩa trên đây, hàm f thừa nhận đúng 3 tham đối, hàm g có số lượng
tham đối tuỳ ý, hàm h phải có tối thiểu 2 tham đối. Ta có thể dễ dàng định nghĩa hàm list
theo dạng 2 :
(define (list . L) L)
(list 1 2 3 4 5 6 7 8 9 0)
--> ’(1 2 3 4 5 7 8 9 0)
Ta có thể mở rộng một hàm hai tham đối để định nghĩa thành hàm có nhiều tham đối. Ví
dụ, từ hàm cộng + là phép toán hai ngôi, ta định nghĩa hàm add để cộng dồn các phần tử của
tham đối là một dãy số nguyên tuỳ ý :
(define (add . L) ; L : List(Number)
(define (add-bis L R)
(if (null? L)
R
(add-bis (cdr L) (+ (car L) R))))
(add-bis L 0))
(add 2 3 4 5)
--> 14
Áp dụng hàm apply, hàm add có thể định nghĩa theo cách khác như sau :
(define (add . L)
(if (null? L)
0
(+ (car L) (apply add (cdr L )))))
(add 1 2 3 4 5)
--> 15
(define L ’(1 2 3 4 5))
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 104
(apply add L)
--> 15
Một cách tổng quát, các dạng định nghĩa 2 và 3 có thể viết dưới dạng :
(define (f v0 v1 . . vn- 1 .vN) s)
tương đương với dạng lambda :
(define (f)
(lambda (L) s))
trong đó thân hàm s là một biểu thức, L là một danh sách các đối tuỳ ý.
III.13. Một số ví dụ
III.13.1. Phương pháp xấp xỉ liên tiếp
Nhiều khi, ta cần giải phương trình có dạng :
x = f (x) (1)
trong đó f là một hàm có biến số x. Chẳng hạn để tìm giá trị x từ phương trình :
x2 = 2
ta có thể viết lại dưới dạng phương trình (1) :
x = (x2 + 2)/2x (2)
Để giải phương trình (2), ta sử dụng phương pháp xấp xỉ liên tiếp như sau :
x0 = c
xn+1 = (xn2 + 2)/2xn
trong đó c là một giá trị nào đó để khởi động quá trình lặp. Nếu viết lại các phương trình trên
theo dạng (1) và đặt hằng c = f (xo), ta nhận được :
x0 = f (xo) (3)
xn+1 = f(xn)
Như vậy, lời giải tìm nghiệm x từ phương trình (1) là tìm gíới hạn của dãy { xn} theo (3).
Trong toán học, người ta phải chứng minh tính hội tụ của dãy để nhận được x = f(x). Áp dụng
phương pháp xấp xỉ liên tiếp, ta có thể «giải» hay giải thích cách dùng phép đệ quy tính n! :
fac(n) = 1, với n ≤ 0
fac(n) = n * fac(n-1)
Từ cách tính lặp các giá trị { xn}, ta xây dựng hàm xấp xỉ liên tiếp như sau :
(define (n-iter f x x0)
(if (zero? n)
x0
(f (n-iter f (- n 1) x0))))
Ví dụ 1 : tính 2 nhờ phương pháp xấp xỉ liên tiếp 1
2
xx
x
= + với x0 = 1 :
(n-iter (lambda (x) (+ (/ x 2)
(/ 1. 0 x))) 10 1)
--> 1. 41421
Ví dụ 2 : Tính gần đúng x = sin (2x) :
(n-inter (lambda (x) (sin (* 2 x))) 10 1)
--> 0.948362
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 105
III.13.2. Tạo thủ tục định dạng
Sau đây là một ví dụ tự tạo một thủ tục định dạng format các kết quả đưa ra. Thủ tục
hoạt động tương tự Common Lisp có dạng như sau :
(define (format format-str . L-args)
trong đó, format-str là chuỗi định dạng, còn L-args là các biểu thức tham đối (số lượng
tuỳ ý) được đưa ra theo cách quy ước trong chuỗi định dạng tương ứng.
Chuỗi định dạng đưa ra mọi ký tự có mặt trong đó, trừ ra các ký tự có đặt trước một ký tự
đặc biệt ^ được quy ước như sau :
^s hoặc ^S chỉ vị trí để write đưa ra giá trị tham đối tương ứng
^a hoặc ^A chỉ vị trí để display đưa ra giá trị tham đối tương ứng
^% nhảy qua dòng mới
^^ in ký tự ^
Ví dụ :
(format "Display ^^ with the format:^% x = ^s" (+ 1 2))
--> Display ^ with the format:
x = 3
(format "sin ^s = ^s^%" (/ 3.141592 2) (sin (/ 3.141592 4)))
--> sin 1.5708 = 0.707107
Thủ tục format-str không trả về giá trị mà tuỳ theo nội dung chuỗi định dạng để đưa ra
kết quả. Cách hoạt động đơn giản như sau : thủ tục duyệt chuỗi định dạng và đưa ra mọi ký tự
không đặt trước một ký tự đặc biệt ^. Khi gặp ^, thủ tục sẽ xử lý lần lượt từng trường hợp bởi
case. Tham số i trong hàm bổ trợ format-to chỉ định vị trí của ký tự hiện hành.
(define (format str . L)
(let ((len (string-length str)))
(letrec ((format-to (lambda (i L)
(if (= i len)
(newline) ; 'indefinite
(let ((c (string-ref str i)))
(if (eq? c #\^)
(case (string-ref str (+ 1 i))
((#\a #\A)
(display (car L))
(format-to (+ i 2) (cdr L)))
((#\s #\S)
(write (car L))
(format-to (+ i 2) (cdr L)))
((#\%)
(newline) (format-to (+ i 2) L))
((#\^)
(display #\^)
(format-to (+ i 2) L))
(else (display "unknown character")))
(begin (display c)
(format-to (+ i 1) L))))))))
(format-to 0 L))))
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 106
III.13.3. Xử lý đa thức
1. Định nghĩa đa thức
Ta xét các đa thức (polynomial) biến x hệ số thực. Mỗi đa thức là tổng hữu hạn các đơn
thức (monomial). Mỗi đơn thức có thể là 0 hoặc một biểu thức dạng :
axn
với a là hệ số thực và n là bậc của x, n nguyên dương và a ≠ 0.
Khi n = 0, đơn thức ax0 trở thành hằng a. Đơn thức 0 không có bậc, đôi khi người ta nói
bậc của nó là -∞.
Ví dụ :
9x4+ 7x5 + -10+ -7x5 +27x
là một đa thức của x. Lúc này dấu cộng có vai trò phân cách các đơn thức. Ta chưa định nghĩa
phép cộng trên đa thức, tuy nhiên một cách trực giác, ta có thể rút gọn đa thức thành :
9x4 + -10 +27x
Để xử lý các đa thức trong Scheme, trước hết ta cần đưa ra một cách biễu diễn đa thức
thống nhất. Ta đã biết rằng phép cộng các đơn thức có cùng bậc theo nguyên tắc như sau :
axn +bxn = o nếu a+b = 0
axn +bxn = (a+b)xn nếu không
Bằng cách cộng tất cả các đơn thức cùng bậc và sắp xếp một đa thức theo bậc giảm dần, ta
nhận được cách biễu diễn chính tắc (canonical) cho các đa thức như sau :
apxp + ... +aixi
Đối với một đa thức khác 0, đơn thức có bậc cao nhất được gọi là đơn thức trội (dominant),
và bậc của nó là bậc của đa thức, hệ số của nó là hệ số định hướng (director) hay hệ số trội. Ví
dụ đa thức trên đây có bậc là 4 và được viết lại là :
9x4 +27x + -10
Người ta định nghĩa phép cộng hai đa thức :
(apxp + ... +aixi) + (aqxq + ... +ajxj)
bằng cách cộng các đơn thức cùng bậc và đặt các đơn thức dưới dạng chính tắc, ta nhận được
đa thức tổng.
2. Biễu diễn đa thức
Có hai phương pháp biễu diễn các đa thức trong Scheme :
1. Biễu diễn đầy đủ (full representation)
Theo phương pháp này, người ta tạo một danh sách gồm tất cả các hệ số (bằng 0 hoặc khác
0), bắt đầu từ hệ số trội. Ví dụ đa thức 9x4 +27x + -10 được biễu diễn bởi danh sách :
’(9 0 0 27 -10)
2. Biễu diễn hổng (hollow representation)
Sử dụng một danh sách các đơn thức khác 0 theo bậc giảm dần, mỗi đơn thức được xác
định bởi bậc và hệ số tương ứng. Nghĩa là mọi đa thức P, khác 0, đều có dạng :
P = cxd + Q
với cxd là đơn thức trội có bậc d và hệ số d, Q là đa thức còn lại có bậc thấp hơn.
Sau đây ta chọn xét phương pháp biễu diễn hổng các đa thức. Việc sử dụng nguyên tắc
phân tách một cách chính tắc một đa thức thành một đơn thức trội và một đa thức còn lại có
bậc thấp hơn cho phép định nghĩa đa thức như là một cấu trúc dữ liệu. Giả thiết từ lúc này trở
đi, mọi đa thức đều có cùng biến x và các hàm xử lý lấy tên với tiếp đầu ngữ poly.
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 107
Trước tiên, ta tiến hành các thao tác xử lý đa thức mà chưa nêu ra cách biểu diễn hổng
trong Scheme.Ta gọi zero-poly là đa thức 0, còn cons-poly là hàm tạo đa thức khác 0,
có ba tham biến. Hai tham biến thứ nhất và thứ hai định nghĩa đơn thức trội gồm hệ số và bậc
của nó, tham biến thứ ba chỉ định đa thức còn lại có bậc thấp hơn (là đa thức đã cho nhưng đã
bỏ đi đơn thức có bậc cao nhất) :
poly = zero-poly
hoặc :
poly = (cons-poly coeff degree poly) với điều kiện coeff ≠ 0
Các hàm tiếp cận đến các thành phần đa thức là degree-poly, coeff-domin,
remain-poly thõa mãn các quan hệ sau :
(coeff-domin (cons-poly coeff degree poly)) = coeff
(degree-poly (cons-poly coeff degree poly)) = degree
(remain-poly (cons-poly coeff degree poly)) = poly
Để phân biệt các đa thức 0, xây dựng vị từ zero-poly? thõa mãn quan hệ :
(zero-poly? zero-poly) = #t
(zero-poly? (cons-poly coeff degree poly)) = #f
Để duy trì quan điểm trừu tượng hoá vấn đề, tạm thời ta chưa nêu cụ thể cách xây dựng các
hàm tạo mới đa thức và các hàm tiếp cận đến các thành phần của đa thức vừa trình bày trên
đây mà tiếp tục sử dụng chúng trong các phép xử lý dưới đây.
3. Xử lý đa thức
Cho trước đa thức P = cxd + Q, các phép xử lý trên P bao gồm :
• Nhân đa thức với một hằng số a*P
• So sánh hai đa thức P1 ? P2
• Cộng hai đa thức P1 + P2
• Nhân hai đa thức P1 * P2
1. Nhân đa thức với một hằng số
Để nhân hằng số a với đa thức P, lấy a nhân với hệ số trội rồi sau đó lấy a nhân với đa thức
còn lại :
(define (mult-scalar-poly a P)
(cond ((zero? a) zero-poly) ; xử lý hệ số =0
((zero-poly? P) zero-poly) ; xử lý đa thức 0
(else
(let ((d (degree-poly))
(c (coeff-domin P))
(Q (remain-poly)))
(cons-poly d (* c a)
(mult-scalar-poly a Q))))))
2. So sánh hai đa thức
So sánh hai đa thức bằng cách so sánh hai đơn thức trội, sau đó tiếp tục so sánh hai đa thức
còn lại :
(define (equal-poly? P1 P2)
(cond ; xử lý đa thức 0
((zero-poly? P1) (zero-poly? P2))
((zero-poly? P2) (zero-poly? P1))
(else; xử lý đa thức ≠ 0
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 108
(let ((d1 (degree-poly P1))
(d2 (degree-poly P2))
(c1 (coeff-domin P1))
(c2 (coeff-domin P2))
(Q1 (remain-poly P1))
(Q2 (remain-poly P2)))
; hoặc so sánh hai đa thức còn lại trước tiên theo bậc và hệ số
(and (= d1 d2) (= c1 c2)
; và so sánh hai đa thức còn lại của hai đa thức còn lại
(equal-poly? Q1 Q2))))))
3. Phép cộng đa thức
Khi cộng hai đa thức P1 và P2 cần phân biệt hai trường hợp :
• Nếu các đa thức P1 và P2 có cùng bậc, chỉ việc cộng các đơn thức trội và xem xét trường
hợp chúng có triệt tiêu lẫn nhau không.
• Nếu các đa thức P1 và P2 không có cùng bậc, tìm đa thức có đơn thức bậc cao hơn,
sau đó thực hiện phép cộng các đa thức còn lại.
(define (add-poly P1 P2)
(cond
; xử lý các đa thức 0
((zero-poly? P1) P2)
((zero-poly? P2) P1)
; xử lý các đa thức ≠ 0
(else
(let ((d1 (degree-poly P1))
(d2 (degree-poly P2))
(c1 (coeff-domin P1))
(c2 (coeff-domin P2))
(Q1 (remain-poly P1))
(Q2 (remain-poly P2)))
(cond
((= d1 d2) ; hai đa thức có cùng bậc
(let ((c (+ c1 c2)))
(if (zero? c)
(add-poly Q1 Q2)
(cons-poly
d1
c
(add-poly Q1 Q2)))))
((< d1 d2) ; hai đa thức không có cùng bậc
(cons-poly d2 c2 (add-poly P1 Q2)))
(else
(cons-poly d1 c1 (add-poly Q1 P2))))))))
4. Phép nhân hai đa thức
Giả sử :
P1 = (c1xd1 + Q1) và P2 = (c2xd2 + Q2)
khi đó, kết quả phép nhân hai đa thức P1 và P2 là :
(c1xd1 + Q1) ∗ (c2xd2 + Q2) = c1∗c2 x(d1 + d2) + (c1xd1 + Q1) ∗ Q2 + Q1 ∗ (c2xd2 + Q2)
Hàm mul-poly được xây dựng như sau :
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 109
(define (mul-poly P1 P2)
(cond ((zero-poly? P1) zero-poly)
((zero-poly? P2) zero-poly)
(else
(let
((d1 (degree-poly P1))
(d2 (degree-poly P2))
(c1 (coeff-domin P1))
(c2 (coeff-domin P2))
(Q1 (remain-poly P1))
(Q2 (remain-poly P2)))
(if (zero-poly? Q1)
; Q1 = 0
(cons-poly (+ d1 d2) (* c1 c2)
(mul-poly P1 Q2))
; Q1 ≠ 0
(add-poly
(add-poly
(mul-poly
(cons-poly d1 c1 zero-poly) P2)
(mul-poly Q1 Q2))
(mul-poly Q1 P2)))))))
5. Biễu diễn trong một đa thức
Bây giờ ta tìm cách biểu diễn hổng các đa thức trong Scheme : mỗi đa thức được biễu diễn
bởi một danh sách các đơn thức khác 0 theo thứ tự bậc giảm dần. Ta chọn cách biễu diễn mỗi
đơn thức bởi một bộ đôi như sau :
(degree . coefficient)
Như vậy mỗi đa thức được biễu diễn bởi một danh sách kết hợp alist. Chẳng hạn đa thức :
9x4 +27x + -10
được biễu diễn bởi alist :
((4. 3) (1. 20) (0. -10))
Sau đây ta xây dựng các hàm tạo mới đa thức và các hàm tiếp cận đến các thành phần của
đa thức như sau :
(define (cons-poly degree coeff Q)
(cond
((zero? coeff)
(display
”Error: the coefficient can’t be zero !”))
((zero-poly? Q)
(list (cons degree coeff)))
(else
(cons (cons degree coeff) Q))))
(define degree-poly caar)
(define coeff-domin cdar)
(define (remain-poly Q)
(if (null? (cdr Q))
zero-poly
(cdr Q)))
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 110
Hình 0.14. Biễu diễn đơn thức bởi bộ đôi.
Đối với đa thức 0 ta có thể biễu diễn tùy ý, Chẳng hạn :
(define zero-poly 0)
(define (zero-poly? P)
(and (number? P) (zero? P)))
6. Đưa ra đa thức
Để nhìn thấy các đa thức trên màn hình, hay trên giấy in, ta có thể biễu diễn chúng nhờ các
kí tự ASCII thông dụng. Chẳng hạn đơn thức axn sẽ được biễu diễn dạng ax^n.
Sau đây ta xây dựng hàm print-poly để đưa ra một đa thức. Hàm này đưa ra liên tiếp
các đơn thức nhờ gọi đến hàm print-mono. Trong trường hợp bậc một đơn thức là 1 thì
không in ra dạng ax^1 mà in ra x (đảm bảo tính thẩm mỹ) :
(define (print-poly P)
(if (zero-poly? P)
zero-poly
(let ((c (coeff-domin P))
(d (degree-poly P))
(Q (remain-poly P)))
(print-mono d c)
(if (not (zero-poly? Q))
(begin (display ”+”)
(print-poly Q))))))
(define (print-mono degree coeff)
(cond ((zero? degree) (display coeff))
((=1 degree) (display coeff) (display “x”))
(else (display coeff)
(display ”x”)
(display ”^”)
(display degree))))
(define P1
(cons-poly 2 9 (cons-poly 1 27 (cons-poly 0 -10 0))))
P1
--> ’((2 . 9) (1 . 27) (0 . -10))
(print-poly P1)
--> 9x^2+27x+-10
(add-poly P1 P1)
--> ’((2 . 18) (1 . 54) (0 . -20))
(print-poly (add-poly P1 P1))
--> 18x^2+54x+-20
(print-poly (mul-poly P1 P1))
--> 81x^4+486x^3+1278x^2+-1350x+400
Những xử lý ký hiệu trên các đa thức mà ta vừa trình bày trên đây thường được ứng dụng
trong các hệ thống tính toán hình thức (formal calculus).
coefficientdegree
car cdr
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 111
III.13.4. Thuật toán quay lui
Khi giải những bài toán tổ hợp (combinatorial problems) hay bài toán trò chơi đố chữ
(puzzle) người ta thường không có thuật toán trực tiếp để tìm ra lời giải, mà thường áp dụng
thuật toán «thử sai» (try and error). Ý tưởng của thuật toán là người ta phải thử liên tiếp các
phương án, hoặc dẫn đến thành công và kết thúc, hoặc khi thất bại thì phải quay lui
(backtracking) trở lại phương án đã lựa chọn trước đó để tiếp tục quá trình.
1. Bài toán tám quân hậu
Một ví dụ cổ điển là bài toán tám quân hậu (đã xét ở chương 1). Yêu cầu đặt 8 quân hậu lên
bàn cờ vua 8×8 ô lúc đầu không chứa quân nào sao cho các quân hậu không ăn được lẫn nhau.
Lời giải được tiến hành dần dần như sau :
Giả sử gọi quân hậu là Q, đầu tiên đặt Q tại cột thứ nhất, sau đó, tìm cách đặt Q ở cột thứ
hai sao cho không bị Q ở cột trước ăn và cứ thế tiếp tục cho các cột tiếp theo. Có thể tại một sự
lựa chọn nào đó không cho phép đặt được Q tiếp theo (tại cột j). Khi đó, người ta phải xem lại
sự lựa chọn cuối cùng trước sự thất bại đó (ở cột kế trước j-1) để bắt đầu lại. Nếu tất cả khả
năng cho sự lựa chọn cuối cùng đều thất bại, người ta lại phải quay lui đến sự lựa chọn trước
lựa chọn cuối cùng (ở cột j-2), v.v...
Kỹ thuật quay lui thường được minh hoạ bởi một cây mà mỗi nút trong là một trạng thái
tìm kiếm lời giải và một đường đi từ gốc đến một lá (nút ngoài) nào đó có thể là một lời giải
của bài toán đã cho.
Xuất phát từ nút gốc của cây, là trạng thái đầu, có thể tiếp cận đến các con của nó là các lựa
chọn có thể để đạt đến trạng thái tiếp theo. Khi đi đến một nút cho biết thất bại, người ta phải
quay lên nút cha và bắt đầu với nút con chưa được tiếp cận. Nếu như đã được tiếp cận hết các
con mà mà vẫn thất bại, người ta lại quay lên nút tổ tiên (cha của nút cha)v.v... Quá trình quay
lui đã được giải quyết trong phương pháp tìm kiếm theo chiều sâu trước (depth-first-search
algorithm).
Hình 0.15. Tìm lời giải trên cây trạng thái.
Ý tưởng của thuật toán như sau :
Gọi thuật toán depth-first-search với trạng thái đầu (nút gốc) đã biết :
• Nếu trạng thái đầu là đích (goal state), kết thúc thành công
• Ngược lại, tiếp tục các bước sau cho đến khi thành công hoặc thất bại :
a. Từ trạng thái đầu chưa phải là nút thành công, tiếp cận một trạng thái kế tiếp, giả sử
gọi là S. Nếu không tiếp cận được trạng thái kế tiếp nào, ghi nhận thất bại.
b. Gọi lại thuật toán depth-first-search với S là trạng thái đầu.
c. Nếu thành công, kết thúc. Ngược lại, tiếp tục vòng lặp.
2. Tìm kiếm các lời giải
Ta định nghĩa hàm a-solution cho phép trừ một trạng thái đã cho, trả về một lời giải kế
tiếp trạng thái này hoặc #f nếu thất bại. Nguyên lý hoạt động rất đơn giản như sau :
thành công
thành công
xuất phát
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 112
• Nếu là trạng thái cuối cùng (không còn trạng thái kế tiếp), thì đó là một lời giải để trả về,
nếu không phải thì trả về #f và xem như thất bại.
• Nếu đang ở trạng thái trung gian, liệt kê tất cả các trạng thái kế tiếp và bắt đầu sự lựa
chọn một trạng thái kế tiếp chừng nào chưa tìm ra lời giải.
Giả thiết ta đã xây dựng được các hàm : hàm followingstates cho phép liệt kê tất cả
các trạng thái có thể tiếp cận đến xuất phát từ một trạng thái đã cho, vị từ finalstate?
kiểm tra trạng thái cuối cùng, vị từ solution? kiểm tra tính hợp thức của lời giải. Trong
hàm a-solution có sử dụng some là dạng or mở rộng (xem III.12.1) :
(define (a-solution state)
(if (finalstate? state)
(if (solution? state) state #f)
(some a-solution (followingstates state))))
Trong một số trường hợp, người ta mong muốn nhận được danh sách tất cả các lời giải. Ta
xây dựng hàm list-of-solutions bằng cách gộp vào danh sách lần lượt các lời giải tiếp
theo có thể tìm được như sau :
(define (list-of-solutions state)
if (finalstate? state)
(if (solution? state)
(list state) ’())
(append-map
list-of-solutions
(followingstates state))))
Cuối cùng, người ta mong muốn nhận được một số lời giải tùy theo yêu cầu của người sử
dụng. Ta xây dựng hàm some-solutions dựa theo cách xây dựng hàm a-solution
nhưng khi tìm thấy một lời giải, máy yêu cầu người sử dụng trả lời có tiếp tục tìm lời giải khác
không. Vị từ other-solution? dùng để duy trì quá trình tìm kiếm. Sau khi đã đưa ra hết
các lời giải tìm thấy, hàm trả về #f.
(define (some-solutions state)
(if (finalstate? state)
(if (solution? state)
(other-solution? state)
#f)
(some-solutions (followingstates state))))
(define (other-solution? state)
(display state)
(newline)
(display ”Other solution (Y/N)?:”)
(eq? (read) ’n))
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 113
Cột j
8 Q
7 Q
6 Q
5 Q
4 Q Hàng i
3 Q
2 Q
1 Q
1 2 3 4 5 6 7 8
Hình 0.16. Một lời giải của bài toán 8 quân hậu.
Sau khi đã định nghĩa các hàm chính, ta cần định nghĩa các hàm bổ trợ finalstate?,
solution?, followingstates tương ứng với một cách tổ chức dữ liệu cho bài toán 8
quân hậu. Trước tiên ta cần tìm cách biểu diễn các trạng thái tìm kiếm lời giải.
Bàn cờ vua có 8×8 ô được đánh số theo hàng 1..8 và theo cột 1..8. Do mỗi cột chỉ đăt được
một con Q, ta cần biết vị trí là toạ độ hàng tương ứng với mỗi cột đang xét. Như vậy, một trạng
thái sẽ là một danh sách các số nguyên (x1,..., xk) với xi là số thứ tự hàng của con Q thứ i đặt ở
cột thứ i, i=1..k, k=1..8.
Ví dụ trạng thái của một lời giải được cho trong Error! Reference source not found. là
danh sách đầy đủ :
(1 5 8 6 3 7 2 4)
Nghĩa là lời giải được tìm thấy khi trạng thái đã có đủ 8 con Q và mỗi con Q không thể bị
ăn bởi một con Q nào khác trên bàn cờ. Từ đó, ta định nghĩa vị từ solution? cũng là
finalstate?.
(define (finalstate? state)
(= (length state) 8))
(define solution? finalstate?)
Để liệt kê các trạng thái tiếp theo từ một trạng thái đã cho, ta cần xét 8 vị trí là 8 hàng có
thể trên cột tiếp theo để đặt con Q vào. Vị trí cho Q mới này (newqueen) phải được chọn sao
cho không bị ăn bởi các Q khác trong trạng thái đang xét. Ta cần xác dịnh hàm
admissible? để kiểm tra nếu một vị trí cho một con Q mới là tương thích với những con Q
đã dặt trước đó.
... Q
... ×
... × Q
... Q ×
... × con Q mới tại một vị trí chấp nhận được
... × × × × Q
... ×
... Q ×
Hình 0.17. Vị trí chấp nhận được của một quân hậu.
Nói cách khác, không có con Q nào nằm trên hàng, trên cột, trên đường chéo thuận và trên
đường chéo nghịch đi qua vị trí này. Để xây dựng hàm admissible?, ta cần xây dựng một
hàm bổ trợ kiểm tra khả năng chấp nhận của con Q mới với một con Q đã đặt trước đó tại một
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 114
khoảng cách (distance) đã cho. Tham biến distance là khoảng cách giữa con Q mới và con
Q trước đó. Còn existing-queens là danh sách trạng thái đang xét.
Từ ý tưởng trên, vị từ admissible? được xây dựng như sau :
(define (admissible? newqueen existing-queens)
(letrec ((admisible-to?
(lambda (existing-queens distance)
(if (null? existing-queens)
#t
(let ((aqueen (car existing-queens)))
; kiểm tra lần lượt từng con hậu đã có mặt trong danh sách
(and ; kiểm tra không cùng đường chéo thuận
(not (= aqueen (+ newqueen distance)))
; kiểm tra không cùng hàng
(not (= aqueen newqueen))
; kiểm tra không cùng đường chéo nghịch
(not (= aqueen (- newqueen distance)))
; tiếp tục kiểm tra các quân hậu tiếp theo
(admisible-to? (cdr existing-queens)
(+ 1 distance))))))))
(admisible-to? existing-queens 1)))
Vị từ admissible? không kiểm tra hai quân hậu nằm trên cùng cột. Vị trí chấp nhận
được của một con Q được minh hoạ trên hình 0.17.
Để xây dựng danh sách các trạng thái tiếp theo một trạng thái đã đạt được ở bước trước,
hàm followingstates dưới đây tìm kiếm vị trí chấp nhận được cho một con Q mới. Nếu
tìm được vị trí thoả mãn, thêm toạ độ hàng vào cuối danh sách trạng thái để trả về :
(define (followingstates state)
(append-map
(lambda (position)
(if (admissible? position state)
(list (cons position state))
’()))
(list-1-to-n 8)))
Hàm list-1-to-n có mặt trong hàm followingstates dùng để liệt kê danh sách
các số nguyên từ 1.. n được xây dựng như sau :
(define (list-1-to-n n)
(letrec ((loop
(lambda (k L)
(if (zero? k)
L
(loop (- k 1) (cons k L))))))
(loop n ’())))
(list-1-to-n 8)
--> ’(1 2 3 4 5 6 7 8)
3. Tổ chức các lời giải
Như vậy, ta đã xây dựng xong các hàm chính và các hàm bổ trợ để tìm lời giải cho bài toán
8 quân hậu. Các hàm chính là :
(a-solution state)
--> Tìm một lời giải.
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 115
(list-of-solutions state)
--> Tìm tất cả các lời giải.
(some-solutions state)
--> Tìm lần lượt các lời giải theo yêu cầu.
Lúc đầu, trạng thái state là một danh sách rỗng, sau khi tìm ra lời giải, danh sách được
làm đầy. Các hàm bổ trợ được sử dụng trong các hàm trên đây là :
(finalstate? state)
--> Vị từ kiểm tra nếu một trạng thái tương ứng với một vị trí không thể tiếp tục.
(solution? state)
--> Vị từ kiểm tra một trạng thái là một lời giải (tương tự hàm finalstate?).
(followingstate state)
--> Danh sách tất cả các trạng thái tiếp theo chấp nhận được và khả năng xuất phát từ một
trạng thái đã cho.
(admissible? newqueen existing-queens)
--> Vị từ kiểm tra nếu vị trí của con Q mới không bị ăn bởi các con Q khác đặt tại các cột
trước đó trong danh sách trạng thái.
Bây giờ ta chạy demo bài toán 8 quân hậu và nhận được kết quả như sau :
(a-solution ’())
--> ’(4 2 7 3 6 8 5 1)
(some-solutions '())
--> (4 2 7 3 6 8 5 1)
Other solution (Y/N)?: y
(5 2 4 7 3 8 6 1)
Other solution (Y/N)?: y
(3 5 2 8 6 4 7 1)
Other solution (Y/N)?: y
(3 6 4 2 8 5 7 1)
Other solution (Y/N)?: y
(5 7 1 3 8 6 4 2)
Other solution (Y/N)?: y
(4 6 8 3 1 7 5 2)
Other solution (Y/N)?: n
#t
Số lượng tất cả các lời giải cho bài toán tám quân hậu được tính như sau :
(length (list-of-solutions '()))
--> 92
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 116
Bài tập chương 2 − NGÔN NGỮ SCHEME
1. Giải thích các biểu thức số học sau đây, sau đó tính giá trị và so sánh kết quả :
(+ 23 (- 55 44 33) (* 2 (/ 8 4)))
(define a 3)
a
(/ 6 a)
(define b (+ a 1))
(+ a b (* a b))
2. Giải thích các biểu thức lôgic sau đây, sau đo tính giá trị và so sánh kết quả (có thể sử
dụng hai biến a và b trong bài tập 1) :
(= 2 3)
(= a b)
(not (or (= 3 4) (= 5 6)))
(+ 2 (if (> a b) a b))
3. Giải thích các biểu thức điều kiện sau đây, sau đo tính giá trị và so sánh kết quả :
(if (= 1 1) ”waaw” ”brrr”)
(if (= 4 4) 5 6)
(if (> a b) a b)
(if (and (> b a) (< b (* a b))) b a)
(+ 2 (if (> a b) a b))
((if (< a b) + -) a b)
(cond ((= 1 1) ”waaw 1”)
((= 2 2) ”waaw 2”)
((= 3 3) ”waaw once more”)
(else ”waaw final”))
(* (cond ((> a b) a)
((< a b) b)
(else -1))
(+ a 1))
4. Viết dạng ngoặc tiền tố của các biểu thức :
a) (p−a) (p−b) (p−c)
b) 1 + 2x2 + 3x3
c) )cos(1
)cos()sin(
yx
yxyx
++
−+
5. Các biểu thức sau đây có đúng về mặt cú pháp hay không?
f (x y z) (f) (x y z) (f) ((f f))
() ff ((a) (b) (c))
6. Giải thích các s-biểu thức sau đây, sau đo tính giá trị và so sánh kết quả :
(and #f (/ 1 0))
(if #t 2 (/1 0))
(if #f 2 (/ 1 0))
(and #t #t #f (/ 1 0))
(and #t #t #t (/ 1 0))
7. Viết hàm yêu cầu người sử dụng gõ vào một số nằm giữa 0 và 1000 để trả về giá trị
bình phương của số đó. Đặt hàm này vào trong một vòng lặp với menu.
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 117
8. Viết hàm sử dụng menu để giải hệ phương trình đại số tuyến tính :
ax + by = 0
cx + dy = 0
9. Viết hàm tính giá trị tiền phải trả từ giá trị không thuế (duty-free). Biết rằng hệ số thuế
VAT là 18,6%.
10. Viết hàm tính chiều cao h của tam giác theo các cạnh a, b, c cho biết diện tích tam giác
được tính :
S = p(p-a) (p-b) (p-c)
với p là nửa chu vi (sử dụng hàm bổ trợ tính để tính p).
11. Viết biểu thức tính nghiệm phương trình bậc hai ax2 + bx + c = 0.
12. Cho biết giá trị của a=10, hãy tính :
(let ((a (* a a)) (b (* 4 5)) (c (* a 5)))
(+ a b c))
13. Tính giá trị hai biểu thức sau :
(let ((x 5))
(let* ((y (+ x 10)) (z (* x y)))
(+ x y z)))
(let ((x 4))
(if (= x 0) 1 (let ((x 10)) (* x x))))
14. Viết biểu thức Scheme để tính giá trị :
2 2 2 2
2 2 2 2
x + y - x - y
1 + x + y + x - y
khi biết giá trị của x, y
15. Viết hàm (sum n) = 1 + 1/2 +...+ 1/n vói n nguyên, n > 0
16. Viết hàm (power n x) = xn với x bất kỳ và n nguyên. Cho xn = x * xn−1.
Mở rộng cho trường hợp n < 0.
17. Tương tự bài tập 16 nhưng sử dụng phương pháp chia đôi :
x0 = 1, xn = x* xn−1 nếu n lẻ và xn = (xn/2)
2
nếu n chẵn.
18. Viết vị từ kiểm tra một năm đã cho có phải là năm nhuần không ?
19. Viết hàm(nbsec h m s) tính ra số giây từ giờ, phút, giây đã cho. Ví dụ :
(nbsec 10 3 45)
--> 36225
20. Viết hàm (Hanoi n A B C) giải bài toán «Tháp Hà Nội». Ví dụ :
(Hanoi 2 ’A ’B ’C)
--> Move A to B
Move A to C
Move B to C
1. Giải thích các biểu thức sau đây, sau đó tính giá trị và so sánh kết quả :
(cons 1 2)
(car (cons (cons 1 2) (cons 3 4)))
(cons (cons (cons (cons 1 2) 3) 4) 5)
(cons 1 (cons 2 (cons 3 (cons 4 (cons 5 ())))))
(list 1 2 3 4 5)
(car (list 1 2 3 4 5))
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 118
(cdr (list 1 2 3 4 5))
(cadr (list 1 2 3 4 5))
(caddr (list 1 2 3 4 5))
2. Viết hàm tạo các danh sách sau :
(a b c d)
(a ((b c) d (e f)))
(a (b (c d) . e) (f g) . h)
3. Cho biết những biểu thức nào có cùng kết quả trong số các biểu thức sau đây :
(list 1 ’(2 3 4))
(append ’(1) ’(2 3 4))
(list 1 2 3 4)
(cons ’1 ’(2 3 4))
(cons ’(1) ’(2 3 4))
(cons ’1 ’((2 3 4)))
(append ’(1) ’((2 3 4)))
4. Cho biết giá trị của các biểu thức sau :
’(+ 4 7) ’(a b)
’5 (cons ’a ’((b c)))
(cdr ’(a)) (cdr week-list)
(car ’((+ 4 1))) (cdr ’((+ 4 1)))
(cdr ’((a) b)) (cdr ’((a) (b)))
(cdr ’’(a b))
5. Cho biết các danh sách tương ứng với các sơ đồ sau :
1) 2)
6. Từ hàm member, hãy định nghĩa vị từ member?.
7. Định nghĩa một hàm để phá hết các ngoặc trong một danh sách.
Chẳng hạn, đối với danh sách ((a b c) (d (e f) g) h (i j))
thì hàm trả về : (a b c d e f g h i j)
8. Hàm concat sau đây dùng để ghép hai danh sách tương tự append :
(define (concat list1 list2)
(define (iter response remaining)
(if (null? remaining)
(reverse response)
(iter (cons (car remaining) response)
(cdr remaining))))
(iter list2 list1))
Tuy nhiên hàm không trả về kết quả đúng, hãy viết lại cho phù hợp, sao cho :
(concat ’(1 2 3 4 5) ’(6 7 8 9))
--> ’(1 2 3 4 5 6 7 8 9)
a b
d e
c
a b c d e f
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 119
9. Viết biểu thức trích danh sách để trả về kết quả là danh sách con ’(sat, sun) :
’(mon tue wed thu fri sat sun).
10. Viết các hàm trả về phần tử thứ hai, thứ ba và thứ tư của một danh sách.
11. Viết dạng tổ hợp của car và cdr để nhận được giá trị là ký hiệu a từ các danh sách : ((b
a) (c d)), (() (a d)), (((a)))
12. Cho biết giá trị của (car ’’a) và (cdr ’’a) (chú ý hai dấu quote).
13. Viết định nghĩa tương tự định nghĩa của kiểu list cho kiểu plate-list.
14. Viết hàm (count s L) đếm số lượng ký hiệu s là chữ cái xuất hiện trong danh sách chữ
cái L. Ví dụ :
(count ’R ’(T O M A N D J E R R Y))
--> 2
15. Viết hàm (double L) nhận vào một danh sách các ký hiệu L để trả về danh sách mà các
ký hiệu đã được viết lặp lại. Ví dụ :
(double ’(TOM AND JERRY))
--> ’(TOM TOM AND AND JERRY JERRY)
16. Viết hàm (undouble L) nhận vào một danh sách các ký hiệu L trong đó các ký hiệu
đều bị viết lặp lại để trả về danh sách chỉ chứa mỗi ký hiệu một lần. Ví dụ :
(undouble (double ’(TOM AND JERRY)))
--> ’(TOM AND JERRY)
17. Từ ví dụ xử lý hình chữ nhật trình bày trong lý thuyết, viết vị từ disjoint? trả về #t
nếu hai hình chữ nhật rời nhau, nghĩa là không có điểm nào chung.
18. Xây dựng các hàm xử lý hình chữ nhật sử dụng biểu diễn các thành phần bởi danh sách.
19. Cho biết giá trị các biểu thức dạng quasiquode sau đây :
`(1 + 2 = ,(+ 1 2))
`(the car of the list (a b) is ,(car ’(a b)))
`(cons ,(+ 2 5) ,(list ’a ’b))
(let ((L ’(1 2 3)))
`((+ ,@L) = ,(+ 1 2 3)))
20. Dùng kiểu bộ đôi (pair-doublet) để biểu diễn số phức (a + bi). Hãy tính cộng, nhân và luỹ
thừa bậc n nguyên dương của một số phức. Cho biết :
Cộng: (a + bi) ± (c + di) = (a ± c) + (b ± d)i
Trừ : (a + bi) − (c + di) = (a − c) + (b − d)i
Nhân : (a + bi) × (c + di) = (ac − bd) + (ad ± bc)i
Chia : −2 2 2 2
(a + bi) (ac + bd) (bc ad) = + i
(c + di) (c + d ) (c + d )
, với điều kiện c2 + d2 ≠ 0.
Luỹ thừa : (a + bi)n = rn(cosnϕ + isinnϕ), trong đó :
r = 2 2a + b , ϕ = barctg
a
Căn bậc hai : a + bi = x + yi , trong đó :
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 120
⎛ ⎞ ⎛ ⎞ ⎛ ⎞ ⎛ ⎞+ = − +⎜ ⎟ ⎜ ⎟ ⎜ ⎟ ⎜ ⎟⎝ ⎠ ⎝ ⎠ ⎝ ⎠ ⎝ ⎠
2 2 2 2a a b a a bx = + , y +
2 2 2 2 2 2
Nếu a > 0, tính x và lúc đó, y = b x
2
, nếu a < 0, tính y và lúc đó, x = b y
2
.
21. Cho biết giá trị của :
((lambda (x y z) (+ x y z)) 1 (+ 2 5) 3)
((lambda (L) ((lambda (x) (append L (list x))) 0)) '(a))
22. Cho các định nghĩa sau :
(define (f xl x2 x3 x4)
(lambda (m) (m xl x2 x3 x4)))
(define (g i z)
(z (lambda (u v w x)
(cond ((= i 1) u)
((= i 2) v)
((= i v) w)
(else '())))))
(define x (f -2 3 4 20))
(define y (list 3 5))
(define z (cons y (list 3 5)))
Cho biết kết quả trả về của các lời gọi sau đây :
(g 0 x)
(g 4 1)
(g 3 x)
(eq? (cadr z) (car y))
(eq? (car z) (cdr z))
23. Cho :
U0 =V0 =1
Un = Un-1 + Vn-1
Vn = Un-1 * Vn-1
Dùng letrec tính giá trị của U3 *V4 ?
24. Các hàm sau đây làm gì ? Tìm độ phức tạp của chúng ?
(define (mystery1 L)
(if (null? L)
()
(append (mystery1 (cdr L))
(list (car L)))))
(define (mystery2 L)
(if (null? (cdr L))
(car L)
(if (> (car L) (mystery2 (cdr Ll)))
(car L)
(mystery2 (cdr L)))))
25. Cho đa thức bậc n hệ số thực (hoặc nguyên) một biến như sau :
P(x) = a0 + a1x + a2x
2 + . . . + anx
n
Để biểu diễn P(x) trong Scheme, người ta thường sử dụng một danh sách các hệ số theo
một chiều :
(a0, a1, a2, . . . an) hoặc (an, an−1,. . . a1, a0)
Hãy viết trong Scheme hàm (eval-pol p x) để tính giá trị của đa thức P(x) với một
LẬP TRÌNH HÀM VÀ LẬP TRÌNH LÔGIC 121
giá trị x sử dụng cả hai phương pháp đệ quy và phương pháp lặp, mỗi phương pháp xử lý theo
hai cách biểu diễn hệ số trên đây.
26. Viết hàm tính độ sâu của danh sách :
(profondeur ’(a (b ( c d)) e))
--> 3
27. Viết hàm đếm số lượng các phần tử có giá trị bằng phần tử đã cho :
(nb-occurrence* ’a ’(e a c a (b c a) (a a))
--> 5
28. Viết hàm tạo danh sách nối vòng cho một danh sách đã cho.
29. Viết hàm kiểm tra một danh sách có là tiền tố của một danh sách đã cho.
30. Viết hàm đếm các phần tử của một danh sách nối vòng đã cho.
31. Viết đầy đủ thủ tục xác định danh sách con L[B..A], nghĩa là tìm hai cận B (below), A
(above), 1 ≤ A, B ≤ N sao cho tổng các phần tử của nó là tổng con lớn nhất của L (xem
ví dụ ở mục III.8.1, 3).
32. Cho một xâu ký tự có độ dài N, N được xem rất lớn. Hãy phân loại mỗi ký tự theo 4
kiểu như sau : kiểu chữ thường, kiểu chữ hoa, kiểu chữ số và kiểu khác (ký tự không
thuộc ba kiểu trên) ?
33. Cho một danh sách có N từ (word) 32 bit, N được xem rất lớn. Hãy đếm số bit bằng 1
trong mỗi từ của danh sách đã cho ?
34. Cho một danh sách có N số nguyên. Hãy viết các thủ tục sắp xếp mô phỏng các thuật
toán sắp xếp chèn và chọn.
35. Khi sắp xếp một dãy, người ta thường sử dụng hàm bổ trợ swap(a, b) để hoán đối
giá trị của hai biến. Hãy cho biết vì sao hàm sau đây không sử dụng được ?
(define (swap a b)
(let ((temp a))
(begin (set! a b)
(set! b temp))))
Các file đính kèm theo tài liệu này:
- giao_trinh_lap_trinh_ham_va_lap_trinh_logic_phan_huy_khanh.pdf