Đề cương chủ yếu về 4 thuật toán: Tham Lam, Chia Để Trị, Quay Lui và Quy Hoạch Động. Ngoài lý thuyết đơn giản ra, đề cương có 31 bài tập, không có đề bài, chỉ bao gồm ý tưởng giải, ví dụ đơn giản và code lập trình bằng C++ đầy đủ.
Sắp xếp môn học
Đoạn code trên giải quyết bài toán lựa chọn các khóa học sao cho số lượng khóa học được chọn là lớn nhất, mà không có hai khóa học nào trùng thời gian Ý tưởng chính của phương pháp Greedy trong đoạn code này là sắp xếp các khóa học theo thời gian kết thúc tăng dần, sau đó lựa chọn lần lượt các khóa học không trùng thời gian với nhau
Giả sử có 5 khóa học như sau:
Khóa học 1 (id=1): bắt đầu lúc 6, kết thúc lúc 7
Khóa học 2 (id=2): bắt đầu lúc 7, kết thúc lúc 9
Khóa học 3 (id=3): bắt đầu lúc 8, kết thúc lúc 14
Khóa học 4 (id=4): bắt đầu lúc 10, kết thúc lúc 20
Khóa học 5 (id=5): bắt đầu lúc 9, kết thúc lúc 12
Sử dụng phương pháp Greedy, ta sẽ sắp xếp các khóa học theo thời gian kết thúc tăng dần
Sau khi sắp xếp, ta lựa chọn lần lượt các khóa học không trùng thời gian với nhau Trong ví dụ này, ta sẽ lựa chọn các khóa học sau: 1, 2, 5, 4 Do đó, số lượng khóa học được chọn là 4 và đây là lịch trình tối ưu.
Đóng thùng
Ý tưởng chính của thuật toán là sắp xếp các đồ vật theo thứ tự giảm dần của kích thước, sau đó lần lượt đặt từng đồ vật vào thùng có thể chứa được nó Nếu không thể đặt vào thùng hiện tại, thì tìm thùng trống khác có thể chứa được và đặt đồ vật vào đó
Giả sử có các đồ vật có kích thước là {3, 2, 5, 1, 4, 3} và kích thước của thùng là 6 Chương trình sẽ thực hiện như sau:
Lần lượt đặt các đồ vật vào thùng:
Thùng 1: {5}, còn lại kích thước 1
Thùng 2: {4, 1}, còn lại kích thước 1
Thùng 3: {3, 3}, còn lại kích thước 0
Thùng 4: {2}, còn lại kích thước 4
Rút tiền
Sắp xếp các mệnh giá tiền giảm dần: Trước tiên, danh sách các mệnh giá tiền được sắp xếp theo thứ tự giảm dần, từ mệnh giá lớn nhất đến nhỏ nhất Điều này giúp chương trình rút tiền bằng các tờ tiền có mệnh giá lớn trước, giảm thiểu số lượng tờ tiền cần rút
Rút tiền từ mệnh giá lớn nhất đến nhỏ nhất: Thuật toán lặp qua từng mệnh giá tiền từ lớn nhất đến nhỏ nhất Với mỗi mệnh giá, nó tính số tờ tiền cần rút bằng cách chia số tiền cần rút cho mệnh giá đó và lấy phần nguyên của kết quả Sau đó, cập nhật số tiền cần rút cho lần lặp tiếp theo
Giả sử danh sách các mệnh giá tiền có sẵn là {100,000, 50,000, 40,000, 20,000, 10,000}, và người dùng muốn rút 180,000 VND từ máy ATM
Bắt đầu với mệnh giá 100,000 VND:
Số tờ tiền cần rút = 180,000 / 100,000 = 1 (phần nguyên)
Cập nhật số tiền cần rút: 180,000 - 1 * 100,000 = 80,000 VND
Tiếp tục với mệnh giá 50,000 VND:
Số tờ tiền cần rút = 80,000 / 50,000 = 1 (phần nguyên)
Cập nhật số tiền cần rút: 80,000 - 1 * 50,000 = 30,000 VND ….
Trồng hoa
Đếm hậu
- Sử dụng phương pháp quay lui để thử tất cả các vị trí có thể đặt quân hậu trên bàn cờ
- Sử dụng một mảng 1 chiều để lưu vị trí của các quân hậu trên các hàng của bàn cờ
Bước 1: Tạo một hàm đệ quy XepHau(int i) để thử vị trí đặt quân hậu thứ i trên bàn cờ
Bước 2: Trong hàm XepHau(int i), thử tất cả các cột trên hàng i để xem vị trí nào phù hợp để đặt quân hậu
Bước 3: Kiểm tra xem vị trí đó có phù hợp để đặt quân hậu không bằng cách kiểm tra các quân hậu đã đặt trước đó Nếu vị trí đó hợp lệ, tiến hành đặt quân hậu và gọi đệ quy để thử vị trí tiếp theo
Bước 4: Nếu đã đặt quân hậu vào vị trí cuối cùng trên bàn cờ (i == n), thì in ra cách xếp hậu đó
Bước 5: Lặp lại các bước trên cho tất cả các quân hậu cho đến khi tìm được tất cả các cách xếp hậu đúng trên bàn cờ hoặc không còn cách nào thỏa mãn.
Dãy nhị phân
Thuật toán quay lui được sử dụng để sinh ra tất cả các tổ hợp nhị phân có độ dài n Ý tưởng chính của thuật toán là sử dụng một mảng để lưu trữ các giá trị 0 và 1, mỗi giá trị tại một vị trí của mảng biểu diễn cho một bit trong tổ hợp nhị phân Thuật toán sẽ thử tất cả các kết hợp có thể của các bit này, từ bit đầu tiên đến bit cuối cùng, và đệ quy tiến hành thử các giá trị có thể của bit tiếp theo cho đến khi đạt tới bit cuối cùng Khi đạt đến bit cuối cùng, thuật toán sẽ in ra kết quả và quay lui để thử các giá trị khác cho các bit trước đó
Dưới đây là các bước cụ thể của thuật toán:
- Khởi tạo một mảng có độ dài n để lưu trữ các giá trị của tổ hợp nhị phân
- Gọi hàm quay lui với các tham số là mảng đã khởi tạo, chỉ số của bit hiện tại (bắt đầu từ 1), và độ dài của tổ hợp nhị phân
- Lặp qua các giá trị có thể của bit hiện tại (0 hoặc 1)
- Gán giá trị cho bit hiện tại
- Nếu đã đạt tới bit cuối cùng, in ra tổ hợp nhị phân và kết thúc
- Nếu chưa đạt tới bit cuối cùng, gọi đệ quy để thử các giá trị cho bit tiếp theo
- Khi đã thử hết tất cả các giá trị cho bit hiện tại, quay lui để thử các giá trị khác cho bit trước đó.
Hoán vị
- Sử dụng một mảng x[] để lưu trữ hoán vị hiện tại và một mảng b[] để đánh dấu các số đã được chọn
- Sử dụng đệ quy để thử tất cả các cách chọn số cho vị trí hiện tại trong hoán vị
- Khi đã điền đủ n số vào x[], in ra hoán vị đó
Các bước giải: a Khởi tạo mảng x[] và b[] với giá trị ban đầu b Tạo hàm output() để in ra một hoán vị c Tạo hàm đệ quy hoanvi() để sinh ra tất cả các hoán vị:
- Với mỗi vị trí i từ 1 đến n, thử tất cả các số chưa được chọn để điền vào vị trí i
- Đánh dấu số đã chọn và gọi đệ quy để điền số tiếp theo
- Khi đã điền đủ n số, in ra hoán vị đó
- Hủy đánh dấu số đã chọn để thử các số khác d Gọi hàm hoanvi() từ main() để bắt đầu sinh và in ra tất cả các hoán vị của các số từ 1 đến n.
Mã đi tuần theo chu trình đóng
- Sử dụng một ma trận A[][] để lưu vị trí các bước di chuyển trên bàn cờ
- Sử dụng hai mảng X[] và Y[] để định nghĩa các hướng di chuyển từ mỗi ô
- Sử dụng biến dem để đếm số bước đã đi
- Sử dụng biến x_first và y_first để lưu vị trí ban đầu
- Sử dụng hàm xuat() để in ra bàn cờ sau mỗi bước di chuyển
- Sử dụng hàm diChuyen() để thực hiện việc di chuyển từ vị trí hiện tại đến các vị trí khác trên bàn cờ, đồng thời kiểm tra xem mã đã đi hết các ô chưa và đã trở về vị trí ban đầu chưa.
Sudoku
- Kiểm tra tính hợp lệ của giá trị trong ô: kiểm tra xem có thể đặt giá trị k vào vị trí (x, y) trên bảng Sudoku hay không Kiểm tra xem giá trị k đã tồn tại trong hàng, cột hoặc ô 3x3 chứa vị trí (x, y) chưa Nếu không hợp lệ, trả về 0; ngược lại trả về 1
- Giải bài toán Sudoku bằng đệ quy: Sử dụng phương pháp đệ quy để thử tất cả các giá trị từ 1 đến 9 cho ô hiện tại và tiếp tục đệ quy để thử giá trị tiếp theo cho ô kế tiếp Nếu không tìm thấy giải pháp, quay lui và thử các giá trị khác cho ô hiện tại
- Tìm kiếm giải pháp: kết thúc cột hiện tại, kiểm tra xem đã đến cuối bảng chưa Nếu đã đến cuối bảng, in ra giải pháp và kết thúc chương trình Nếu chưa, sẽ tiếp tục đệ quy với hàng tiếp theo Nếu ô hiện tại đã có giá trị, nó sẽ tiếp tục đệ quy với ô kế tiếp Nếu ô hiện tại chưa có giá trị, nó sẽ thử tất cả các giá trị hợp lệ cho ô đó.
Tổ hợp chập k của n
Tính tổ hợp
Ý tưởng chính là sử dụng một bảng hoặc một mảng để lưu trữ kết quả của các bài toán con nhỏ
Ta bắt đầu với các trường hợp cơ bản: C(n, 0) = C(n, n) = 1
Sau đó, ta sử dụng công thức tổ hợp: C(n, k) = C(n-1, k-1) + C(n-1, k) để tính toán các giá trị còn lại
Bằng cách tính toán từ trên xuống dưới và từ trái sang phải, ta có thể xây dựng bảng hoặc mảng chứa kết quả của tất cả các bài toán con nhỏ
Kết quả cuối cùng sẽ nằm ở ô góc phải dưới cùng của bảng hoặc mảng.
Dãy Fibonacci
Khởi tạo mảng F và giá trị ban đầu của dãy Fibonacci: Mảng F được khai báo với kích thước n và được sử dụng để lưu trữ các số trong dãy Fibonacci Ban đầu, hai phần tử đầu tiên của dãy Fibonacci là 0 và 1 được gán trực tiếp vào F[0] và F[1]
Duyệt qua từng phần tử của dãy Fibonacci từ 2 đến n: Sử dụng một vòng lặp, với mỗi giá trị i từ 2 đến n, tính giá trị của F[i] bằng cách cộng hai giá trị trước đó của dãy Fibonacci, tức là F[i-1] và F[i-2]
In ra dãy Fibonacci: Cuối cùng, dãy Fibonacci được in ra màn hình từ F[0] đến F[n].
Tổng tập con
Sử dụng bảng hoặc mảng: Bước đầu tiên là tạo một bảng hoặc mảng 1 chiều để lưu trữ kết quả của các bài toán con nhỏ Mỗi phần tử trong mảng này thường là một giá trị boolean, chỉ ra xem có thể tạo thành tổng mục tiêu từ một tập con của dãy ban đầu hay không
Trường hợp cơ bản: Khởi tạo các giá trị cơ bản của bảng hoặc mảng Ví dụ, nếu mục tiêu là 0, thì tất cả các giá trị trong mảng này đều là True vì có thể tạo ra tổng 0 từ bất kỳ tập con nào (bằng cách không chọn phần tử nào)
Tính toán các giá trị còn lại: Sử dụng phương pháp quy hoạch động, tính toán các giá trị còn lại của bảng hoặc mảng dựa trên giá trị của các bài toán con nhỏ hơn Mỗi ô của bảng hoặc mảng thường được tính toán dựa trên hai trường hợp: (a) nếu không bao gồm phần tử hiện tại, (b) nếu bao gồm phần tử hiện tại
Trả về kết quả: Kết quả cuối cùng thường được lấy từ ô cuối cùng của bảng hoặc mảng Nếu giá trị tại ô này là True, có nghĩa là có thể tạo thành tổng mục tiêu từ một tập con của dãy ban đầu.
Cái ba lô
Xây dựng bảng: Tạo một bảng 2 chiều có kích thước n x W, trong đó n là số lượng đồ vật và W là trọng lượng tối đa của cái ba lô
Khởi tạo giá trị ban đầu: Đặt tất cả các giá trị trong hàng đầu tiên và cột đầu tiên của bảng là 0
Tính toán giá trị tối ưu: Sử dụng công thức tối ưu L[i][t] = max(L[i - 1][t], L[i][t-g[i]] + v[i]), trong đó v [i] là giá trị của đồ vật thứ i, g[i] là trọng lượng của đồ vật thứ i, và L[i][t] là giá trị tối ưu có thể đạt được với i đồ vật và trọng lượng t
Trả về giá trị tối ưu: Giá trị tối ưu cuối cùng sẽ nằm ở ô góc dưới cùng bên phải của bảng
Dãy con tăng dài nhất
Khởi tạo mảng L để lưu độ dài của dãy con tăng dài nhất kết thúc tại mỗi vị trí của dãy
Bổ sung phần tử ảo INT_MAX vào cuối dãy A
Duyệt qua từng phần tử của dãy, cập nhật giá trị của L[i] bằng cách so sánh với các phần tử trước đó
Trả về độ dài của dãy con tăng dài nhất và in ra dãy con đó
Đổi tiền
Khởi tạo mảng P và L: Mảng P được khởi tạo với giá trị INT_MAX, đại diện cho số lượng đồng tiền ít nhất cần để đổi đến số tiền tương ứng Mảng L cũng được khởi tạo tương tự nhưng có thêm phần tử 0 ở đầu
Duyệt qua từng loại đồng tiền và mỗi mức số tiền từ 1 đến M: Tại mỗi bước lặp, ta cập nhật mảng L dựa trên mảng P, sử dụng các loại đồng tiền hiện có
Cập nhật mảng L: Tại mỗi mức số tiền t, ta cập nhật L[t] bằng giá trị nhỏ nhất giữa P[t] (số lượng đồng tiền ít nhất cho số tiền t mà không sử dụng đồng tiền hiện tại) và L[t-a[i]] + 1 (số lượng đồng tiền ít nhất cho số tiền t - a[i] cộng thêm một đồng tiền hiện tại)
Trả về kết quả: Trả về giá trị cuối cùng của L[M], tức là số lượng đồng tiền ít nhất cần thiết để đổi số tiền M
Độ dài xâu con dài nhất
Mảng P được khởi tạo với giá trị 0 cho tất cả các phần tử, đại diện cho xâu con chung dài nhất tính đến thời điểm trước đó
Mảng L được khởi tạo với giá trị 0 cho phần tử đầu tiên, đại diện cho xâu con chung dài nhất tính đến thời điểm hiện tại
Duyệt qua từng ký tự trong xâu X và Y:
Nếu X[i] bằng Y[j], tức là ta tìm thấy một ký tự mới trong xâu con chung Ta cập nhật giá trị của L[j] bằng giá trị của P[j-1]
Ngược lại, ta cập nhật giá trị của L[j] bằng giá trị lớn nhất giữa P[j] và L[j-1]
Sau khi duyệt qua tất cả các ký tự trong X và Y, ta cập nhật mảng P bằng mảng L và tiếp tục duyệt
Kết quả cuối cùng là giá trị của L[ny], tức là độ dài của xâu con chung dài nhất
Tìm kiếm nhị phân
1 Chia mảng thành các phần nhỏ: Đầu tiên, chúng ta chia mảng thành hai phần bằng cách chọn phần tử ở giữa mảng làm điểm chia Phần này được chia thành hai phần nhỏ hơn, phần bên trái và phần bên phải
2 Kiểm tra phần tử ở giữa:
So sánh phần tử ở giữa mảng với giá trị cần tìm Nếu phần tử này trùng khớp với giá trị cần tìm, chúng ta đã tìm thấy nó và trả về chỉ số của phần tử đó
3 Chọn phần mảng để tiếp tục tìm kiếm:
Nếu phần tử ở giữa mảng lớn hơn giá trị cần tìm, chúng ta chỉ cần tìm kiếm trong nửa mảng bên trái
Nếu phần tử ở giữa mảng nhỏ hơn giá trị cần tìm, chúng ta chỉ cần tìm kiếm trong nửa mảng bên phải
4 Lặp lại quá trình cho đến khi tìm thấy hoặc không thể tìm thấy:
Lặp lại quá trình trên cho đến khi tìm thấy phần tử cần tìm hoặc phần tử này không tồn tại trong mảng Nếu phần tử không tồn tại, chúng ta sẽ trả về một giá trị đặc biệt để biểu thị việc không tìm thấy
Nếu tìm thấy phần tử, trả về chỉ số của nó trong mảng
Nếu không tìm thấy, trả về một giá trị đặc biệt để biểu thị việc không tìm thấy.
Dãy con liên tục có tổng lớn nhất
1 Khởi tạo các biến: max_so_far: lưu trữ tổng lớn nhất của dãy con liên tục đã tìm thấy curr_max: lưu trữ tổng tạm thời của dãy con liên tục hiện tại begin và end: lưu trữ chỉ số của dãy con liên tục có tổng lớn nhất
Bắt đầu từ phần tử thứ 2 của mảng, vì chúng ta đã xử lý phần tử đầu tiên ở bước khởi tạo Duyệt qua từng phần tử của mảng:
So sánh a[i] với curr_max + a[i] Nếu a[i] lớn hơn tổng tạm thời hiện tại (curr_max + a[i]), tức là bắt đầu một dãy con mới từ a[i]
Cập nhật curr_max thành giá trị lớn hơn giữa a[i] và curr_max + a[i]
So sánh max_so_far với curr_max để cập nhật giá trị max_so_far nếu cần
3 Xác định dãy con liên tục có tổng lớn nhất:
Sau khi duyệt qua toàn bộ mảng, max_so_far sẽ chứa tổng lớn nhất của dãy con liên tục
Sử dụng begin và end để in ra dãy con liên tục có tổng lớn nhất
Trả về giá trị max_so_far, tức là tổng lớn nhất của dãy con liên tục.
Fibonacci
Sử dụng một hàm đệ quy để tính số Fibonacci của số nguyên dương n
Nếu n nhỏ hơn 2, trả về n vì các số Fibonacci đầu tiên là 0 và 1
Nếu không, tính số Fibonacci của n-1 và n-2 bằng cách đệ quy, sau đó cộng chúng lại với nhau.
Tìm phần tử lớn nhất
Tìm phần tử lớn nhất trong mảng A[] từ chỉ số low đến high với low = 0 và high = n-1
Nếu low và high trùng nhau (tức là chỉ có một phần tử trong mảng), trả về phần tử đó là phần tử lớn nhất
Ngược lại, chia mảng thành hai phần bằng cách tính chỉ số trung bình mid
Gọi đệ quy hàm FindMax trên hai phần tử mảng con bên trái và bên phải của mid, sau đó so sánh hai giá trị trả về
Trả về phần tử lớn nhất của hai phần tử mảng con đó.
Ước chung lớn nhất
Sử dụng thuật toán Euclid, trong đó gcd(a, b) là ước số chung lớn nhất của hai số a và b Trong khi b khác 0:
Gán giá trị của b cho a và giá trị của r cho b
Trả về giá trị của a, đó là ước số chung lớn nhất của a và b.
Tính lũy thừa
Sử dụng phương pháp chia để trị để tính lũy thừa của một số a với một số mũ n
Ngược lại, tính lũy thừa của a^(n/2) bằng cách đệ quy gọi hàm power(a, n/2)
Nếu n là số chẵn, trả về x * x, nếu n là số lẻ, trả về x * x * a.
Sắp xếp chèn
Bước 1: Bắt đầu từ phần tử thứ hai của mảng (index 1), lấy phần tử hiện tại và gán vào biến v
Bước 2: Lặp qua các phần tử trước đó (các phần tử đã được sắp xếp) để tìm vị trí đúng cho phần tử v
Bước 3: So sánh phần tử đang xét (v) với các phần tử đứng trước trong mảng Nếu phần tử trước đó lớn hơn v, di chuyển phần tử đó sang phải để tạo ra một vị trí trống cho v
Bước 4: Lặp lại bước 3 cho đến khi tìm được vị trí thích hợp cho v
Bước 5: Gán v vào vị trí thích hợp trong mảng
Bước 6: Lặp lại các bước trên cho các phần tử tiếp theo trong mảng Điều này tiếp tục cho đến khi tất cả các phần tử được sắp xếp.