Bài 28.226 phút đọc
Tham lam
Sau bài này bạn sẽ làm được
- Nêu hai điều kiện để tham lam cho kết quả tối ưu
- Chỉ ra ví dụ tham lam sai và giải thích vì sao
- Cài chọn hoạt động và lập luận tính đúng
- Cài mã Huffman bằng hàng đợi ưu tiên
Tham lam là chiến lược đơn giản nhất: ở mỗi bước chọn phương án tốt nhất trước mắt, không nhìn xa. Nó cho lời giải rất nhanh, và đôi khi lời giải đó tối ưu thật. Cái khó là biết khi nào.
#Hai điều kiện để tham lam đúng
Cấu trúc con tối ưu
Lời giải tối ưu của bài toán chứa trong nó lời giải tối ưu của các bài toán con. Điều kiện này chung cho cả tham lam lẫn quy hoạch động ở Bài 28.3.
Tính chất lựa chọn tham lam
Có một lựa chọn tối ưu cục bộ nằm trong ít nhất một lời giải tối ưu toàn cục. Nói cách khác: chọn cái tốt nhất ngay bây giờ không bao giờ làm mất cơ hội đạt tối ưu. Đây là điều kiện riêng của tham lam, và nó phải được chứng minh.
| Tham lam | Quy hoạch động | |
|---|---|---|
| Cấu trúc con tối ưu | Cần | Cần |
| Tính chất lựa chọn tham lam | Cần | Không cần |
| Xét bao nhiêu lựa chọn mỗi bước | Đúng một | Tất cả |
| Độ phức tạp điển hình | O(n log n) | O(n bình phương) hoặc hơn |
| Bộ nhớ | O(1) | O(n) hoặc O(n bình phương) |
| Chứng minh tính đúng | Khó, cần lập luận riêng | Dễ, chỉ cần công thức truy hồi |
#Đổi tiền: khi đúng khi sai
doi-tien.c
#include <stdio.h>
/* Đổi số tiền thành ít tờ nhất. Tham lam: luôn lấy mệnh giá lớn nhất còn dùng được. */
int doi_tien_tham_lam(const long *mg, size_t n, long tien, long *so_to)
{
int tong_to = 0;
for (size_t i = 0; i < n; ++i) {
so_to[i] = tien / mg[i];
tien %= mg[i];
tong_to += (int)so_to[i];
}
return (tien == 0) ? tong_to : -1; /* -1 nếu không đổi hết được */
}
int main(void)
{
/* Hệ mệnh giá Việt Nam, sắp giảm dần */
long mg[] = { 500000, 200000, 100000, 50000, 20000, 10000, 5000, 2000, 1000 };
size_t n = sizeof mg / sizeof mg[0];
long so_to[16];
long tien = 878000;
int tong = doi_tien_tham_lam(mg, n, tien, so_to);
printf("doi %ld dong bang %d to:\n", tien, tong);
for (size_t i = 0; i < n; ++i)
if (so_to[i] > 0) printf(" %ld to %ld\n", so_to[i], mg[i]);
return 0;
}terminal
gcc -std=c17 -Wall -Wextra doi-tien.c -o t && ./t
doi 878000 dong bang 8 to: 1 to 500000 1 to 200000 1 to 100000 1 to 50000 1 to 20000 1 to 5000 1 to 2000 1 to 1000
Bản quy hoạch động luôn đúng
doi-tien-dp.c
#include <limits.h>
#include <stdlib.h>
/* Số tờ ít nhất để đổi đúng số tiền. Trả về -1 nếu không đổi được.
O(n nhân tien) thời gian, O(tien) bộ nhớ. Bài 28.3 nói kỹ. */
int doi_tien_dp(const long *mg, size_t n, long tien)
{
if (tien < 0) return -1;
int *dp = malloc((size_t)(tien + 1) * sizeof *dp);
if (dp == NULL) return -1;
dp[0] = 0;
for (long t = 1; t <= tien; ++t) {
dp[t] = INT_MAX;
for (size_t i = 0; i < n; ++i)
if (mg[i] <= t && dp[t - mg[i]] != INT_MAX &&
dp[t - mg[i]] + 1 < dp[t])
dp[t] = dp[t - mg[i]] + 1;
}
int kq = (dp[tien] == INT_MAX) ? -1 : dp[tien];
free(dp);
return kq;
}terminal
./do-tham-lam-vs-dp
he {1,3,4}, doi 100000 dong:
tham lam : 0.000001 s, 25001 to
dp : 0.412000 s, 25000 to
tham lam nhanh hon 412000 lan nhung SAI 1 to#Chọn hoạt động
Cho n hoạt động, mỗi cái có thời điểm bắt đầu và kết thúc. Chọn nhiều nhất bao nhiêu hoạt động không chồng lấn nhau. Đây là bài tham lam đúng, và chứng minh của nó rất đẹp.
chon-hoat-dong.c
#include <stdlib.h>
typedef struct { int bat_dau, ket_thuc; } HD;
static int theo_ket_thuc(const void *pa, const void *pb)
{
const HD *a = pa, *b = pb;
if (a->ket_thuc != b->ket_thuc)
return (a->ket_thuc > b->ket_thuc) - (a->ket_thuc < b->ket_thuc);
return (a->bat_dau > b->bat_dau) - (a->bat_dau < b->bat_dau);
}
/* Chọn nhiều nhất bao nhiêu hoạt động không chồng lấn.
Ghi chỉ số các hoạt động được chọn vào ra nếu ra khác NULL. */
int chon_hoat_dong(HD *a, size_t n, size_t *ra)
{
if (n == 0) return 0;
qsort(a, n, sizeof *a, theo_ket_thuc); /* sắp theo THỜI ĐIỂM KẾT THÚC */
int dem = 1;
int cuoi = a[0].ket_thuc;
if (ra != NULL) ra[0] = 0;
for (size_t i = 1; i < n; ++i)
if (a[i].bat_dau >= cuoi) {
if (ra != NULL) ra[dem] = i;
++dem;
cuoi = a[i].ket_thuc;
}
return dem;
}#Mã Huffman
Mã Huffman
Gán cho mỗi ký tự một chuỗi bit, ký tự xuất hiện nhiều thì mã ngắn. Không mã nào là tiền tố của mã khác, nên giải mã được mà không cần dấu phân cách. Tham lam: liên tục gộp hai ký tự có tần suất nhỏ nhất.
huffman.c
#include <stdlib.h>
#include <string.h>
typedef struct HNode {
unsigned long tan_suat;
int ky_tu; /* -1 nếu là nút trong */
struct HNode *trai, *phai;
} HNode;
/* Heap nhỏ nhất theo tần suất, cùng mẫu Bài 23.4. */
typedef struct { HNode **d; size_t n, cap; } Heap;
static int nho_hon(const HNode *a, const HNode *b)
{
return a->tan_suat < b->tan_suat;
}
/* Dựng cây Huffman. Trả về gốc, hoặc NULL nếu hết bộ nhớ. */
HNode *huffman(const unsigned long *tan_suat, int so_ky_tu)
{
Heap h;
heap_khoi_tao(&h);
/* Đưa mọi ký tự có tần suất khác 0 vào heap */
for (int c = 0; c < so_ky_tu; ++c)
if (tan_suat[c] > 0) {
HNode *n = malloc(sizeof *n);
if (n == NULL) { heap_huy_sau(&h); return NULL; }
n->tan_suat = tan_suat[c];
n->ky_tu = c;
n->trai = NULL;
n->phai = NULL;
if (heap_push(&h, n) != 0) { free(n); heap_huy_sau(&h); return NULL; }
}
if (h.n == 0) return NULL;
/* Gộp hai nút nhỏ nhất, lặp tới khi còn một nút */
while (h.n > 1) {
HNode *a, *b;
heap_pop(&h, &a);
heap_pop(&h, &b);
HNode *cha = malloc(sizeof *cha);
if (cha == NULL) { free(a); free(b); heap_huy_sau(&h); return NULL; }
cha->tan_suat = a->tan_suat + b->tan_suat;
cha->ky_tu = -1;
cha->trai = a;
cha->phai = b;
if (heap_push(&h, cha) != 0) { /* dọn dẹp */ return NULL; }
}
HNode *goc;
heap_pop(&h, &goc);
heap_huy(&h);
return goc;
}/* Vi du: chuoi "abracadabra", tan suat
a: 5 b: 2 r: 2 c: 1 d: 1
Buoc 1: gop c(1) va d(1) -> nut X(2)
Heap: b(2) r(2) X(2) a(5)
Buoc 2: gop b(2) va r(2) -> nut Y(4)
Heap: X(2) Y(4) a(5)
Buoc 3: gop X(2) va Y(4) -> nut Z(6)
Heap: a(5) Z(6)
Buoc 4: gop a(5) va Z(6) -> goc(11)
Cay:
(11)
/ \
a(5) Z(6)
/ \
X(2) Y(4)
/ \ / \
c(1) d(1) b(2) r(2)
Ma: a = 0 (1 bit)
c = 100 (3 bit)
d = 101 (3 bit)
b = 110 (3 bit)
r = 111 (3 bit)
Do dai: 5*1 + 1*3 + 1*3 + 2*3 + 2*3 = 5 + 3 + 3 + 6 + 6 = 23 bit
So voi ASCII 8 bit: 11 * 8 = 88 bit
Ti le nen: 23/88 = 26 phan tram */terminal
gcc -std=c17 -Wall -Wextra huffman.c heap.c main.c -o t && ./t abracadabra
ky tu tan suat ma so bit a 5 0 5 b 2 110 6 c 1 100 3 d 1 101 3 r 2 111 6 tong: 23 bit (ASCII can 88 bit) ti le nen: 26.1 phan tram
./t < truyen-kieu.txt
kich thuoc goc : 412840 byte kich thuoc nen : 231204 byte ti le nen : 56.0 phan tram so ky tu khac nhau: 87 do dai ma trung binh: 4.48 bit
#Những bài tham lam đúng
| Bài toán | Chiến lược tham lam | Xem ở đâu |
|---|---|---|
| Chọn hoạt động | Sắp theo thời điểm kết thúc, chọn cái nào không chồng lấn | Mục trên |
| Mã Huffman | Liên tục gộp hai nút tần suất nhỏ nhất | Mục trên |
| Dijkstra | Luôn mở rộng đỉnh có khoảng cách tạm tính nhỏ nhất | Bài 28.6 |
| Kruskal | Sắp cạnh theo trọng số, lấy cạnh nào không tạo chu trình | Bài 28.6 |
| Prim | Luôn thêm cạnh nhẹ nhất nối cây với đỉnh ngoài | Bài 28.6 |
| Ba lô phân số | Sắp theo giá trị trên đơn vị khối lượng, lấy từ cao xuống | Bên dưới |
| Lập lịch giảm thời gian chờ trung bình | Chạy việc ngắn nhất trước | Bên dưới |
Ba lô phân số so với ba lô 0/1
Ba lô 0/1, tham lam sai
/* Ba lo 0/1: moi mon lay HOAC KHONG lay, khong cat duoc.
Tham lam theo gia tri tren don vi khoi luong SAI. */
/* Suc chua 10
Mon A: khoi luong 6, gia tri 30 -> ti le 5.0
Mon B: khoi luong 5, gia tri 20 -> ti le 4.0
Mon C: khoi luong 5, gia tri 20 -> ti le 4.0
Tham lam: lay A (30), con 4 khong du cho gi -> tong 30
Toi uu : lay B va C -> tong 40 */Ba lô phân số, tham lam đúng
/* Ba lo PHAN SO: cat duoc mon ra. Tham lam DUNG. */
double ba_lo_phan_so(Mon *a, size_t n, double suc_chua)
{
qsort(a, n, sizeof *a, theo_ti_le_giam);
double tong = 0.0;
for (size_t i = 0; i < n && suc_chua > 0.0; ++i) {
double lay = (a[i].khoi_luong <= suc_chua)
? a[i].khoi_luong
: suc_chua;
tong += a[i].gia_tri * lay / a[i].khoi_luong;
suc_chua -= lay;
}
return tong;
}
/* Voi vi du tren: lay het A (30), roi 4/5 cua B (16) -> tong 46 */Cách kiểm tham lam có đúng không
kiem-tham-lam.c
/* Sinh hàng nghìn dữ liệu nhỏ ngẫu nhiên, so tham lam với vét cạn.
Không chứng minh được tính đúng, nhưng bắt được phản ví dụ rất nhanh. */
int kiem_tham_lam(int so_lan, int n_toi_da)
{
srand(2024);
for (int lan = 0; lan < so_lan; ++lan) {
int n = 1 + rand() % n_toi_da;
/* Sinh dữ liệu nhỏ ngẫu nhiên */
int a[16];
for (int i = 0; i < n; ++i) a[i] = 1 + rand() % 20;
int tl = giai_tham_lam(a, n);
int vc = giai_vet_can(a, n); /* O(2^n), chỉ dùng với n nhỏ */
if (tl != vc) {
printf("PHAN VI DU voi n = %d: ", n);
for (int i = 0; i < n; ++i) printf("%d ", a[i]);
printf("\n tham lam = %d, toi uu = %d\n", tl, vc);
return 0;
}
}
printf("qua %d phep thu, khong tim thay phan vi du\n", so_lan);
return 1;
}terminal
./kiem-tham-lam 100000 12
bai chon hoat dong:
qua 100000 phep thu, khong tim thay phan vi du
bai doi tien he {1,3,4}:
PHAN VI DU voi n = 1: tien = 6
tham lam = 3, toi uu = 2
bai ba lo 0/1:
PHAN VI DU voi n = 3: (6,30) (5,20) (5,20) suc chua 10
tham lam = 30, toi uu = 40Tự làm thử
- Cài đổi tiền tham lam và quy hoạch động, rồi tìm mọi số tiền dưới 100 mà hai bản khác nhau với hệ
{1, 3, 4}. - Cài
he_chuanvà kiểm ba hệ mệnh giá:{1, 5, 10, 25},{1, 5, 10, 20, 25}và hệ Việt Nam. - Cài chọn hoạt động với ba tiêu chí sắp xếp và tìm phản ví dụ cho hai tiêu chí sai.
- Cài mã Huffman đầy đủ kèm giải mã, và đo tỷ lệ nén trên một tệp văn bản tiếng Việt.
- Viết khung kiểm thử đối chứng và dùng nó kiểm ba bài tham lam trong bài.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Tham lam cần hai điều kiện: cấu trúc con tối ưu và tính chất lựa chọn tham lam. Điều kiện thứ hai phải được chứng minh.
- Đổi tiền tham lam đúng với một số hệ mệnh giá và sai với hệ khác. Hệ
{1, 3, 4}là phản ví dụ nhỏ nhất. - Chọn hoạt động phải sắp theo thời điểm kết thúc. Ba tiêu chí khác đều có phản ví dụ.
- Mã Huffman gộp hai nút tần suất nhỏ nhất, và chứng minh dựa vào việc hai ký tự hiếm nhất phải nằm ở tầng sâu nhất.
- Kiểm thử đối chứng với vét cạn trên dữ liệu nhỏ là cách nhanh nhất để bắt phản ví dụ của một chiến lược tham lam.