Hướng dẫn giải của Khăn đỏ và bó hoa tặng bà
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 flower: Khăn đỏ và bó hoa tặng bà
Hiểu bài toán
Bài toán yêu cầu tìm đường đi từ ô (1, 1) đến ô (M, N) trên một lưới M x N. Các ô có thể:
- Có số nguyên dương: đại diện cho hoa với độ đẹp bằng số đó.
- Có số 0: ô trống, không có hoa.
- Có số -1: ô có sói, không thể đi vào.
Quy tắc di chuyển: Chỉ được phép di chuyển sang phải (cùng hàng, cột kế tiếp) hoặc xuống dưới (hàng kế tiếp, cùng cột).
Mục tiêu: Tìm đường đi hợp lệ (không đi qua ô -1) sao cho tổng độ đẹp của các bông hoa trên đường đi là lớn nhất. Nếu không có đường đi hợp lệ,输出 -1.
Lưu ý: Đề bài không đề cập đến việc có thu thập hoa ở ô bắt đầu (1,1) hay ô kết thúc (M,N), nhưng các giải pháp đều bao gồm giá trị của chúng vào tổng. Các giải pháp cũng không loại trừ các ô trống (0) ra khỏi tính toán (cộng 0 không ảnh hưởng).
Các cách tiếp cận
Cách Quy hoạch động (Dynamic Programming)
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int max(int a, int b) {
return a > b ? a : b;
}
int main() {
int m, n;
scanf("%d %d", &m, &n);
// Dùng mảng 2D lưu giá trị và DP
// Kích thước tối đa 1005 theo đề bài
int a[1005][1005];
int dp[1005][1005];
// Khởi tạo các ô biên và đọc dữ liệu
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
if (i == 0 || j == 0) {
dp[i][j] = -1; // Ô ngoài lề không hợp lệ
} else {
scanf("%d", &a[i][j]);
}
}
}
// Xử lý ô bắt đầu (1,1)
if (a[1][1] == -1) {
printf("-1");
return 0;
}
dp[1][1] = a[1][1];
// Duyệt qua tất cả các ô
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (i == 1 && j == 1) continue; // Đã xử lý
if (a[i][j] == -1) {
dp[i][j] = -1; // Ô sói, không đi được
continue;
}
// Lấy giá trị lớn nhất từ trên xuống và từ trái sang
int from_top = dp[i - 1][j];
int from_left = dp[i][j - 1];
int best_prev = -1;
if (from_top != -1) best_prev = max(best_prev, from_top);
if (from_left != -1) best_prev = max(best_prev, from_left);
if (best_prev != -1) {
dp[i][j] = best_prev + a[i][j];
} else {
dp[i][j] = -1; // Không thể đến được ô này
}
}
}
printf("%d", dp[m][n]);
return 0;
}
- Time Complexity: O(M x N)
- Space Complexity: O(M x N)
Đây là cách tiếp cận chuẩn cho bài toán đường đi trên lưới có ràng buộc.
- Mảng
dp[i][j]lưu tổng độ đẹp lớn nhất có thể đạt được khi đến ô (i, j). - Khởi tạo
dp[1][1] = a[1][1](nếu không phải sói). - Với mỗi ô (i, j), ta cập nhật giá trị dựa trên hai nguồn:
- Từ trên xuống:
dp[i-1][j] - Từ trái sang:
dp[i][j-1]
- Từ trên xuống:
- Nếu ô (i, j) là sói (-1) hoặc cả hai ô nguồn đều không thể đến được (-1), thì
dp[i][j] = -1. - Kết quả là giá trị tại
dp[M][N]. Nếu giá trị này là -1, in ra -1.
Cách Quy hoạch động tối ưu không gian (Rolling Array)
#include <stdio.h>
#include <string.h>
#define MAX_N 1005
int main() {
int m, n;
scanf("%d %d", &m, &n);
// Chỉ cần lưu 2 hàng: hàng hiện tại và hàng trước đó
int dp[2][MAX_N];
int a[MAX_N];
memset(dp, -1, sizeof(dp));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
scanf("%d", &a[j]);
}
for (int j = 1; j <= n; j++) {
if (a[j] == -1) {
dp[i % 2][j] = -1;
continue;
}
if (i == 1 && j == 1) {
dp[1][1] = a[1];
continue;
}
int max_prev = -1;
// Từ trên xuống (hàng trước đó)
if (i > 1 && dp[(i - 1) % 2][j] != -1) {
if (dp[(i - 1) % 2][j] > max_prev) max_prev = dp[(i - 1) % 2][j];
}
// Từ trái sang (cùng hàng)
if (j > 1 && dp[i % 2][j - 1] != -1) {
if (dp[i % 2][j - 1] > max_prev) max_prev = dp[i % 2][j - 1];
}
if (max_prev != -1) dp[i % 2][j] = max_prev + a[j];
else dp[i % 2][j] = -1;
}
}
printf("%d", dp[m % 2][n]);
return 0;
}
- Time Complexity: O(M x N)
- Space Complexity: O(N)
Cải tiến từ quy hoạch động cơ bản.
- Ta nhận thấy để tính giá trị cho hàng
i, ta chỉ cần thông tin từ hàngi-1và các ô đã tính ở hàngi. - Sử dụng biến
i % 2để lật qua lại giữa 2 hàng trong mảngdp. - Cần lưu giá trị của lưới vào mảng 1 chiều
ađể xử lý từng hàng. - Logic tính toán giống hệt phương pháp cơ bản.
Cách DFS + Memoization (Đệ quy nhớ)
#include <stdio.h>
#define MAX 1005
int m, n;
int a[MAX][MAX];
int memo[MAX][MAX]; // Lưu kết quả đã tính
int max(int a, int b) { return a > b ? a : b; }
int solve(int i, int j) {
// Kiểm tra biên
if (i > m || j > n) return -1;
// Ô sói
if (a[i][j] == -1) return -1;
// Ô đích
if (i == m && j == n) return a[i][j];
// Đã tính trước đó
if (memo[i][j] != -2) return memo[i][j];
// Thử xuống dưới và sang phải
int down = solve(i + 1, j);
int right = solve(i, j + 1);
int res = -1;
if (down != -1) res = down;
if (right != -1) {
if (res == -1 || right > res) res = right;
}
if (res != -1) res += a[i][j];
return memo[i][j] = res;
}
int main() {
scanf("%d %d", &m, &n);
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
scanf("%d", &a[i][j]);
memo[i][j] = -2; // Đặt giá trị đánh dấu chưa tính
}
}
printf("%d", solve(1, 1));
return 0;
}
- Time Complexity: O(M x N)
- Space Complexity: O(M x N)
Đây là cách tiếp cận đệ quy.
- Hàm
solve(i, j)trả về tổng độ đẹp lớn nhất từ (i, j) đến (M, N). - Nếu đã tính toán
memo[i][j]thì trả về luôn. - Nếu chưa, đệ quy gọi
solve(i+1, j)vàsolve(i, j+1). - Chọn giá trị lớn nhất trong 2 hướng (nếu hợp lệ), cộng với giá trị ô hiện tại, lưu vào
memovà trả về. - Khá dễ viết nhưng có thể gây tràn ngăn xếp (stack overflow) nếu lưới quá lớn (M, N <= 1000 thì độ sâu đệ quy tối đa 2000, thường an toàn, nhưng vẫn rủi ro hơn DP vòng lặp).
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(M x N) | O(M x N) | Quy hoạch động (Dynamic Programming) |
| 2 | O(M x N) | O(N) | Quy hoạch động tối ưu không gian (Rolling Array) |
| 3 | O(M x N) | O(M x N) | DFS + Memoization (Đệ quy nhớ) |
Bài học kinh nghiệm
- Bài toán có cấu trúc tối ưu con (optimal substructure): Đường đi tốt nhất đến (i, j) phụ thuộc vào đường đi tốt nhất đến (i-1, j) hoặc (i, j-1).
- Phương pháp Quy hoạch động (DP) là tối ưu nhất về thời gian cho bài toán này (O(MN)).
- Vấn đề có thể được coi là tìm đường đi dài nhất (longest path) trong Directed Acyclic Graph (DAG) do các ô chỉ đi xuống hoặc sang phải, không có chu trình.
Lỗi thường gặp
- Xử lý sai các trường hợp biên: Ô (1,1), hàng đầu tiên, cột đầu tiên. Ví dụ, không thể đi sang trái ở cột 1 hoặc đi lên trên ở hàng 1.
- Quên kiểm tra ô (1,1) có phải là sói hay không trước khi bắt đầu.
- Lỗi truy cập mảng: Các giải pháp C dùng mảng tĩnh, cần đảm bảo kích thước đủ (Ví dụ: 1005 cho M, N ≤ 1000).
- Giá trị âm: Đề bài cho phép a[i][j] là số âm (nhưng -1 dùng để đánh dấu sói, các số âm khác có thể coi là hoa xấu?). Thực tế trong các giải pháp,
a[i][j] == -1được xử lý riêng, các giá trị khác cộng bình thường. Tuy nhiên, một số bài toán tương tự có thể có bẫy nếu giá trị âm làm tổng giảm, nhưng ở đây ta vẫn chọn max. Logicmax(dp[i-1][j], dp[i][j-1])đảm bảo chọn hướng tốt nhất.
Bình luận