Các chiến lược như quy hoạch động dynamic programming, chia để trị divide and conquer, tham lam greedy, và quay lui backtracking không chỉ giúp tìm ra lời giải chính xác mà còn đảm bảo h
Trang 1TRƯỜNG ĐẠI HỌC GIAO THÔNG VẬN TẢI TP.HCM
VIỆN ĐÀO TẠO CHẤT LƯỢNG CAO KHOA CÔNG NGHỆ THÔNG TIN
BÀI BÁO CÁO PHÂN TÍCH THIẾT KẾ GIẢI THUẬT
Giảng Viên: Huỳnh Thanh Việt
Sinh viên: Nguyễn Tiến Vươn
MSSV: 0522050110044
TP.Hồ Chí Minh, ngày 23 tháng 11 năm 2024
Trang 2MỤC LỤC
MỞ ĐẦU 3
I BÀI TOÁN 4
1 Lịch thuê máy tối ưu 4
2 Bài toán tìm lộ trình tối ưu trong bảng A 8
II ƯU ĐIỂM VÀ NHƯỢC ĐIỂM 17
1 Ưu điểm 17
2 Nhược điểm 17
III HƯỚNG PHÁT TRIỂN 18
KẾT LUẬN 19
Trang 3MỞ ĐẦU
Môn Phân tích Thiết kế Giải thuật đóng vai trò quan trọng trong việc trang bị các kỹ năng và phương pháp giải quyết bài toán một cách tối ưu Các chiến lược như quy hoạch động (dynamic programming), chia để trị (divide and conquer), tham lam (greedy), và quay lui (backtracking) không chỉ giúp tìm ra lời giải chính xác mà còn đảm bảo hiệu quả về mặt thời gian và không gian
Hai bài toán được trình bày trong bài tiểu luận này là minh chứng cho sức mạnh của chiến lược quy hoạch động Bài toán thứ nhất tập trung vào việc tối ưu hóa doanh thu trong một lịch trình có ràng buộc thời gian, trong khi bài toán thứ hai là bài toán tìm đường tối ưu trong một bảng hai chiều
Trang 4I BÀI TOÁN
1 Lịch thuê máy tối ưu
Trung tâm tính toán hiệu năng cao nhận được đơn đặt hàng của n khách hàng Khách hàng i muốn sử dụng máy trong khoảng thời gian từ ai đến bi Hãy thiết kế chương trình
để bố trí lịch thuê máy để tổng số tiền thu được là lớn nhất mà thời gian sử dụng máy của 2 khách hàng bất kì được phục vụ đều không giao nhau (cả trung tâm chỉ có một máy cho thuê)
Sử dụng chiến lược quy hoạch động
a.1) Ý tưởng thực hiện
1 Nhập liệu:
Người dùng sẽ nhập số lượng khách hàng và thông tin về thời gian bắt đầu (ai), thời gian kết thúc (bi), và lợi nhuận (vi) cho mỗi khách hàng
2 Sắp Xếp Dữ Liệu:
Các cuộc hẹn (hoặc khoảng thời gian thuê máy) sẽ được sắp xếp theo thời gian kết thúc (bi) Điều này giúp chúng ta dễ dàng xác định các khoảng thời gian không giao nhau
3 Xây Dựng Bảng Quy Hoạch Động:
Sử dụng một mảng f để lưu trữ lợi nhuận tối đa có thể thu được cho mỗi khách hàng
Với mỗi khách hàng, chúng ta sẽ xem xét hai lựa chọn: Lựa chọn 1: Không phục vụ khách hàng này (lợi nhuận là lợi nhuận tối đa của khách hàng trước đó)
Lựa chọn 2: Phục vụ khách hàng này, cộng với lợi nhuận từ khách hàng không giao nhau trước đó
4 Tìm Lợi Nhuận Tối Đa:
Cuối cùng, giá trị lớn nhất trong mảng f sẽ là lợi nhuận tối đa
có thể đạt được
Trang 5b.1) Giải thích thuật toán
#include <iostream>: Thư viện cần thiết để sử dụng các chức năng nhập xuất như cin và cout
#include <vector>: Cho phép sử dụng cấu trúc dữ liệu vector
để lưu trữ các giá trị động
#include <algorithm>: Cung cấp các hàm hỗ trợ như sort và max_element
int n;: Khai báo biến n để lưu số lượng khách hàng
vector<int> s(n + 1), e(n + 1), v(n + 1);: Khai báo ba vector: s: Lưu thời gian bắt đầu cho từng khách hàng
e: Lưu thời gian kết thúc cho từng khách hàng
v: Lưu lợi nhuận tương ứng với từng khách hàng
s[0] = 0; e[0] = 0; v[0] = 0;: Khởi tạo phần tử đầu tiên của mỗi vector với giá trị 0 để dễ dàng xử lý trong thuật toán
Trang 6Vòng lặp for từ 1 đến n yêu cầu người dùng nhập thời gian bắt đầu, thời gian kết thúc và lợi nhuận cho từng khách hàng Sắp Xếp:
Hai vòng lặp lồng nhau để sắp xếp các lần thuê theo thời gian kết thúc tăng dần Nếu thời gian kết thúc của lần thuê i nhỏ hơn thời gian kết thúc của lần thuê j, nó sẽ hoán đổi thông tin giữa chúng
Sử dụng swap để hoán đổi giá trị giữa các vector
Khởi tạo giá trị:
f[i] là một mảng dùng để lưu lợi nhuận tối đa đạt được khi chọn đến lần thuê thứ i
v[i] là mảng lợi nhuận của từng lần thuê
s[i] là thời gian bắt đầu của lần thuê thứ i
e[i] là thời gian kết thúc của lần thuê thứ i
Duyệt qua các cuộc họp:
for (int i = 1; i <= n; i++): Vòng lặp này duyệt qua tất cả các lần thuê từ 1 đến n
f[i] = v[i];: Khởi tạo lợi nhuận ban đầu cho lần thuê thứ i
là giá trị lợi nhuận của nó (v[i]), giả định rằng nếu chỉ chọn một mình lần thuê i, thì lợi nhuận đạt được là v[i]
So sánh với các lần thuê trước đó:
for (int j = 1; j < i; j++): Vòng lặp lồng trong để duyệt qua tất cả các lần thuê j trước i (từ 1 đến i-1)
Trang 7 if (s[i] >= e[j] && f[i] < f[j] + v[i]): Kiểm tra xem lần thuê i có thể chọn được sau lần thuê j (nghĩa là s[i] >= e[j]) và liệu lợi nhuận thu được khi chọn lần thuê i sau j có lớn hơn lợi nhuận hiện tại của f[i] hay không
o Nếu điều kiện này thỏa mãn, thì cập nhật lợi nhuận tối đa cho f[i] bằng cách thêm lợi nhuận v[i] vào lợi nhuận tối
đa của f[j]
Vd minh họa:
Áp dụng đoạn mã:
Khởi tạo:
o f[1] = v[1] = 5
o f[2] = v[2] = 6
o f[3] = v[3] = 5
o f[4] = v[4] = 8
Duyệt từng lần thuê:
o Lần thuê2:
Không thể chọn cuộc họp 2 sau cuộc họp 1 vì s[2] < e[1]
o Lần thuê 3:
So với lần thuê 1: s[3] >= e[1] và f[3] < f[1] + v[3], nên f[3] = f[1] + v[3] = 5 + 5 = 10, và t[3] = 1
So với lần thuê 2: không thỏa mãn điều kiện
o Lần thuê 4:
So với lần thuê 1: s[4] >= e[1] và f[4] < f[1] + v[4], nên f[4] = f[1] + v[4] = 5 + 8 = 13, và t[4] = 1
Trang 8 So với lần thuê 3: s[4] >= e[3] và f[4] < f[3] + v[4], nên f[4] = f[3] + v[4] = 10 + 8 = 18, và t[4] = 3 Kết quả, lợi nhuận tối đa là 18, và các lần thuê được chọn
là lần thuê 1, 3 và 4
Tìm lợi nhuận và in ra kết quả
2 Bài toán tìm lộ trình tối ưu trong bảng A.
Cho bảng A gồm M N ô Từ ô (i,j) có thể di chuyển sang∗
3 ô (i+1, j), (i+1, j−1) và (i+1, j+1) Hãy thiết kế giải thuật để xác định một lộ trình đi từ hàng 1 đến hàng M sao cho tổng các ô đi qua là lớn nhất
a.2) Ý tưởng thực hiện
1 Khởi tạo mảng DP:
Gọi dp[i][j] là tổng giá trị lớn nhất có thể đạt được khi đến ô (i, j) từ hàng đầu tiên
2 Công thức quy hoạch động:
dp[i][j]=A[i][j]+max(dp[i−1][j],dp[i−1][j−1],dp[i−1][j+1])
3 Tính toán cho các hàng tiếp theo:
Với mỗi ô (i, j), cập nhật dp[i][j] bằng tổng của A[i][j] và giá trị lớn nhất trong ba ô phía trên: Ô trên (i-1, j), Ô trên bên trái (i-1, j-1) ,Ô trên bên phải (i-1, j+1)
4 Truy vết để tìm lộ trình
Trang 95 Kết quả:
Tìm giá trị lớn nhất trong hàng cuối của dp, đây là tổng lớn nhất có thể đạt được từ hàng đầu đến hàng cuối cùng
b.2) Giải thích thuật toán
Thư viện:
· #include <iostream>: Thư viện cho phép sử dụng các hàm nhập/xuất như cin và cout
· #include <vector>: Thư viện cho phép sử dụng cấu trúc
dữ liệu vector
· #include <algorithm>: Thư viện cung cấp các hàm thuật toán như reverse
Hằng số:
const int MAX = 100;: Đặt hằng số MAX là 100, đại diện cho kích thước tối đa của bảng A (100x100) Kích thước này
có thể thay đổi tùy theo yêu cầu bài toán
Hàm maxPathSum:
int maxPathSum( ): Định nghĩa hàm maxPathSum, nhận các tham số:
Trang 10A[MAX][MAX]: Bảng giá trị đầu vào A có kích thước tối
đa MAX x MAX
M: Số hàng của bảng A
N: Số cột của bảng A
path: Tham chiếu đến một vector lưu các cặp tọa độ (i, j)
để theo dõi lộ trình tối ưu
int dp[MAX][MAX], trace[MAX][MAX];: Khai báo mảng
dp lưu tổng lớn nhất đạt được khi đến mỗi ô (i, j) và mảng trace để lưu lại cột trước đó từ đó ta đi đến ô (i, j) trong lộ trình tối ưu
Khởi tạo hàng đầu tiên của dp:
· Vòng lặp for (int j = 0; j < N; ++j): Duyệt qua từng ô trong hàng đầu tiên (hàng 0)
· dp[0][j] = A[0][j];: Khởi tạo dp[0][j] bằng giá trị tại ô A[0] [j] vì hàng đầu tiên không có ô phía trên, nên tổng lớn nhất đến ô này chính là giá trị của nó
· trace[0][j] = -1;: Gán -1 cho trace[0][j] vì hàng đầu tiên không có ô trước đó
Tính toán dp cho các hàng tiếp theo:
Trang 11· for (int i = 1; i < M; ++i): Vòng lặp chạy qua từng hàng từ hàng 1 đến hàng M-1
· for (int j = 0; j < N; ++j): Vòng lặp chạy qua từng cột trong mỗi hàng
· int maxPrev = dp[i - 1][j];: Giả sử ô tối ưu phía trên là ô trực tiếp trên (i-1, j)
· trace[i][j] = j;: Ghi nhận vị trí cột của ô phía trên (i-1, j) làm ô trước đó
Kiểm tra các ô phía trên bên trái và bên phải:
· if (j > 0 && dp[i - 1][j - 1] > maxPrev): Nếu tồn tại ô phía trên bên trái (i-1, j-1) và nó có tổng lớn hơn maxPrev, thì cập nhật:
maxPrev = dp[i - 1][j - 1];: Cập nhật maxPrev bằng tổng lớn nhất từ ô phía trên bên trái
trace[i][j] = j - 1;: Cập nhật trace[i][j] để chỉ đến ô phía trên bên trái (i-1, j-1)
· if (j < N - 1 && dp[i - 1][j + 1] > maxPrev): Nếu tồn tại
ô phía trên bên phải (i-1, j+1) và nó có tổng lớn hơn maxPrev, thì cập nhật:
maxPrev = dp[i - 1][j + 1];: Cập nhật maxPrev bằng tổng lớn nhất từ ô phía trên bên phải
trace[i][j] = j + 1;: Cập nhật trace[i][j] để chỉ đến ô phía trên bên phải (i-1, j+1)
Cập nhật tổng lớn nhất cho dp[i][j]:
Trang 12dp[i][j] = A[i][j] + maxPrev;: Tổng lớn nhất để đến ô (i, j)
là giá trị của ô đó A[i][j] cộng với tổng lớn nhất từ ô phía trên tối ưu (maxPrev)
Tìm tổng đường đi lớn nhất ở hàng cuối cùng:
· int result = dp[M - 1][0], lastCol = 0;: Khởi tạo result là tổng lớn nhất của cột đầu tiên ở hàng cuối và lastCol là chỉ số cột này
· for (int j = 1; j < N; ++j): Duyệt qua tất cả các ô trong hàng cuối để tìm tổng lớn nhất
· if (dp[M - 1][j] > result): Nếu tìm được ô có tổng lớn hơn result, thì cập nhật result và lastCol với giá trị mới
Truy ngược lại lộ trình:
· for (int row = M - 1, col = lastCol; row >= 0; row): Bắt đầu từ hàng cuối cùng M-1 và cột có tổng lớn nhất lastCol, đi ngược lên đầu bảng
Trang 13· path.push_back({ row, col });: Thêm vị trí (row, col) vào path
· col = trace[row][col];: Cập nhật col để chuyển sang cột của ô trước đó từ trace
· reverse(path.begin(), path.end());: Đảo ngược path để lộ trình hiển thị theo thứ tự từ trên xuống dưới
Trả về kết quả:
return result;: Trả về tổng lớn nhất của lộ trình
Hàm main:
· int main() { }: Hàm chính của chương trình
· int M, N, A[MAX][MAX];: Khai báo M và N là số hàng và cột của bảng A
· cin >> M >> N;: Nhập vào M và N từ người dùng
Nhập giá trị cho bảng A:
· cout << "Nhap cac phan tu bang A:\n";: In ra dòng nhắc để người dùng nhập vào các phần tử của bảng
Trang 14· for (int i = 0; i < M; ++i) và for (int j = 0; j < N; ++j): Hai vòng lặp lồng nhau để duyệt qua từng ô (i, j) trong bảng A, với i là chỉ số hàng (chạy từ 0 đến M-1) và j là chỉ số cột (chạy từ 0 đến N-1)
· cin >> A[i][j];: Tại mỗi ô (i, j), người dùng nhập vào giá trị A[i][j] của bảng A
Phần tính và hiển thị kết quả:
Sau khi nhập bảng A, chúng ta gọi hàm maxPathSum() để tính tổng lớn nhất và lộ trình Sau đó, in ra kết quả
· vector<pair<int, int>> path;: Khai báo một vector path để lưu lộ trình tối ưu Mỗi phần tử của path là một cặp (i, j), biểu thị vị trí của ô trên lộ trình
· int maxSum = maxPathSum(A, M, N, path);: Gọi hàm maxPathSum với các tham số là bảng A, số hàng M, số cột N,
và path
Hàm này sẽ trả về maxSum, tức là tổng lớn nhất của lộ trình
từ hàng đầu đến hàng cuối
Tiếp theo là phần hiển thị kết quả:
· cout << "Tong lon nhat cua lo trinh la: " << maxSum << endl;: In ra tổng lớn nhất của lộ trình, tức là giá trị maxSum
· cout << "Cac buoc di chuyen:\n";: In ra một dòng thông báo rằng các bước di chuyển sẽ được liệt kê tiếp theo
Trang 15· for (auto& p : path): Dùng vòng lặp để duyệt qua từng ô (i, j) trong lộ trình path
· cout << "(" << p.first << ", " << p.second << ") ";: In tọa độ của từng ô trên lộ trình, biểu thị theo dạng (i, j)
· cout << endl;: Kết thúc dòng in ra
c.2) Ví dụ:
Giả sử bảng A là:
1 2 3
4 5 6
7 8 9
Bước 1: Khởi tạo mảng dp bằng hàng đầu của A:
1 2 3
0 0 0
0 0 0
Bước 2: Tính toán hàng thứ 2 của dp:
1 2 3
5 7 9
0 0 0
Ví dụ: dp[1][1] = A[1][1] + max(dp[0][0], dp[0][1]) = 5 + max(1, 2) = 7 Bước 3: Tính toán hàng thứ 3 của dp:
1 2 3
5 7 9
12 15 18
Kết quả: Tổng lớn nhất là 18, đạt được bằng cách đi theo lộ trình 1 -> 2 -> 9
Trang 16II ƯU ĐIỂM VÀ NHƯỢC ĐIỂM
1 Ưu điểm
Quy hoạch động là một kỹ thuật hiệu quả để giải quyết các bài toán có tính chất đệ quy và con
Các bước cơ bản của quy hoạch động:
Xác định cấu trúc của lời giải tối ưu: Tìm cách biểu diễn lời giải tối ưu thông qua các lời giải con
Xây dựng công thức truy hồi: Lập công thức liên hệ giữa lời giải của bài toán con với lời giải của bài toán lớn hơn
Tính toán từ dưới lên: Tính toán các giá trị con trước rồi mới tính toán các giá trị lớn hơn
Ứng dụng: Quy hoạch động có thể được áp dụng vào rất nhiều lĩnh vực khác nhau, từ khoa học máy tính đến kinh
tế, tài chính
Quy hoạch động giúp giảm thiểu thời gian tính toán và tránh tính toán trùng lặp
2 Nhược điểm
Không phải bài toán nào cũng có thể giải quyết bằng quy hoạch động Chỉ những bài toán có tính chất đệ quy và con mới phù hợp
Độ phức tạp của thuật toán: Trong một số trường hợp, việc thiết kế thuật toán quy hoạch động có thể khá phức tạp
Trang 17III HƯỚNG PHÁT TRIỂN
Tìm hiểu sâu hơn về các biến thể của quy hoạch động:
Ví dụ như quy hoạch động top-down, bottom-up,
memoization
Áp dụng quy hoạch động vào các bài toán thực tế: Tìm
kiếm các bài toán trong lĩnh vực bạn quan tâm và thử giải quyết bằng quy hoạch động
Kết hợp quy hoạch động với các kỹ thuật khác: Ví dụ
như thuật toán tham lam, chia để trị
Trang 18KẾT LUẬN
Môn Phân tích Thiết kế Giải thuật là một trong những môn
học cốt lõi trong ngành khoa học máy tính, cung cấp cho sinh viên những kiến thức và kỹ năng cần thiết để phân tích, thiết
kế và đánh giá hiệu quả của các thuật toán Qua môn học này, chúng ta đã được trang bị những công cụ và phương pháp để giải quyết các bài toán thuật toán một cách hiệu quả
Hai bài toán về lịch thuê máy tối ưu và tìm đường đi tối ưu trong bảng đã minh họa rất rõ ràng cách áp dụng quy hoạch
động để giải quyết các bài toán tối ưu hóa Quy hoạch động là một kỹ thuật mạnh mẽ, cho phép ta chia nhỏ bài toán lớn thành các bài toán con nhỏ hơn, từ đó tìm ra lời giải tối ưu cho toàn bộ bài toán