Đây là chuyên đề tham lam do 1 nhóm học sinh thực hiện. Mong nhận được đóng góp tích cực từ mọi người.Giúp mọi người tìm hiểu rõ hơn về thuật toán tham lam, thuật toán được sử dụng nhiều trong các bài thi tin học trong và ngoài nước.
Trang 1
CHUYÊN ĐỀ: THUẬT TOÁN THAM LAM Nhóm 3:Nguyễn Hồng Hoa(trưởng nhóm) Phạm Bằng Hữu(trợ lý) Nguyễn Phan Quỳnh Trang Nguyễn Thị Phương Thảo Đỗ Tuấn Trung Đào Hữu Huy Nguyễn Huy Đức Trần Văn Hùng
Mục lục Mục lục 1
A Lý thuyết chung 2
1 Khái niệm 2
2.Đặc điểm của chiến lược tham lam: 2
3.Tính chất lựa chọn tham lam 3
4 Mô tả tổng quát của thuật toán 3
5.Ví dụ áp dụng: 4
6 Đánh giá chung: 5
B Bài tập 5
Bài toán 1: VATSUA 5
Bài toán 2: THANGMAY 7
Bài toán 3: CHENDAU 8
Bài toán 4: OTO 10
Trang 2A Lý thuyết chung
1 Khái niệm
Thuật toán tham lam là thuật toán luôn luôn tìm kiếm lựa chọn tối ưu ở thời điểm hiện tại để đưa ra kết quả tối ưu Điều này có nghĩa rằng, sự lựa chọn tốt nhất ở mỗi bước sẽ dẫn tới lời giải tối ưu nhất
Ví dụ: Bài toán trả tiền của máy rút tiền tự động ATM
Trong máy rút tiền tự động ATM, ngân hàng đã chuẩn bị sẵn các loại tiền có mệnh giá 100.000 đồng, 50.000 đồng, 20.000 đồng và 10.000 đồng Giả sử mỗi loại tiền đều có số lượng không hạn chế Khi có một khách hàng cần rút một số tiền n đồng (tính chẵn đến 10.000 đồng, tức là n chia hết cho 10000) Hãy tìm một phương án trả tiền sao cho trả đủ n đồng và số tờ giấy bạc phải trả là ít nhất
Gọi X = (X1, X2, X3, X4) là một phương án trả tiền, trong đó X1 là số tờ giấy bạc mệnh giá 100.000 đồng, X2 là số tờ giấy bạc mệnh giá 50.000 đồng, X3 là số tờ giấy bạc mệnh giá 20.000 đồng và X4 là số tờ giấy bạc mệnh giá 10.000 đồng Theo yêu cầu ta phải có X1 + X2 + X3 + X4 nhỏ nhất và X1 * 100.000 + X2 * 50.000 + X3 * 20.000 + X4 * 10.000 = n
Áp dụng “tinh thần” tham lam để giải bài toán này là: để có số tờ giấy bạc phải trả (X1 + X2 + X3 + X4) nhỏ nhất thì các tờ giấy bạc mệnh giá lớn phải được chọn nhiều nhất
Trước hết ta chọn tối đa các tờ giấy bạc mệnh giá 100.000 đồng, nghĩa là X1 là số nguyên lớn nhất sao cho X1 * 100.000 ≤ n Tức là X1 = n DIV 100.000
Xác định số tiền cần rút còn lại là hiệu n – X1 * 100000 và chuyển sang chọn loại giấy bạc 50.000 đồng…
Ví dụ khách hàng cần rút 1.290.000 đồng (n = 1290000), phương án trả tiền như sau: X1 = 1290000 DIV 100000 = 12
Số tiền cần rút còn lại là 1290000 – 12 * 100000 = 90000
X2 = 90000 DIV 50000 = 1
Số tiền cần rút còn lại là 90000 – 1 * 50000 = 40000
X3 = 40000 DIV 20000 = 2
Số tiền cần rút còn lại là 40000 – 2 * 20000 = 0
X4 = 0 DIV 10000 = 0
Ta có X = (12, 1, 2, 0), tức là máy ATM sẽ trả cho khách hàng 12 tờ 100.000 đồng, 1 tờ 50.000 đồng và 2 tờ 20.000 đồng
2.Đặc điểm của chiến lược tham lam:
Phương pháp tham lam gợi ý chúng ta 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
Trang 3hai trật tự là tăng hoặc giảm dần theo một chỉ tiêu nào đó
Giải thuật cho những bài toán tối ưu thường đi qua một số bước, với một tập hợp các chọn lựa tại mỗi bước Phương pháp tham lam luôn chọn phương án tốt nhất vào thời điểm hiện tại
Nó chọn tối ưu cục bộ với hy vọng rằng lựa chọn này sẽ dẫn đến một kết quả tối ưu toàn cục Đối với một số bài toán, đây có thể là một thuật toán không chính xác
3 Tính chất lựa chọn tham lam
Lựa chọn của thuật toán tham lam có thể phụ thuộc vào các lựa chọn trước đó Nhưng nó không thể phụ thuộc vào một lựa chọn nào trong tương lai hay phụ thuộc vào lời giải của các bài toán con Thuật toán tiến triển theo kiểu thực hiện các chọn lựa theo một vòng lặp, cùng lúc
đó thu nhỏ bài toán đã cho về một bài toán con nhỏ hơn
4 Mô tả tổng quát của thuật toán
Khác với quy hoạch động, thường giải quyết các bài toán con từ dưới lên, một chiến lược (thuật toán) tham lam thường tiến triển theo cách từ trên xuống, phương án X được xây dựng bằng cách lựa chọn từng thành phần xi của X cho đến khi hoàn chỉnh (đủ n thành phần) Với mỗi xi, ta sẽ chọn xi tối ưu Với cách này thì có thể ở bước cuối cùng ta không còn gì để chọn mà phải chấp nhận một giá trị cuối cùng còn lại
Mô tả thuật toán:
void Greedy_Method(A,X);
// Xây dựng phương án X từ tập các đối tượng A
{
X=φ;
While (A !=φ)
{
x= Select(A); // Hàm chọn x tốt nhất trong A
A=A-{x};
If (X∪{x}) chấp nhận được X=X∪{x};
}
Return (X); // phương án tối ưu
}
Ta có thể dễ dàng thấy tại sao các thuật toán như thế được gọi là “tham lam” Tại mỗi bước, nó chọn “miếng ngon nhất” (được xác định bởi hàm chọn), nếu thấy có thể đưa vào nghiệm nó sẽ
chọn ngay, nếu không nó sẽ bỏ đi, sau này không bao giờ xem xét lại
Cần lưu ý: thuật toán tham lam trong một số bài toán, nếu xây dựng được hàm chọn thích hợp
có thể cho nghiệm tối ưu Trong nhiều bài toán, thuật toán tham lam chỉ tìm được nghiệm gần đúng với nghiệm tối ưu
Trang 45.Ví dụ áp dụng:
Giải quyết bài toán trả tiền của máy rút tiền tự động ATM như đã nêu ở trên
Input : - Có n loại tiền, mỗi loại tiền có giá trị tương ứng là d1,d2, ,dn
- Lượng tiền M đồng
Output : Đổi M đồng ra tiền lẻ sao cho số loại tiền đổi là ít nhất
*Phân tích : Cần phải tìm 1 nghiệm X = (x1,x2, xm) với xi là số loại tiền thứ i có giá trị di sao cho M= i
m
i i
d x
∑
= 1
⇒Tìm cách đổi sao cho tổng loại tiền cần đổi là ít nhất Vậy ta sẽ bắt đầu đổi từ đồng có giá trị lớn nhất và cứ giảm dần cho đến khi số tiền M đã được đổi hết thì thông báo là tìm được nghiệm, ngược lại thì thông báo không đổi được
Áp dụng tư tưởng của thuật toán tham lam, ta có thể viết như sau:
void Đổitiền_Thamlam(M);
{
Const int D={d 1 ,d 2 , ,d n } // mảng lưu giá trị từng loại tiền
int x, sum, i; // x là nghiệm bài toán
- Sắp xếp D giảm dần
X=φ ; sum:=φ ; i:=1;
While (sum!=M && i <=n) // Trong khi chưa đổi hết tiền
{
x i = (M - sum)/d i ; // Số tờ tiền loại d i
if (sum+x i *d i ≤ M)
{
X=X+{x i };
sum:=sum+x i *d i ; // Số tiền đã đổi được tính đến loại tiền i
}
i++;
}
If (sum=M ) Return(X); // Trả lại nghiệm của bài toán
Else <Thông báo không đổi được>;
}
Nhận xét: - Tính chất tham lam thể hiện ở chỗ, tại mỗi bước luôn chọn “miếng ăn ngon nhất”
mà không để ý hậu quả sau này Cho nên, với một số trường hợp thuật toán cho ta nghiệm tối
ưu, nhưng trong nhiều trường hợp nghiệm tìm được không phải là nghiệm tối ưu mà chỉ gần đúng với nghiệm tối ưu, thậm chí còn không tính được nghiệm đúng
Ví dụ: Nếu M=13 và các loại tiền có mệnh giá là: d1=3, d2=4, d3=6 Cần tìm cách đổi 13 đồng sao cho số tiền đổi là ít nhất Với thuật toán trên ta cần 2 tờ 6 đồng dư 1 đồng Như vậy số tiền này sẽ không đổi được, nhưng trong thực tế, ta có thể đổi được dễ dàng với 1 tờ 6 đồng, 1 tờ 4
Trang 5đồng và một tờ 3 đồng.
6 Đánh giá chung:
*Ưu điểm :
- 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)
- Có nhiều thuật toán được thiết kế theo phương pháp 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ị định hướng, các thuật toán Prim và Kruskal tìm cây bao chùm ngắn nhất trong đồ thị vô hướng,
- Một ứng dụng quan trọng: mô hình của các chuẩn nén dữ liệu
* Nhược điểm: 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 đối tốt
- Khó để chứng minh thuật toán cho giải pháp tối ưu
B Bài tập
Vào một buổi sáng anh Bo sắp một đàn bò gồm n con bò để vắt sữa Anh dự kiến là vào sáng hôm đó, con bò thứ i có khả năng sẽ vắt được ai lít sữa Tuy nhiên đàn bò của anh có đặc tính là
cứ mỗi lần vắt sữa một con, những con còn lại trông thấy sợ quá nên sẽ bị giảm sản lượng mỗi con 01 lít sữa Nếu vắt sữa con bò thứ nhất, n-1 con còn lại bị giảm sản lượng Sau đó vắt sữa con bò thứ hai thì n-2 con còn lại bị giảm sản lượng Bạn hãy giúp anh Bo tính xem thứ tự vắt sữa bò như thế nào để số lượng sữa vắt được là nhiều nhất nhé
Dữ liệu vào: gồm 2 dòng
- Dòng thứ nhất là số nguyên n (1 ≤ n ≤ 100) là số lượng con bò
- Dòng thứ hai gồm n số nguyên a1, a2, , an (1 ≤ ai ≤ 1000) là sản lượng sữa của các con bò
Dữ liệu xuất:
- Là một số nguyên xác định số lít sữa nhiều nhất mà anh Bo có thể vắt được
Ví dụ
input
4
4 4 4 4
output
Trang 6input
4
2 1 4 3
output
6
- Trong test 1: vắt sữa con bò 1 (được 4), lượng sữa còn lại là 3 3 3; vắt sữa con bò 2 (được 3), lượng sữa còn lại là 2 2, vắt sữa con bò 3 (được 2) và con bò 4 (được 1), tổng cộng 10
- Trong test 2: vắt sữa con bò 1 (được 2), lượng sữa còn lại là 0 3 2; vắt sữa con bò 3 (được 3)
và vắt sữa con bò 4 (được 1) tổng cộng 6
Ý tưởng thuật toán:
Ta sử dụng thuật toán là tham lam, ở mỗi thời điểm ta chọn con bò còn nhiều sữa nhất để vắt
Đầu tiên, ta sắp xếp theo chiều giảm dần số lít sữa mà mỗi con bò có thể vắt được
Sau đó ta vắt từ con bò có nhiều sữa nhất đến khi không còn con bò nào có thể vắt sữa, mỗi lần vắt sữa thì những con bò còn lại giảm đi 1 lít sữa
Độ phức tạp: O(nlogn)
Code tham khảo:
#include <bits/stdc++.h>
using namespace std;
int n,a[1001],kq=0;
bool cmp(int p, int q)
{
return p>q;
}
int main()
{
cin >> n;
for (int i=0; i<n; i++) cin >> a[i];
Trang 7sort (a,a+n,cmp);
for (int i=0; i<n; i++) kq=kq+max(0,a[i]-i);
cout << kq;
}
Bài toán 2: THANGMAY
Có n người đang đứng chờ trước một thang máy duy nhất tại tầng trệt trong một tòa cao ốc cao 2000 tầng, họ muốn đi đến các tầng trong tòa nhà Các tầng của cao ốc được đánh số 1, 2, 3,
4, , 2000 Tầng trệt là tầng 1 Người thứ i muốn đi đến tầng ai Thang máy chỉ chở được k người cùng lúc Thời gian thang máy đi từ tầng x đến tầng y là |x - y| giây Hãy tính thời gian tối thiểu để thang máy có thể vận chuyển hết n người đến tầng mà họ mong muốn và thang máy quay trở lại tầng trệt (giả sử thời gian ra vào thang máy là không đáng kể)
Dữ liệu nhập: gồm 2 dòng
- Dòng thứ nhất là 2 số nguyên n, k cách nhau một khoảng trắng (1 ≤ n, k ≤ 2000)
- Dòng thứ hai gồm n số nguyên ai, mỗi số cách nhau một khoảng trắng (2 ≤ ai ≤ 2000)
Dữ liệu xuất:
- Là một số nguyên xác định thời gian tối thiểu để đạt được mục đích
Ví dụ
input
3 2
2 3 4
output
8
input
4 2
50 100 50 100
output
296
input
10 3
2 2 2 2 2 2 2 2 2 2
output
8
Trong test 1:
- Lần thứ nhất chở 2 người lên tầng 3, 4 và quay lại trệt: 6 giây
Trang 8- Lần thứ hai chở 1 người lên tầng 2 và quay lại trệt: 2 giây.
Ý tưởng thuật toán:
Sử dụng thuật toán tham lam
Sắp xếp mảng a theo chiều tăng dần
Vì mỗi lần vận chuyển chỉ được chở tối đa k người
Để có được kết quả tối ưu, ở lần vận chuyển thứ 1 ta sẽ vận chuyển n%k người đầu tiên trước, sau đó mỗi lần quay lại vận chuyển k người cho đến khi vận chuyển hết
Độ phức tạp: O(nlogn)
Code tham khảo:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int i,n,k, a[2001];
cin >> n >> k;
for (i=1; i<=n; i++) cin >> a[i];
sort(a+1, a+n+1);
long kq=max(0,a[n%k]-1);
for (i=n%k+k; i<=n; i+=k)
kq=kq+a[i]-1;
cout << kq*2;
}
Bài toán 3: CHENDAU
Cho dãy số nguyên A gồm các số từ 1 -> n Tìm cách chèn (n-1) dấu ‘+’ hoặc ‘–‘ vào giữa các số sao cho khi tính biểu thức đó cho kết quả là S
Input :
Cho n và S (1 ≤ n ≤ 500, |S| ≤ 125250)
Output :
Nếu có xuất ra biểu thức, không thì xuất ‘Impossible’
Ví dụ:
Input:
9 5
Trang 91+2-3+4+5+6+7-8-9
Input:
5 6
Output:
Impossible
Ý tưởng thuật toán:
Ở bài này ta sẽ dùng thuật toán tham lam
Trước hết ta tính tổng các số từ 1 -> n thông qua biến sum
=> sum = n*(n+1)/2
Bây giờ ta sẽ tìm cách chèn dấu ‘ – ‘ vào các số để được S
Nhận xét ta sẽ thấy rằng khi chèn thêm dấu ‘ – ‘ vào trước một số x nào đó thì tổng sum ban đầu của ta phải giảm đi 2x lần vì trừ một lần để triệt tiêu x ban đầu, trừ thêm một lần cho việc đặt dấu ‘ – ‘
Do đó khi ta chèn thêm dấu trừ vào thì sum luôn giảm chẵn lần để được S hay nói cách khác sum – 2c = S -> sum – S = 2c là số chẵn , trong đó c là tổng các số ta cần đặt dấu trừ trước nó
Do đó ta có ngay một điều kiện để loại bỏ việc giải bài toán là sum – S chẵn Bây giờ ta sẽ tìm các số sao cho tổng các số bằng c = 2c/2 = (sum – S) / 2 Thuật toán tham lam như sau : Xét từ n -> 2 nếu số nào c trừ được vẫn giữ cho không âm thì ta cứ trừ Đến cuối cùng nếu c khác 0 thì xem như không
có cách
Độ phức tạp: O(n)
Code tham khảo:
#include <bits/stdc++.h>
using namespace std;
long n,s;
bool a[600]={};
int main()
{
scanf("%d%d",&n,&s);
long sum=n*(n+1)/2 , c=sum - s;
if (c%2==1)
{
Trang 10printf("Impossible");
return 0;
}
c=c/2;
for (long i=n; i>=2; i )
{
if (c-i>= 0)
{
a[i]=true;
c=c-i;
}
}
if (c!=0) printf("Impossible");
else
{
printf("1");
for (long i=2; i<=n; i++)
{
if (a[i]) printf("-");
else printf("+");
printf("%d",i);
}
}
return 0;
}
Bài toán 4: OTO
Một cơ sở sửa chữa ô tô có nhận n chiếc xe để sửa Do các nhân viên làm việc quá lười nhác nên đã đến hạn trả cho khách hàng mà vẫn chưa tiến hành sửa được chiếc xe nào Theo hợp đồng đã ký kết từ trước, nếu bàn giao xe thứ i quá hạn ngày nào thì sẽ phải trả thêm một khoản tiền phạt là A[i]
Ông chủ cơ sở sửa chữa quyết định sa thải toàn bộ công nhân và thuê nhân công mới Với lực lượng mới này, ông ta dự định rằng để sửa chiếc xe thứ i sẽ cần B[i] ngày Vấn đề đặt ra đối với ông là phải lập lịch sửa tuần tự các chiếc xe sao cho tổng số tiền bị phạt là ít nhất
Yêu cầu: Hãy lập lịch sửa xe giúp cho ông chủ cơ sở sửa chữa ô tô
Trang 11• Dòng 1: Chứa số n (n ≤ 10000)
• Dòng 2: Chứa n số nguyên dương A[1], A[2], , A[n] (1 ≤ A[i] ≤ 10000)
• Dòng 3: Chứa n số nguyên dương B[1], B[2], , B[n] (1 ≤ B[i] ≤ 100)
Output:
• Dòng 1: Ghi số tiền bị phạt tối thiểu
• Dòng 2: Ghi số hiệu các xe sẽ tiến hành sửa chữa, theo thứ tự từ xe được sửa đầu tiên đến xe sửa sau cùng
Ví dụ:
Input:
4
1 3 4 2
3 2 3 1
Output:
44
4 2 3 1
Xong công việc 4 vào cuối ngày 1 => phải trả 2 * 1 = 2
Xong công việc 2 vào cuối ngày 3 => phải trả 3 * 3 = 9
Xong công việc 3 vào cuối ngày 6 => phải trả 6 * 4 = 24
Xong công việc 1 vào cuối ngày 9 => phải trả 1 * 9 = 9
Vậy tổng cộng phải trả 44
Ý tưởng thuật toán:
Ta sửa dụng phương pháp tham lam để giải quyết bài toán như sau
Đầu tiên, ta sắp các xe lại theo A/B giảm dần, sau đó lần lượt sửa các xe theo đúng thứ tự đó
Chứng minh tính đúng đắn của phương pháp:
Để chứng minh cách làm trên là đúng, ta xét một lịch sửa xe bất kì, trong đó tồn tại 2 xe i và i+1 có A[i]/B[i]<A[i+1]/B[i+1]
Cần chứng minh khi đổi vị trí 2 xe đó thì tổng tiền phạt sẽ giảm
Khi đổi vị trí 2 xe, chỉ có tiền phạt của 2 xe đó bị thay đổi, còn các xe trước
và sau đó vẫn giữ nguyên Chứng minh:
Gọi S là thời gian để sửa tất cả các xe từ 1 đến i-1
Ta có:
Trang 12X=(S+B[i])*A[i]+(S+B[i]+B[i+1])*A[i+1] là tổng tiền phạt của 2 xe i và i+1 theo thứ tự ban đầu
Y=(S+B[i+1])*A[i+1]+(S+B[i+1]+B[i])*A[i] là tổng tiền phạt của 2 xe sau khi hoán đổi vị trí cho nhau
=> X-Y=B[i]*A[i+1]-B[i+1]*A[i]>0 vì A[i]/B[i]<A[i+1]/B[i+1]
Vậy X>Y (đpcm)
Độ phức tạp: O(nlogn)
Code tham khảo:
#include <bits/stdc++.h>
using namespace std;
#define MAX 10000+5
typedef struct {
int a, b, i; double x;
} car;
bool cmp( const car & a, const car & b ) {
return a.x < b.x;
}
car v[MAX];
int n;
int main()
{
int n;
cin >> n;
for(int i=1; i <=n; i++) cin >> v[i].a;
for(int i=1; i <=n; i++)
{
cin >> v[i].b;
v[i].x=(double) v[i].a/v[i].b;
v[i].i=i;
}
sort(v+1,v+n+1,cmp);
long long money=0, d=0;
for( int i=n; i>=1; i )
{
d += v[i].b;
money += d*v[i].a;
}
cout << money << "\n";