Hướng dẫn giải của Vườn treo Babylon
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.
Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.
Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.
Bạn phải đăng nhập để mở lời giải này.
Đăng nhậpTác giả: ,
Editorial for vtbaby: Vườn treo Babylon
Hiểu bài toán
Bài toán yêu cầu xây dựng một tòa tháp càng cao càng tốt bằng cách xếp chồng các khối đá lên nhau. Mỗi khối đá là hình hộp chữ nhật với chiều dài, rộng, cao cho trước. Có vô số khối đá của mỗi loại. Khi xếp chồng, một khối có thể đặt lên trên một khối khác nếu và chỉ nếu cả chiều dài và chiều rộng của khối trên đều nhỏ hơn chiều dài và chiều rộng của khối dưới (tại mặt phẳng tiếp xúc). Các khối có thể xoay tùy ý (tức là có thể chọn bất kỳ mặt nào làm đáy và chiều cao tương ứng). Mục tiêu: Tìm chiều cao tối đa của tòa tháp có thể xây được.
Các cách tiếp cận
Cách Quy hoạch động cơ bản (Dynamic Programming)
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int l, w, h;
} Block;
int cmp(const void *a, const void *b) {
Block *x = (Block*)a;
Block *y = (Block*)b;
// Sắp xếp giảm dần theo diện tích đáy
long long area1 = (long long)x->l * x->w;
long long area2 = (long long)y->l * y->w;
if (area1 != area2) return (area1 > area2) ? -1 : 1;
// Nếu diện tích bằng, ưu tiên chiều cao lớn hơn (không bắt buộc nhưng tốt cho DP)
return y->h - x->h;
}
int main() {
int n;
while (scanf("%d", &n) == 1 && n != 0) {
Block blocks[600]; // 200 loai * 3 huong
int cnt = 0;
for (int i = 0; i < n; i++) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
// 3 huong xoay (chon 1 chieu lam cao)
// Truong hop 1: chieu cao la z
int l = x, w = y;
if (l < w) { int t = l; l = w; w = t; }
blocks[cnt++] = (Block){l, w, z};
// Truong hop 2: chieu cao la y
l = x; w = z;
if (l < w) { int t = l; l = w; w = t; }
blocks[cnt++] = (Block){l, w, y};
// Truong hop 3: chieu cao la x
l = y; w = z;
if (l < w) { int t = l; l = w; w = t; }
blocks[cnt++] = (Block){l, w, x};
}
// Sap xep giam dan theo kich thuoc day (l*w)
qsort(blocks, cnt, sizeof(Block), cmp);
// Quy hoach dong
int dp[600];
int max_h = 0;
for (int i = 0; i < cnt; i++) {
dp[i] = blocks[i].h;
for (int j = 0; j < i; j++) {
// Kiem tra dieu kien an toan: day phai lon hon day duoi
if (blocks[j].l > blocks[i].l && blocks[j].w > blocks[i].w) {
if (dp[j] + blocks[i].h > dp[i]) {
dp[i] = dp[j] + blocks[i].h;
}
}
}
if (dp[i] > max_h) max_h = dp[i];
}
printf("%d\n", max_h);
}
return 0;
}
- Time Complexity: O(N^2)
- Space Complexity: O(N)
Hướng tiếp cận cơ bản nhất là sử dụng quy hoạch động. Đầu tiên, ta tạo ra tất cả các biến thể có thể của các khối đá bằng cách xoay chúng (mỗi khối có 3 cách xoay để thay đổi chiều cao). Sau đó, ta sắp xếp các khối theo diện tích đáy giảm dần. Lý do sắp xếp là để khi duyệt qua các khối, ta có thể đảm bảo rằng mọi khối có thể nằm trên khối hiện tại đều đã được xét (vì diện tích nhỏ hơn thường nằm sau trong mảng đã sắp xếp giảm dần, nhưng thực tế ta cần kiểm tra điều kiện 'lớn hơn' nên ta sẽ so sánh chỉ số trước và sau). Trong thuật toán này, dp[i] lưu chiều cao lớn nhất của tháp kết thúc bằng khối thứ i. Với mỗi khối i, ta duyệt qua tất cả các khối j < i. Nếu khối i có thể đặt lên trên khối j (tức là cả chiều dài và rộng của i đều nhỏ hơn của j), ta cập nhật dp[i] = max(dp[i], dp[j] + height[i]). Giá trị lớn nhất trong mảng dp là kết quả.
Cách Optimized DP with Sorting
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int l, w, h;
} Block;
int cmp(const void *a, const void *b) {
Block *x = (Block*)a;
Block *y = (Block*)b;
long long area1 = (long long)x->l * x->w;
long long area2 = (long long)y->l * y->w;
return (area1 > area2) ? -1 : 1;
}
int main() {
int n;
while (scanf("%d", &n) == 1 && n != 0) {
Block blocks[600];
int cnt = 0;
for (int i = 0; i < n; i++) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
// Sinh 3 hướng xoay, chuẩn hóa để length >= width
int dims[3][3] = {{x, y, z}, {y, z, x}, {z, x, y}};
for (int k = 0; k < 3; k++) {
int l = dims[k][0];
int w = dims[k][1];
int h = dims[k][2];
if (l < w) { int t = l; l = w; w = t; }
blocks[cnt++] = (Block){l, w, h};
}
}
qsort(blocks, cnt, sizeof(Block), cmp);
int dp[600];
int ans = 0;
for (int i = 0; i < cnt; i++) {
dp[i] = blocks[i].h;
for (int j = 0; j < i; j++) {
// Do đã sort giảm dần diện tích, chỉ cần so sánh l hoặc w là đủ
// Tuy nhiên điều kiện an toàn vẫn là cả 2 chiều đều nhỏ hơn
if (blocks[j].l > blocks[i].l && blocks[j].w > blocks[i].w) {
if (dp[j] + blocks[i].h > dp[i])
dp[i] = dp[j] + blocks[i].h;
}
}
if (dp[i] > ans) ans = dp[i];
}
printf("%d\n", ans);
}
return 0;
}
- Time Complexity: O(N^2)
- Space Complexity: O(N)
Đây là phiên bản chuẩn hóa và tối ưu hóa của hướng đầu tiên. Logic cơ bản tương tự: Sinh các hướng -> Sắp xếp -> Quy hoạch động. Code được viết gọn gàng hơn, sử dụng mảng 2 chiều để sinh hướng xoay. Việc sắp xếp giảm dần diện tích đáy đảm bảo rằng khi ta xét dp[i], các khối có thể nằm bên dưới nó (diện tích lớn hơn) đã được xử lý trước. Tuy nhiên, ta vẫn phải kiểm tra điều kiện l và w một cách chính xác vì diện tích lớn hơn không đảm bảo rằng cả hai chiều đều lớn hơn. Ví dụ: (10, 1) diện tích 10 không thể chứa (9, 2) diện tích 18.
Cách Thuật toán tham lam (Greedy) - Phân tích sai
// Lưu ý: Đây là một hướng tiếp cận THAM LAM thường bị nhầm lẫn.
// Nó KHÔNG giải quyết đúng bài toán này vì điều kiện 'cả hai chiều'
// dẫn đến các cấu trúc phức tạp không tối ưu theo局部最优.
// Ví dụ: Khối (5,5,10) và (4,6,10).
// Tham lam có thể chọn (5,5) rồi không còn chỗ cho (4,6) hoăc ngược lại.
// Code minh họa cho thấy tại sao Greedy không hoạt động:
// 1. Sắp xếp giảm dần chiều cao? -> Sai.
// 2. Chọn khối diện tích lớn nhất? -> Sai.
// 3. Chọn khối cao nhất? -> Sai.
// Chỉ có Quy hoạch động là chính xác trong bài toán này.
- Time Complexity: O(N log N)
- Space Complexity: O(1)
Một số người có thể cố gắng áp dụng thuật toán tham lam, ví dụ: 'luôn chọn khối có diện tích đáy lớn nhất', hoặc 'luôn chọn khối cao nhất'. Tuy nhiên, bài toán này có cấu trúc phụ thuộc phức tạp (2 chiều dài phải nhỏ hơn). Một khối diện tích lớn nhưng 'hẹp' (ví dụ 10x1) có thể không chứa được một khối diện tích nhỏ hơn nhưng 'vuông' (ví dụ 5x5). Do đó, các quyết định tham lam cục bộ không đảm bảo có được tổng chiều cao tối đa. Đây là bài toán quy hoạch động kinh điển.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(N^2) | O(N) | Quy hoạch động cơ bản (Dynamic Programming) |
| 2 | O(N^2) | O(N) | Optimized DP with Sorting |
| 3 | O(N log N) | O(1) | Thuật toán tham lam (Greedy) - Phân tích sai |
Bài học kinh nghiệm
- Biến đổi bài toán thành bài toán tìm đường đi dài nhất trong đồ thị có hướng (hoặc quy hoạch động): Xét mỗi khối là một đỉnh, có cạnh từ khối A đến khối B nếu B có thể nằm trên A.
- Sinh tất cả các biến thể xoay của các khối đá. Mỗi khối cho 3 cách chọn chiều cao.
- Sắp xếp các khối theo diện tích đáy giảm dần. Điều này giúp việc kiểm tra điều kiện 'đáy nhỏ hơn' trở nên đơn giản hơn trong vòng lặp lồng nhau (hoặc cải thiện hiệu năng duyệt)
- Quy hoạch động:
dp[i] = max(dp[j] + h_i)với mọijmàicó thể đặt trênj.
Lỗi thường gặp
- Quên xoay các khối đá: Nếu chỉ sử dụng mỗi loại 1 khối, ta sẽ bỏ lỡ các cấu trúc cao hơn.
- Sai điều kiện an toàn: Đặt khối lên khi chỉ cần 1 chiều nhỏ hơn. Yêu cầu là CẢ HAI chiều đều phải nhỏ hơn.
- Lỗi tràn số (Integer Overflow): Khi tính diện tích hoặc tổng chiều cao, nếu dùng
intcho diện tích lớn (các biến thể của 200 số) có thể bị tràn. Nên dùnglong longcho phép so sánh diện tích hoặc kiểm tra kỹ giới hạn. - Khởi tạo DP sai: Phải khởi tạo
dp[i] = h[i]trước khi lặp.
Bình luận