CHƯƠNG 2. MỘT SỐ THUẬT TOÁN XẤP XỈ
2.2. Các thuật toán tìm kiếm đường đi ngắn nhất trên mô hình đồ thị
2.3.2. Phương pháp tham lam
Thuật toán tham lam (Greedy algorithms) là một thuật toán giải quyết một số bài toán theo kiểu heuristic để tìm kiếm lựa chọn tối ưu địa phương ở mỗi bước đi với hy vọng tìm được tối ưu toàn cục. Ưu điểm của thuật toán tham lam là dễ đề xuất theo tư tưởng tự nhiên (Miếng ngon ăn trước). Trong hầu hết các bài toán tối ưu, để nhận được nghiệm tối ưu chúng ta có thể đưa về thực sự thực hiện một dãy các lựa chọn. Ý tưởng của chiến lược tham lam là tại mỗi bước ta sẽ lựa chọn một phương án được xem là tốt nhất trong ngữ cảnh nào đó được xác định bởi bài toán tức là việc lựa chọn ở mỗi bước chính là phương án tối ưu địa phương. Tùy theo từng bài toán mà ta đưa ra tiêu chuẩn lựa chọn thích hợp.
Các thuật toán tham lam (greedy algorithm) nói chung là đơn giản và hiệu quả (vì các tính toán để tìm ra quyết định tối ưu địa phương thường là đơn giản). Tuy nhiên các thuật toán tham lam có thể không tìm được nghiệm tối ưu, nói chung nó chỉ cho ra nghiệm gần tối ưu, nghiệm tương lai tốt. Nhưng cũng có nhiều thuật toán được thiết kế theo kỹ thuật tham lam cho ta nghiệm tối ưu, chẳng hạn thuật toán Dijkstra tìm đường đi ngắn nhất từ một đỉnh tới các đỉnh còn lại trong đồ thị vô
30
hướng. Phương pháp tham lam là kỹ thuật thiết kế thường được dùng để giải các bài toán tối ưu, phương pháp được tiến hành theo nhiều bước, tại mỗi bước theo một lựa chọn nào đó (xác định bằng một hàm chọn) sẽ tìm một lời giải tối ưu cho bài toán nhỏ tương ứng. Lời giải của bài toán được bổ sung dần từng bước từ lời giải của các bài toán con. Các lời giải tìm được bằng phương pháp tham lam thường chỉ là chấp nhận theo điều kiện nào đó, chưa chắc là tối ưu.
2.3.2.2. Đặc trưng của phương pháp tham lam
Phương pháp tham lam cần tìm một trật tự hợp lí để duyệt dữ liệu nhằm đạt được mục tiêu một cách chắc chắn và nhanh chóng. Thông thường dữ liệu được duyệt theo một trong hai trật tự là tăng hoặc giảm dần theo một chi tiêu nào đó.
Thuật toán tham lam có trường hợp luôn tìm ra đúng phương án tối ưu, có trường hợp không. Nhưng trong trường hợp thuật toán tham lam không tìm ra đúng phương án tối ưu, chúng ta thường thu được một phương án khả dĩ chấp nhận được. Với một bài toán có nhiều thuật toán để giải quyết, thông thường thuật toán tham lam có tốc độ tốt hơn hẳn so với các thuật toán tối ưu tổng thể.
2.3.2.3. Các thành phần cơ bản
Thuật toán tham lam có 5 thành phần:
• Một tập hợp các ứng viên để từ đó tạo ra lời giải
• Một hàm lựa chọn để lựa chọn ứng viên tốt nhất để bổ sung vào lời giải
• Một hàm khả thi dùng để quyết định một ứng viên có thể là một lời giải
• Một hàm mục tiêu để ấn định giá trị của lời giải hoặc một lời giải chưa hoàn chỉnh
• Một hàm đánh giá để chỉ ra khi nào ta tìm ra một lời giải hoàn chỉnh
Không có cách tổng quát cho một thuật toán tham lam giải quyết một bài toán tối ưu, nhưng chiến lược lựa chọn tham lam và cấu trúc con tối ưu là hai thành phần then chốt, thực tế đã chứng minh rằng các bài toán có 2 thuộc tính này là rất thuận lợi cho việc xây dựng một thuật toán tham lam giải quyết nó.
Hai yếu tố quyết định tới tính tham lam của thuật toán:
31
+ Tính lựa chọn tham lam: Đây là thành phần then chốt đầu tiên, một giải pháp tối ưu toàn cục có thể đạt được bằng cách lựa chọn tối ưu cục bộ (tham lam).
Như vậy, khi có nhiều sự lựa chọn thì ta lựa chọn phương án nào tốt nhất ở hiện tại trong bài toán đang xét mà không cần quan tâm đến kết quả của các bài toán con của nó.
+ Cấu trúc con tối ưu: Một bài toán có cấu trúc con tối ưu nếu giải pháp tối ưu cho bài toán này chứa trong nó các giải pháp tối ưu cho các bài toán con. Thuộc tính này là điểm quyết định để có thể giải bài toán bằng phương pháp tham lam được hay không?
Thuật toán tham lam có được một giải pháp tối ưu cho một bài toán bằng cách thực hiện một chuỗi các lựa chọn. Đối với mỗi quyết định chỉ ra trong thuật toán sự lựa chọn này thường là tốt nhất tại thời điểm được chọn. Nếu có thể chứng minh rằng một thuật toán tham lam cho ra kết quả tối ưu toàn cục cho một lớp bài toán nào đó, thì thuật toán thường sẽ trở thành phương pháp được chọn lựa, vì nó chạy nhanh hơn các phương pháp tối ưu hóa khác. Tuy nhiên đại đa số các trường hợp, thuật toán tham lam chỉ cho nghiệm gần đúng với nghiệm tối ưu.
2.3.2.4. Các bước xây dựng thuật toán tham lam
Thuật toán tham lam được xây dựng theo từng bước như sau:
• Xác định cấu trúc bên trong tối ưu của bài toán.
• Xây dựng một giải pháp đệ quy.
• Chứng minh rằng tại mỗi bước của thuật toán đệ quy, lựa chọn tham lam là một trong những lựa chọn sẽ dẫn đến kết quả tối ưu.
• Chỉ ra rằng sau mỗi lần chọn tham lam thì một trong những bài toán con sẽ rỗng.
• Xây dựng thuật toán đệ quy cho chiến lược tham lam.
• Biến đổi thuật toán đệ quy thành thuật toán lặp.
Qua các bước này đã mô tả nguồn gốc cơ bản của tư tưởng quy hoạch động của thuật toán tham lam. Tuy nhiên ta thường tổ chức hiệu quả các bước trên khi
32
thiết kế thuật toán tham lam. Tổng quát hơn, ta thiết kế thuật toán tham lam theo chuỗi các bước như sau:
• Tìm một phương pháp lựa chọn sao cho bước tiếp theo chỉ việc giải quyết một bài toán con.
• Chứng minh rằng với sự lựa chọn tham lam thì tại mỗi bước ta luôn tìm được một giải pháp tối ưu của bài toán ban đầu.
• Chỉ ra rằng với sự lựa chọn tham lam tại mỗi bước, giải pháp tối ưu của các bài toán con còn lại kết hợp với sự lựa chọn tham lam này sẽ đi đến một giải pháp tối ưu cho bài toán ban đầu.
2.3.2.5. Mô hình thuật toán tham lam
Ta xây dựng tập S dần từng bước, bắt đầu từ tập rỗng. Tại mỗi bước ta sẽ chọn một phần tử "tốt nhất" trong các phần tử còn lại của A để đưa vào S. Việc lựa chọn một phần tử như thế ở mỗi bước được hướng dẫn bởi hàm chọn. Phần tử được chọn sẽ bị loại khỏi tập A. Nếu khi thêm phần tử được chọn vào tập S mà S vẫn còn thỏa mãn các điều kiện của bài toán thì ta mở rộng S bằng cách thêm vào phần tử được chọn.
Thuật toán tham lam tổng quát được mô tả như sau:
Mô hình:
Chọn S từ tập A;
Tính chất tham lam của thuật toán được định hướng bởi hàm Select() - Khởi động 𝑆 = ∅
- Trong khi 𝐴 ≠ ∅:
+ Chọn phần tử tốt nhất của A gán vào x + Cập nhật các đối tượng để chọn A
+ Nếu 𝑆 ∪ {𝑥} thỏa mãn yêu cầu bài toán thì thêm {𝑥} vào tập S.
Thuật toán tham lam được mô tả bằng thủ tục sau:
• Tên thủ tục: Greedy
• Các tham số: A, S // A là một tập các ứng cử viên, X là tập quyết định - nghiệm
• Thực hiện:
33 1 Gán 𝑋 ← ∅
2 Trong khi 𝐴 ≠ ∅, thực hiện:
2.1 Gán x ← Select(A) 2.2 Gán 𝐴 ← 𝐴\{𝑥}
2.3 Nếu 𝑋 ∪ {𝑥} được thừa nhận thì gán 𝑋 ← 𝑋 ∪ {𝑥}
Kết thúc vòng lặp.
Kết thúc thủ tục.
Trong thủ tục tổng quát trên, Select là hàm chọn, cho phép chọn từ tập A một phần tử được xem là tốt nhất, nhiều hứa hẹn nhất là thành viên của tập nghiệm X.
Ví dụ: Xét lại bài toán Knapsack
Dữ liệu vào: + Cho n đồ vật, đồ vật thứ i có thể tích là 𝑤𝑖, có giá trị là 𝑝𝑖 + Cho 1 ba lô có thể tích M
Kết quả ra: Xác định nhóm đồ vật thỏa mãn: tổng thể tích không vượt quá ba lô đồng thời tổng giá trị là lớn nhất.
Phân tích:
Hiển nhiên theo tư tưởng tự nhiên thì để lấy đồ vật có lợi nhất, chúng ta cần thực hiện chiến lược lấy đồ vật có giá trị cao nhất với thể tích nhỏ nhất tại thời điểm hiện tại cho đến khi không thể lấy được nữa thì dừng. Vì vậy ta xây dựng thuật toán theo tư tưởng tham lam như sau:
Bước 1: Sắp xếp các đồ vật theo thứ tự giảm dần của tỉ số 𝑝𝑖
𝑤𝑖 (𝑖 = 1; 2; . . . ; 𝑛) Bước 2: Khởi động tập hợp 𝑆 = ∅ là tập các đồ vật cần lấy.
Bước 3: Kết nạp lần lượt các đồ vật theo thứ tự sắp xếp vào S, mỗi lần kết nạp cần kiểm tra thể tích của S phải không vượt quá M.
Quá trình sẽ kết thúc khi không thể kết nạp thêm đồ vật nào vào S.
34
Thuật toán được mô tả bằng chương trình như sau (các biến được in đậm là tham biến):
Danh sách biến • i, j, M, n, ttl, T: số nguyên
• W, P, X: mảng số nguyên gồm n phần tử (n là hằng nguyên dương)
• C: mảng số thực gồm n phần tử (n là hằng nguyên dương) Thủ tục swap • Tham số:
•• a, b: số nguyên
• Mô tả: Hoán đổi 2 số nguyên
• Các biến cục bộ:
•• tg: số nguyên
• Thực hiện:
1 Gán tg ← a 2 Gán a ← b 3 Gán b ← tg Kết thúc thủ tục.
Thủ tục swapf • Tham số:
•• &a, &b: số thực
• Mô tả: Hoán đổi 2 số thực
• Các biến cục bộ:
•• tg: số thực
• Thực hiện:
1 Gán tg ← a 2 Gán a ← b 3 Gán b ← tg Kết thúc thủ tục.
35 Thủ tục input • Tham số: Không
• Mô tả: Nhập từ bàn phím
• Các biến cục bộ: Không
• Thực hiện:
1 Nhập số đồ vật (n) 2 Nhập thể tích ba lô (M)
3 Cho i chạy tăng từ 1 đến n, thực hiện: Nhập thể tích (W[i]) của từng đồ vật
4 Cho i chạy tăng từ 1 đến n, thực hiện: Nhập giá trị (P[i]) của từng đồ vật
Kết thúc thủ tục.
36 Thủ tục sort • Tham số: Không.
• Mô tả: Sắp xếp tỉ số 𝑝𝑖
𝑤𝑖 theo thứ tự giảm dần.
• Các biến cục bộ: Không.
• Thực hiện:
1 Cho i chạy tăng từ 1 đến n, thực hiện:
1.1 Gán X[i] ← i
1.2 Gán C[i] ← P[i]/W[i] cho kết quả là số thực Kết thúc vòng lặp (1)
2 Cho i chạy tăng từ 1 đến n - 1, thực hiện:
Cho j chạy tăng từ i + 1 đến n, thực hiện:
Nếu 𝐶[𝑖] < 𝐶[𝑗] thì
2.1 Gọi thủ tục swapf(C[i], C[j]) 2.2 Gọi thủ tục swap(W[i], W[j]) 2.3 Gọi thủ tục swap(X[i], X[j]) 2.4 Gọi thủ tục swap(P[i], P[j]) Kết thúc lệnh điều kiện (2). Kết thúc vòng lặp.
Kết thúc thủ tục.
37 Thủ tục output • Tham số: Không.
• Mô tả: Xuất kết quả ra màn hình.
• Các biến cục bộ: Không.
• Thực hiện:
1 Gán ttl ← 0.
2 Gán T ← 0.
3 Cho i chạy tăng từ 1 đến n, thực hiện:
Nếu 𝑡𝑡𝑙 + 𝑊[𝑖] ≤ 𝑀 thì
3.1 Thông báo X[i] (phương án được chọn) lên màn hình
3.2 Gán ttl ← ttl + W[i]
3.3 Gán T ← T + P[i]
Kết thúc lệnh điều kiện (3) Kết thúc vòng lặp.
4 Xuống dòng mới
5 Thông báo ttl (tổng trọng lượng) ra màn hình, xuống dòng mới
6 Thông báo T (tổng giá trị) ra màn hình Kết thúc thủ tục.
Chương trình chính
1 Gọi thủ tục input() 2 Gọi thủ tục sort() 3 Gọi thủ tục output() Kết thúc chương trình.
Ví dụ: Bài toán đổi tiền
Dữ liệu vào: + n loại tiền với các mệnh giá 𝑚𝑖, số lượng các mệnh giá là không hạn chế.
+ Có số tiền thừa là b
Kết quả ra: Phương án trả tiền thừa cho khách hàng sao cho tổng số tờ là ít nhất.
Phân tích:
38
Hiển nhiên theo tư tưởng tự nhiên thì để số tờ là ít nhất, chúng ta cần thực hiện chiến lược trả tiền loại có mệnh giá cao nhất tại thời điểm hiện tại, sau đó chọn tiếp các mệnh giá thấp hơn để trả cho đến khi trả hết số tiền thừa.
Vì vậy ta xây dựng thuật toán theo tư tưởng tham lam như sau:
Bước 1: Sắp xếp các tờ mệnh giá theo giá trị mệnh giá giảm dần.
Bước 2: Khởi động 𝑆 = 0 (S chứa giá trị tiền đã trả)
Bước 3: Lần lượt trả các tờ tiền mệnh giá lớn nhất (Tại thời điểm hiện tại), mỗi lần trả cần kiểm tra 𝑆 − 𝑏 > 0 tức là việc trả là hợp lệ. Nếu không hợp lệ thì chọn tờ tiền có mệnh giá tiếp sau.
Quá trình kết thúc khi 𝑆 − 𝑏 = 0.
Thuật toán được mô tả bằng chương trình như sau:
Danh sách biến • i, j, T, n, k: số nguyên
• A, X, C: mảng số nguyên gồm n phần tử (n là hằng nguyên dương)
Thủ tục swap • Tham số:
•• a, b: số nguyên
• Mô tả: Hoán đổi 2 số nguyên
• Các biến cục bộ:
•• tg: số nguyên
• Thực hiện:
1 Gán tg ← a 2 Gán a ← b 3 Gán b ← tg Kết thúc thủ tục.
39 Thủ tục input • Tham số: Không
• Mô tả: Nhập từ bàn phím
• Các biến cục bộ: Không
• Thực hiện:
1 Nhập số loại tiền (n) 2 Nhập số tiền cần đổi (T)
3 Cho i chạy tăng từ 1 đến n, thực hiện: Nhập mệnh giá (A[i]) của mỗi loại tiền
Kết thúc thủ tục.
Thủ tục sort • Tham số: Không.
• Mô tả: Sắp xếp mệnh giá theo thứ tự giảm dần.
• Các biến cục bộ: Không.
• Thực hiện:
1 Cho i chạy tăng từ 1 đến n, thực hiện: Gán X[i] ← i 2 Cho i chạy tăng từ 1 đến n - 1, thực hiện:
Cho j chạy tăng từ i + 1 đến n, thực hiện:
Nếu 𝐴[𝑖] < 𝐴[𝑗] thì
2.1 Gọi thủ tục swap(A[i], A[j]) 2.2 Gọi thủ tục swap(X[i], X[j]) Kết thúc lệnh điều kiện (2). Kết thúc vòng lặp.
Kết thúc thủ tục.
40 Thủ tục output • Tham số: Không.
• Mô tả: Xuất kết quả ra màn hình.
• Các biến cục bộ: Không.
• Thực hiện:
1 Cho i chạy tăng từ 1 đến n, thực hiện:
1.1 Gán k ← T/A[i]
1.2 Nếu 𝑘 > 0 thì
1.1.1 Gán T ← T - k.A[i]
1.1.2 Thông báo A[i] và k ra màn hình (A[i]
và k lần lượt là mệnh giá và số lượng tờ tiền)
Ngược lại, thông báo A[i] và 0 ra màn hình. Kết thúc lệnh điều kiện (1.2).
Kết thúc vòng lặp (1) 2 Xuống dòng mới
3 Thông báo T (số tiền còn thừa) ra màn hình.
Kết thúc thủ tục.
Chương trình chính
1 Gọi thủ tục input() 2 Gọi thủ tục sort() 3 Gọi thủ tục output() Kết thúc chương trình.
Kết luận:
Nội dung chương 2 đã nghiên cứu một số nguyên tắc thiết kế các thuật toán giải bài toán tối ưu theo tư tưởng Heuristic tìm nghiệm xấp xỉ của các bài toán thuộc lớp NP. Các ví dụ minh họa chứng tỏ tính khả thi của các thuật toán ứng dụng cho các bài toán trong thực tế.
41