Hướng dẫn giải của Máy rút tiền thông minh


Chỉ dùng lời giải này khi không có ý tưởng, và đừng copy-paste code từ lời giải này. Hãy tôn trọng người ra đề và người viết lời giải.
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ập

Tác giả: Hiếu Nguyễn, thanhdoan, hieuochimchim, nquang2909

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 dp là 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] = 0 và các giá trị ban đầu của dp (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

Please read the guidelines before commenting.


Không có bình luận tại thời điểm này.