Hướng dẫn giải của Máy rút tiền thông minh
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ả: , , ,
Hiểu bài toán
Bài toán yêu cầu tìm cách trả số tiền đúng bằng M sử dụng các tờ tiền có mệnh giá cho trước (a1, a2, ..., a_n) sao cho số lượng tờ tiền là ít nhất. Mỗi mệnh giá có số lượng đủ nhiều. Nếu không thể tạo ra số tiền M, output là -1. Đây là bài toán quy hoạch động cơ bản về bài toán cái túi (Knapsack) dạng bài toán tìm tổng bằng M với số lượng phần tử ít nhất.
Các cách tiếp cận
Cách Quy hoạch động cơ bản (Bài toán cái túi)
#include <stdio.h>
#include <limits.h>
int min(int a, int b) {
return a < b ? a : b;
}
int main() {
int n, M;
scanf("%d %d", &n, &M);
int a[n];
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
// dp[i] là số lượng tờ tiền ít nhất để tạo ra tổng i
int dp[M + 1];
// Khởi tạo: dp[0] = 0 (cần 0 tờ để tạo tổng 0), các giá trị khác là vô cùng
dp[0] = 0;
for (int i = 1; i <= M; i++) {
dp[i] = INT_MAX; // Hoặc một giá trị lớn hơn M (ví dụ M+1)
}
// Quy hoạch động
for (int i = 0; i < n; i++) {
for (int j = a[i]; j <= M; j++) {
if (dp[j - a[i]] != INT_MAX) {
dp[j] = min(dp[j], dp[j - a[i]] + 1);
}
}
}
if (dp[M] == INT_MAX) {
printf("-1");
} else {
printf("%d", dp[M]);
}
return 0;
}
- Time Complexity: O(n * M)
- Space Complexity: O(M)
Sử dụng mảng 1 chiều dp, trong đó dp[j] lưu số lượng tờ tiền ít nhất để tạo ra tổng j. Với mỗi tờ tiền có mệnh giá a[i], ta duyệt qua các tổng j từ a[i] đến M. Công thức cập nhật: dp[j] = min(dp[j], dp[j - a[i]] + 1). Nếu dp[M] vẫn là giá trị vô cùng sau khi tính toán, nghĩa là không thể tạo ra tổng M.
Cách Quy hoạch động (Cách tiếp cận khác)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int min(int a, int b) {
return (a < b) ? a : b;
}
int main() {
int n, m;
scanf("%d%d", &n, &m);
int a[n];
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
int dp[m + 1];
dp[0] = 0;
// Khởi tạo các giá trị ban đầu cho dp
for (int i = 1; i <= m; i++) {
dp[i] = m + 1; // Gán giá trị lớn hơn tối đa có thể (số tờ tối đa là m nếu dùng toàn tờ 1)
}
// Duyệt qua từng tổng từ 1 đến m
for (int i = 1; i <= m; i++) {
for (int j = 0; j < n; j++) {
if (i >= a[j]) {
dp[i] = min(dp[i], dp[i - a[j]] + 1);
}
}
}
if (dp[m] == m + 1) printf("-1");
else printf("%d\n", dp[m]);
return 0;
}
- Time Complexity: O(n * M)
- Space Complexity: O(M)
Cách tiếp cận này tương tự như cách trên nhưng thứ tự vòng lặp có thể thay đổi. Ở đây, ta duyệt qua từng tổng i từ 1 đến M, và với mỗi i, ta thử qua tất cả các mệnh giá a[j] để xem có thể cập nhật dp[i] hay không. Logic tính toán vẫn là tìm cách tạo tổng i bằng cách lấy tổng i - a[j] cộng thêm 1 tờ a[j].
Cách Quy hoạch động优化 (Sử dụng INF)
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <ctype.h>
#include <stdbool.h>
#define INF 100000000
int main(){
int n,M;
scanf("%d %d",&n,&M);
int a[n];
int dp[M+1];
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
}
for(int i=1;i<=M;i++){
dp[i]=INF;
}
dp[0]=0;
for(int i=0;i<n;i++){
for(int j=a[i];j<=M;j++){
dp[j]=(dp[j]<dp[j-a[i]]+1)?dp[j]:dp[j-a[i]]+1;
}
}
if(dp[M]>=INF){
printf("-1");
}
else{
printf("%d",dp[M]);
}
}
- Time Complexity: O(n * M)
- Space Complexity: O(M)
Giống hoàn toàn về mặt logic với 'Quy hoạch động cơ bản'. Sử dụng #define INF 100000000 để đánh dấu các giá trị chưa thể tạo được. Đây là cách viết ngắn gọn và phổ biến trong lập trình thi đấu.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(n * M) | O(M) | Quy hoạch động cơ bản (Bài toán cái túi) |
| 2 | O(n * M) | O(M) | Quy hoạch động (Cách tiếp cận khác) |
| 3 | O(n * M) | O(M) | Quy hoạch động优化 (Sử dụng INF) |
Bài học kinh nghiệm
- Bài toán có thể chia thành các bài toán con: tìm số tờ tiền ít nhất để tạo tổng i từ các tổng nhỏ hơn.
- Mảng quy hoạch động
dplà chìa khóa:dp[i]lưu kết quả bài toán con cho tổng i. - Vì M (10^5) và n (20) đều nhỏ, thuật toán O(n*M) là hoàn toàn khả thi (khoảng 2 triệu thao tác).
Lỗi thường gặp
- Quên khởi tạo
dp[0] = 0và các giá trị ban đầu củadp(vô cùng hoặc một giá trị đủ lớn). - Lỗi truy cập ngoài mảng khi
j < a[i](cần kiểm tra điều kiện hoặc bắt đầu vòng lặp từa[i]). - Xử lý sai trường hợp không có lời giải: cần kiểm tra xem
dp[M]có bằng giá trị vô cùng hay không.
Bình luận