



















Preview text:
MỤC LỤC
DANH MỤC BẢNG . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
DANH MỤC HÌNH ẢNH . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
CHƯƠNG 1: CƠ SỞ LÝ THUYẾT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.1. Thuật toán nhánh cận . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2. Nguyên lý . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3. Ưu điểm và nhược điểm của thuật toán nhánh cận . . . . . . . . . . . . . . . . . . . . . 6
1.3.1. Ưu điểm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3.2. Nhược điểm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.4. Các bước chính của thuật toán . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.5. Mô tả thuật toán . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.6. Áp dụng giải bài toán . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.6.1. Bài toán cái túi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.6.2. Bài toán người du lịch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
CHƯƠNG 2: PHÂN TÍCH - XÂY DỰNG THUẬT TOÁN TỐI ƯU HÓA LỰA
CHỌN ĐỒ VẬT TRONG BÀI TOÁN BALO (KNAPSACK) . . . . . . . . . . . . . . . . 14
2.1. Mô tả bài toán . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.1.1. Xây dựng bài toán thực thế . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.1.2. Phát biểu bài toán dưới dạng tổng quát . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.2. Xây dựng thuật toán . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.2.1. Ý tưởng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.2.2. Xây dựng giả mã . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.3. Ví dụ minh họa về tìm kiếm nhánh cận trên và thuật toán backtrack . . . . . . 18
CHƯƠNG 3: CÀI ĐẶT THUẬT TOÁN BẰNG NGÔN NGỮ LẬP TRÌNH
PYTHON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.1. Cài đặt thuật toán . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.1.1. Cài đặt class Item . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.1.2. Cài đặt đọc dữ liệu đầu vào từ tệp tin định dạng .xlsx . . . . . . . . . . . . . . 19
3.1.3. Cài đặt tìm kiếm cận trên . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.1.4. Cài đặt thuật toán backtrack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.1.5. Cài đặt chương trình hoàn chỉnh . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.2. Kiểm thử . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.2.1. Dữ liệu đầu vào . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 3
3.2.2. Dữ liệu đầu ra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.2.3. Kiểm thử các chức năng của thuật toán . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.3. Đánh giá và so sánh . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.3.1. Độ phức tạp của thuật toán . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.3.2. So sánh thời gian thực hiện thuật toán . . . . . . . . . . . . . . . . . . . . . . . . . . 28
TỔNG KẾT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
ĐÁNH GIÁ NỘI BỘ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
TÀI LIỆU THAM KHẢO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 4
DANH MỤC BẢNG
Bảng 1. Bảng dữ liệu đầu vào. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Bảng 2. Bảng đánh giá nội bội. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
DANH MỤC HÌNH ẢNH
Ảnh 1. Xây dựng cây tìm kiếm cho bài toán cái túi. . . . . . . . . . . . . . . . . . . . . . . . . . 11
Ảnh 2. Xây dựng cây tìm kiếm cho bài toán người du lịch. . . . . . . . . . . . . . . . . . . . 12
Ảnh 3. Ví dụ minh họa tìm kiếm nhánh trên và thuật toán backtrack. . . . . . . . . . . . 18 5
CHƯƠNG 1: CƠ SỞ LÝ THUYẾT
1.1. Thuật toán nhánh cận
Thuật toán nhánh cận là một trong các phương pháp chủ yếu giải bài toán tối
ưu tổ hợp. Tư tưởng cơ bản của nó là trong quá trình tìm kiếm ta phân hoạch các
phương án của bài toán ra thành hai hay nhiều tập con như các nút, loại bỏ những
nhánh mà ta biết chắc chắn là không chứa phương án tối ưu.
Kỹ thuật nhánh cận phát triển từ ý tưởng của thuật toán quay lui. Thay vì duyệt
tất cả các trường hợp, nếu ta đến một vị trí mà giá trị của hàm mục tiêu tại đó và các
điểm về sau chắc chắn không tốt nhất thì quay lại luôn. 1.2. Nguyên lý
Nhánh (Branch): Thuật toán chia nhỏ bài toán lớn thành các bài toán con nhỏ
hơn, tức là tạo ra các nhánh trong quá trình giải. Mỗi nhánh đại diện cho một trạng
thái hoặc một tập con của không gian tìm kiếm. Quá trình này tiếp tục cho đến khi đạt đ ợ
ư c một giải pháp hợp lệ hoặc không thể tìm thêm nhánh con nào nữa.
Cận (Bound): Cận là bước quan trọng trong thuật toán nhánh cận, giúp loại bỏ
các nhánh không cần thiết trong quá trình tìm kiếm. Mỗi nhánh sẽ có một giá trị "cận"
tính toán được, và thuật toán chỉ tiếp tục tìm kiếm trong các nhánh mà giá trị cận của
chúng hứa hẹn mang lại giải pháp tốt hơn so với giải pháp đã tìm được trước đó. Nếu
giá trị cận của một nhánh không thể tốt hơn giải pháp hiện tại, nhánh đó sẽ bị loại bỏ.
1.3. Ưu điểm và nhược điểm của thuật toán nhánh cận
1.3.1. Ưu điểm
- Tìm kiếm giải pháp tối ưu: Thuật toán nhánh cận luôn đảm bảo tìm được giải
pháp tối ưu nếu không gian tìm kiếm được duyệt đầy đủ. Đây là một trong những ưu
điểm quan trọng nhất khi so với các thuật toán “tham lam” (Greedy) hoặc xấp xỉ.
- Giảm thiểu không gian tìm kiếm: Thuật toán sử dụng bước lọc nhánh để loại
bỏ các nhánh không khả thi hoặc không có tiềm năng mang lại giải pháp tốt hơn, giúp
giảm đáng kể số lượng trạng thái cần kiểm tra.
- Linh hoạt với nhiều bài toán: Thuật toán có thể áp dụng cho nhiều bài toán
tối ưu hóa rời rạc như bài toán cái balo, bài toán đi đường ngắn nhất (Traveling
Salesman Problem), bài toán phân hoạch, bài toán lập lịch và nhiều bài toán khác.
- Khả năng tận dụng chiến lược cận: Sử dụng các kỹ thuật tính toán cận hiệu
quả (upper bound - cận trên hoặc lower bound - cận dưới) giúp nâng cao hiệu quả của
thuật toán và giảm thời gian tính toán.
- Khả năng dừng sớm: Khi tìm được một giải pháp mà giá trị cận của các nhánh
còn lại không thể tốt hơn giải pháp hiện tại, thuật toán có thể dừng sớm mà không
cần duyệt toàn bộ không gian. 6
1.3.2. Nhược điểm
- Tốn kém về bộ nhớ: Do cần lưu trữ tất cả các nhánh và thông tin liên quan
(như trạng thái, giá trị cận), thuật toán có thể yêu cầu rất nhiều bộ nhớ, đặc biệt đối
với các bài toán có không gian tìm kiếm lớn.
- Độ phức tạp tính toán cao: Mặc dù thuật toán loại bỏ được nhiều nhánh không
cần thiết, việc tính toán cận và quản lý các nhánh vẫn tiêu tốn thời gian và tài nguyên,
đặc biệt với các bài toán có kích thước lớn.
- Phụ thuộc vào chiến lược cận: Hiệu quả của thuật toán phụ thuộc rất nhiều
vào cách tính toán cận trên hoặc cận dưới. Nếu cận không chính xác hoặc không chặt
chẽ, thuật toán có thể phải duyệt qua nhiều nhánh không cần thiết, làm giảm hiệu quả.
- Không phù hợp với không gian tìm kiếm quá lớn: Khi không gian tìm kiếm
quá rộng hoặc không thể dễ dàng tính toán cận, thuật toán có thể trở nên chậm và
kém hiệu quả so với các phương pháp xấp xỉ.
- Khó khăn trong triển khai: Việc triển khai thuật toán nhánh cận yêu cầu hiểu
rõ bài toán và các kỹ thuật tính toán cận, điều này có thể phức tạp hơn so với các thuật
toán tham lam hoặc quy hoạch động.
1.4. Các bước chính của thuật toán
- Khởi tạo: Bắt đầu với một giải pháp ban đầu (có thể là giải pháp xấp xỉ hoặc
giải pháp hợp lệ nhất), tính toán cận dưới (hoặc cận trên) của bài toán. Đây là bước
khởi tạo không gian tìm kiếm.
- Chia nhánh (Branching): Tạo ra các nhánh con bằng cách chia bài toán lớn
thành các bài toán con nhỏ hơn, từ đó phát triển không gian tìm kiếm. Mỗi nhánh có
thể đại diện cho một tập con của các lựa chọn hoặc các trạng thái.
- Tính cận (Bounding): Với mỗi nhánh, tính toán giá trị cận (thường là cận
dưới đối với bài toán tối thiểu hoặc cận trên đối với bài toán tối đa). Cận này là một
ước lượng của giá trị tối ưu trong nhánh đó.
- Lọc nhánh (Pruning): Loại bỏ các nhánh mà giá trị cận của chúng không thể
tốt hơn giải pháp hiện tại hoặc không có khả năng tìm ra một giải pháp tốt hơn. Điều
này giúp giảm thiểu không gian tìm kiếm và tiết kiệm thời gian.
- Tiến hành tìm kiếm: Tiếp tục mở rộng các nhánh còn lại cho đến khi tìm
được giải pháp tối ưu hoặc không còn nhánh nào để kiểm tra.
1.5. Mô tả thuật toán
Ta sẽ mô tả thuật toán trên mô hình bài toán tối ưu tổng quát sau: min { f(x) : x ∈ D },
Trong đó D là tập hữu hạn phần tử.
Giả thiết rằng tập D được mô tả như sau: 7
D={x = (x1, x2, ... , xn) ∈ A1 × A2 × ... × An: x thoả mãn tính chất P},
Với A1, A2, . . , An là các tập hữu hạn, còn P là tính chất cho trên tích Đề-các A1 × A2 × . . × An.
Với giả thiết về tập D nêu trên, chúng ta có thể sử dụng thuật toán quay lui để
liệt kê các phương án của bài toán. Trong quá trình liệt kê theo thuật toán quay lui, ta
sẽ xây dựng dần các thành phần của phương án. Một bộ gồm k thành phần (a1, a2, a3,
… , ak) xuất hiện trong trong quá trình thuật toán sẽ gọi là phương án bộ phận cấp k.
Thuật toán nhánh cận có thể áp dụng để giải bài toán đặt ra nếu như có thể tìm
được một hàm g xác định trên trên tập tất cả phương án bộ phận của bài toán thỏa
mãn bất đẳng thức sau:
g(a1, a2, ... , ak) ≤ min {f(x): x ∈ D, xi = ai, i = 1, 2, ..., k} (*)
Bất đẳng thức (*) có nghĩa là giá trị của hàm g tại phương án bộ phận (a1, a2,
… , ak) là không vượt quá giá trị nhỏ nhất của hàm mục tiêu của bài toán trên tập con các phương án.
D(a1, a2, … , ak)={ x ∈ D : x1 =ai, i = 1, 2, ... , k}
Hay nói một cách khác, g(a1, a2, … , ak) là cận dưới của giá trị hàm mục tiêu
trên tập D(a1, a2, … , ak). Vì lẽ đó, hàm g được gọi là hàm cận dưới và giá trị g(a1, a2,
… , ak) được gọi là cận dưới của tập D(a1, a2, … , ak). Do có thể đồng nhất tập D(a1,
a2, … , ak) với phương án bộ phận (a1, a2, … , ak), nên ta cũng gọi giá trị g(a1, a2, a3,
… , ak) là cận dưới của phương án bộ phận (a1, a2, … , ak).
Giả sử đã có hàm g. Ta xét cách sử dụng hàm này để giảm bớt khối lượng
duyệt trong quá trình duyệt tất cả các phương án theo thuật toán quay lui. Trong quá
trình liệt kê các phương án có thể đã thu được một số phương án của bài toán. Gọi x
là phương án với giá trị hàm mục tiêu nhỏ nhất trong số các phương án đã tìm được,
kí hiệu f = f(x). Ta sẽ gọi x là phương án tốt nhất hiện có, còn f là kỷ lục. Giả sử đã có f, khi đó nếu:
g(a1, a2, … , ak) > f,
Thì từ bất đẳng thức (*) suy ra
f < g(a1, a2, … , ak) ≤ min {f(x): x ∈ D : x1 = ai, i= 1, 2, ... , k}
Vì thế tập con các phương án ấy của bài toán D(a1, a2, … , ak) chắc chắn không
chứa phương án tối ưu. Trong trường hợp này ta không cần tiếp tục phát triển các
phương án bộ phận ((a1, a2, … , ak), nói cách khác là ta có thể loại bỏ các phương án
trong tập D(a1, a2, … , ak) khỏi quá trình tìm kiếm. 8
1.6. Áp dụng giải bài toán
1.6.1. Bài toán cái túi
Đưa ra giả thiết rằng có n loại đồ vật và số lượng đồ vật mỗi loại là không hạn
chế. Khi đó ta có mô hình bài toán cái túi biến nguyên sau đây: Có n loại đồ vật, đồ
vật thứ j có trọng lượng aj và giá trị sử dụng là cj (j = 1, 2, . . , n). Cần chất các đồ vật
này vào một cái túi có trọng lượng là b sao cho tổng giá trị sử dụng của các đồ vật
chất trong túi là lớn nhất.
Mô hình của bài toán sẽ có dạng như sau: Tìm: 𝑛 𝑛
𝑓 = 𝑚𝑎𝑥 {𝑓(𝑥) = ∑ 𝑐𝑗𝑥𝑗 ; ∑ 𝑎𝑗𝑥𝑗 ≤ 𝑏, 𝑥𝑗 ∈ 𝑍+, 𝑗 = 1, 2, . .. , 𝑛} (1) 𝑗=1 𝑗=1
Trong đó Z+ là tập các số nguyên không âm. 𝑛
𝐷 = {𝑥 = (𝑥1, 𝑥2, . .. ,𝑥𝑛; ∑ 𝑎𝑗𝑥𝑗 ≤ 𝑏, 𝑥𝑗 ∈ 𝑍+,𝑗 = 1, 2,. .. , 𝑛} 𝑗=1
Không gian tổng quát ta giả thiết rằng các đồ vật được đánh số sao cho bất
đẳng thức sau được thoả mãn: 𝑐1 𝑐2 𝑐𝑛 𝑎 ≥ ≥ . . . ≥ (2) 1 𝑎2 𝑎𝑛
Để tiếp tục xây dựng hàm tính cận dưới, cùng với bài toán cái túi (1) ta xét bài
toán cái túi biến liên tục sau: Tìm:
𝑔 = 𝑚𝑎𝑥 { 𝑓(𝑥) = ∑ 𝑛 𝑛
𝑗=1𝑐𝑗𝑥𝑗 : ∑𝑗=1 𝑎𝑗𝑥𝑗 ≤ 𝑏, 𝑥𝑗 ≥ 0, 𝑗 = 1, 2, . . . , 𝑛} (3)
Mệnh đề: Phương án tối ưu của bài toán (3) là vectơ x = (x1, x2, ... , xn) với
các thành phần được xác định bởi công thức: 𝑏
𝑥 1 =𝑎 ,𝑥 2 = 𝑥 3 = ...= 𝑥 𝑛 = 0. 1
Và giá trị tối ưu là 𝑔 = 𝑐1𝑏1 𝑎1
Chứng minh: Thực vậy, xét x = (x1, x2, . . , xn) là một phương án tuỳ ý của
bài toán (3). Khi đó từ bất đẳng thức (3) và do x1 ≥ 0, ta suy ra:
𝑐𝑗𝑥𝑗 ≥ (𝑐1/𝑎1) 𝑎𝑗𝑥𝑗, 𝑗 = 1, 2,. .. ,𝑛 Suy ra: 9 𝑛 𝑛 𝑛
∑ 𝑐𝑗𝑥𝑗 ≤ ∑(𝑐1/𝑎1)𝑎𝑗𝑥𝑗 = (𝑐1𝑎1) ∑ 𝑎𝑗𝑥𝑗 ≤ (𝑐1/𝑎1)𝑏 = 𝑔∗. 𝑗=1 𝑗=1 𝑗=1
Mệnh đề được chứng minh.
Bây giờ, giả sử ta có phương án bộ phận cấp k: (u1, u2, . . , uk). Khi đó giá trị
sử dụng của các đồ vật đang có trong túi là:
𝜎𝑘 = 𝑐1𝑢1 + 𝑐2𝑢2+ .. .+𝑐𝑘𝑢𝑘
Và trọng lượng còn lại của túi là:
𝑏𝑘 = 𝑏 − 𝑎1𝑢1 + 𝑎2𝑢2+ . .. +𝑎𝑘𝑢𝑘. Ta có:
𝑚𝑎𝑥{ 𝑓(𝑥): 𝑥 ∈ 𝐷, 𝑥𝑗 = 𝑢𝑗, 𝑗 = 1, 2,. .. , 𝑘} 𝑛 𝑛
= 𝑚𝑎𝑥{ 𝜎𝑘 + ∑ 𝑐𝑗𝑥𝑗 : ∑ 𝑎𝑗𝑥𝐽 ≤ 𝑏𝑘, 𝑥𝑗 ∈ 𝑍+, 𝑗 = 𝑘 + 1, 𝑘 + 2, . .., 𝑛} 𝑗=𝑘+1 𝑗=𝑘+1 𝑛 𝑛
≤ 𝜎𝑘 + 𝑚𝑎𝑥{ ∑ 𝑐𝑗𝑥𝑗 : ∑ 𝑎𝑗𝑥𝐽 ≤ 𝑏𝑘, 𝑥𝑗 ≥ 0, 𝑗 = 𝑘 + 1, 𝑘 + 2, .. . ,𝑛} 𝑗=𝑘+1 𝑗=𝑘+1
(Theo mệnh đề: Giá trị số hạng thứ hai là 𝑐𝑘+𝑗𝑏𝑘/𝑎 ) 𝑘+𝑗
Vậy ta có thể tính cận trên cho phương án bộ phận (u1, u2, … , uk) bởi công thức:
𝑔(𝑢1,𝑢2, . . . , 𝑢𝑘) = 𝜎𝑘 + 𝑐𝑘+1𝑏𝑘/𝑎𝑘+1
Chú ý: Khi tiếp tục xây dựng thành phần thứ k + 1 của lời giải, các giá trị đề
cử cho xk+1 sẽ là 0, 1, … , [bkak+1]. Do có kết quả của mệnh đề, khi chọn giá trị cho
xk+1 ta sẽ duyệt các giá trị đề cử theo thứ tự giảm dần.
Ví dụ ứng dụng: Giải bài toán cái túi theo thuật toán nhánh cận vừa trình bày.
𝑓(𝑥) = 10𝑥1 + 5𝑥2 + 3𝑥3 + 6𝑥4 → 𝑚𝑎𝑥,
5𝑥1 + 3𝑥2 + 2𝑥3 + 4𝑥4 ≤ 8,
𝑥𝑗 ∈ 𝑍+,𝑗 = 1,2, 3,4. Giải: 10
Ảnh 1. Xây dựng cây tìm kiếm cho bài toán cái túi.
Quá trình giải bài toán được mô tả trong cây tìm kiếm trong hình trên. Thông
tin về một phương án bộ phận trên cây được ghi trong các ô trên hình tương ứng theo
thứ tự: đầu tiên là các thành phần của phương án, tiếp đến σ là giá trị của các đồ vật
đang chất trong túi, w – trọng lượng còn lại của túi và g – cận trên.
Kết thúc thuật toán, ta thu được phương án tối ưu là x=(1,1,0,0), và giá trị tối ưu là f=15.
1.6.2. Bài toán người du lịch
Cố định thành phố xuất phát là 𝑇1, bài toán người du lịch dẫn về bài toán: Tìm cực tiểu của hàm:
𝑓(𝑥2,𝑥3,… ,𝑥𝑛) = 𝑐[1, 𝑥2] + 𝑐[𝑥2,𝑥3] + ⋯ + 𝑐[𝑥𝑛−1, 𝑥𝑛] + 𝑐[𝑥𝑛, 1] → min,
Với điều kiện: (𝑥2,𝑥3,… , 𝑥𝑛) là hoán vị của các số 2,3, …, 𝑛.
Ký hiệu: 𝑐𝑚𝑖𝑛 = min{𝑐[𝑖, 𝑗], 𝑖,𝑗 = 1,2, …, 𝑛, 𝑖 𝑘ℎá𝑐 𝑗} là chi phí đi lại nhỏ nhất giữa các thành phố.
Để có được thuật toán nhánh cận giải bài toán người du lịch, trước hết cần đưa
ra cách đánh giá cận dưới cho phương án bộ phận. Giả sử đang có phương án bộ phận
(𝑢1,𝑢2,… , 𝑢𝑘). Phương án này tương ứng với hành trình bộ phận qua k thành phố:
𝑇1 → 𝑇(𝑢2) → ⋯ → 𝑇(𝑢𝑘−1) → 𝑇(𝑢𝑘)
Vì vậy chi phí phải trả theo hành trình bộ phận này sẽ là:
𝜎 = 𝑐[1, 𝑢2] + 𝑐[𝑢2, 𝑢3] + ⋯ + 𝑐[𝑢𝑘−1,𝑢𝑘] 11
Để phát triển hành trình bộ phận này thành hành trình đầy đủ, ta còn phải đi
qua 𝑛 − 𝑘 thành phố còn lại rồi quay trở lại thành phố 𝑇1, tức là còn phải đi qua 𝑛 −
𝑘 + 1 đoạn đường nữa. Do chi phí phải trả cho việc đi qua mỗi một trong số 𝑛 − 𝑘 +
1 đoạn đường còn lại nếu không ít hơn 𝑐𝑚𝑖𝑛, nên cận dưới cho phương án bộ phận
(𝑢1,𝑢2,… , 𝑢𝑘) có thể tính theo công thức
𝑔(𝑢1,𝑢2,… , 𝑢𝑘) = 𝜎 + (𝑛 − 𝑘 + 1)𝑐𝑚𝑖𝑛
Sử dụng cách cận dưới vừa nêu ta có thể áp dụng thuật toán nhánh cận để giải
bài toán người du lịch.
Ví dụ: Giải bài toán người du lịch với ma trận chi phí sau: 0 3 14 18 15 3 0 4 22 20 C = 17 9 0 16 4 6 2 7 0 12 9 15 11 5 0
Ta có 𝑐𝑚𝑖𝑛 = 3. Quá trình thực hiện thuật toán được mô tả bởi cây tìm kiếm dưới đây:
Ảnh 2. Xây dựng cây tìm kiếm cho bài toán người du lịch.
Thông tin về một bộ phận trên cây đuọc ghi trong các ô trên hình tương ứng
theo thứ tự: đầu tiên là các thành phần của phương án, tiếp đến là chi phí tương theo
hành trình bộ phận và g - cận dưới. 12
Kết thúc thuật toán, ta thu được phương án tối ưu (1, 2 , 3, 5, 4, 1) tương ứng với hành trình:
𝑇1 → 𝑇2 → 𝑇3 → 𝑇5 → 𝑇4 → 𝑇1,
Và chi phí nhỏ nhất là 25.
Hiệu quả của thuật toán nhánh cận phụ thuộc vào việc xây dựng hàm tính cận
dưới. Việc xây dựng hàm tính cận dưới lại phụ thuộc vào cách xây dựng thủ tục duyệt
các phương án của bài toán (được gọi là thủ tục phân nhánh). Trên đây chúng ta đã
trình bày cách xây dựng cận dưới khá đơn giản cho bài toán nổi tiếng của tối ưu tổ
hợp. Các chương trình được cài đặt theo các thuật toán đó, tuy rằng làm việc tốt hơn
nhiều so với duyệt toàn bộ, nhưng cũng chỉ có thể áp dụng để giải các bài toán với
kích thước nhỏ. Muốn giải được các bài toán đặt ra với kích thước lơn hơn cần có
cách đánh giá cận tốt hơn. 13
CHƯƠNG 2: PHÂN TÍCH - XÂY DỰNG THUẬT TOÁN TỐI ƯU HÓA
LỰA CHỌN ĐỒ VẬT TRONG BÀI TOÁN BALO (KNAPSACK)
2.1. Mô tả bài toán
2.1.1. Xây dựng bài toán thực thế
Bạn là một nhà thám hiểm và đang chuẩn bị cho một chuyến đi dài. Bạn có
một balo với trọng lượng tối đa cho phép là X Kilogram (KG). Bạn có một số đồ vật
cần mang theo, mỗi đồ vật có một trọng lượng và một giá trị khác nhau. Mục tiêu của
bạn là tối ưu hóa việc lựa chọn đồ vật để tối đa hóa tổng giá trị mang theo mà không
vượt quá trọng lượng tối đa ủ c a balo.
Yêu cầu đầu vào của bài toán bao gồm:
- Trọng lượng tối đa của balo: Một số nguyên dương.
- Danh sách đồ vật: Một danh sách chứa thông tin về từng đồ vật như ID đồ
vật, trọng lượng (KG), giá trị (đơn vị tiền tệ).
Yêu cầu đầu ra của bài toán bao gồm:
- Danh sách đồ vật được chọn: Một danh sách các ID của từng đồ vật đã được chọn mang đi.
- Tổng giá trị: Một số nguyên thể hiện tổng giá trị của các đồ vật đã được chọn.
- Tổng trọng lượng: Một số nguyên thể hiện tổng trọng lượng của các đồ vật đã được chọn. Nhiệm vụ cụ thể:
- Phát triển thuật toán: Sử dụng phương pháp nhánh cận để tìm kiếm giải pháp
tối ưu cho bài toán balo.
- Thực hiện kiểm tra ràng buộc: Đảm bảo rằng tổng trọng lượng của các đồ
vật không vượt quá trọng lượng tối đa của balo.
- In ra kết quả: Hiển thị danh sách các đồ vật được chọn, tổng giá trị và tổng trọng lượng.
- Phân tích và so sánh: So sánh kết quả với các phương pháp khác (nếu có) để
đánh giá hiệu quả của thuật toán nhánh cận.
2.1.2. Phát biểu bài toán dưới dạng tổng quát
Từ bài toán được xây dựng tại Mục 2.1.1., ta có thể phát biểu dưới dạng tổng quát như sau:
Cho n đồ vật khác nhau, trong đó, đồ vật thứ i có mã IDi, trọng lượng wi và giá
trị vi. Bạn mang theo một chiếc balo có tải trọng tối đa là W, nhiệm vụ đặt ra là chọn
các đồ vật để cho vào balo sao cho tổng giá trị các đồ vật lấy được là lớn nhất có thể?
Yêu cầu đầu vào của bài toán là một tập tin định dạng .xlsx, trong đó bao gồm: 14
- Trọng lượng tối đa của balo: Một số nguyên dương.
- Danh sách đồ vật: Một danh sách chứa thông tin về từng đồ vật như ID đồ
vật, trọng lượng (KG), giá trị (đơn vị tiền tệ).
Yêu cầu đầu ra của bài toán:
- Dòng đầu tiên: Một danh sách ID của đồ vật đã được chọn.
- Dòng thứ hai: Một số nguyên thể hiện tổng giá trị của các đồ vật đã được chọn.
- Dòng thứ ba: Một số nguyên thể hiện tổng trọng lượng của các đồ vật đã được chọn.
2.2. Xây dựng thuật toán
2.2.1. Ý tưởng
Bài toán balo yêu cầu tìm kiếm giá trị tối ưu của một tập hợp các đồ vật trong
một balo có trọng lượng tối đa W. Mỗi đồ vật chỉ có thể được chọn hoặc không chọn
một lần duy nhất. Đầu vào gồm trọng lượng tối đa W của balo và một danh sách các
đồ vật, mỗi đồ vật đề có các thuộc tính là ID, trọng lượng (w - weight) và giá trị (v - value).
Bước đầu tiên, thực hiện tính tỷ lệ giá t ịr = v của mỗi đồ vật. Sau đó, ta trọng lượng w
sắp xếp danh sách các đồ vật đã cho theo thứ tự giảm dần của tỷ lệ v , vì việc lựa w
chọn các đồ vật có tỷ lệ v cao sẽ mang lại giá trị tối ưu hơn. w
Tiếp theo, ta áp dụng thuật toán nhánh và cận (branch and bound) để tính toán
giá trị tối ưu. Công thức tính cận trên (upper bound) như sau: vi + 1
𝑢𝑏 = 𝑣 + (𝑊 − 𝑤) ∗ wi + 1 Trong đó: - v
: Tổng giá trị của các đồ vật đã chọn.
- W : Trọng lượng tối đa của balo.
- w : Trọng lượng hiện tại của balo.
- vi+1 : Giá trị của món đồ tiếp theo trong danh sách đã sắp xếp.
- wi+1 : Trọng lượng của món đồ tiếp theo trong danh sách đã sắp xếp.
Thuật toán bắt đầu với trọng lượng w = 0 và giá trị v = 0. Sau đó, ta tính giá
trị cận trên ubMax và tiến hành duyệt qua từng đồ vật. Tại mỗi lần duyệt, nếu trọng
lượng của món đồ hiện tại i (1 ≤ i ≤ n) không vượt quá W (wi ≤ W), ta chọn đồ vật đó
và cập nhật lại giá trị vMax += vi (giá trị tốt nhất hiện tại) và wMax += wi (trọng lượng 15
tối đa hiện tại). Nếu không chọn món đồ đó vì vượt quá trọng lượng tối đa (wi > W), ta bỏ qua nó.
Quá trình này tiếp tục cho đến khi không thể chọn thêm món đồ nào vào balo
mà không vượt quá trọng lượng tối đa (wMax ≥ W), hoặc khi tất cả các món đồ đã được xét qua.
Cuối cùng, thuật toán quay lui backtrack được sử dụng để tìm kiếm các giải
pháp tối ưu hơn. Thuật toán backtrack bắt đầu bằng việc tính toán chỉ số r, đại diện
cho chỉ số của món hàng đầu tiên không được thêm vào túi do trọng lượng vượt quá
khả năng chứa của túi wr > W - wr. Sau đó, tính toán giá trị giới hạn trên theo công thức: 𝑢𝑏 = ∑𝑟−1 𝑗
v + vr (nếu balo chưa đầy)
Nếu giá trị ub này không vượt quá ubMax hiện tại, thuật toán sẽ quay lại một
bước trước đó trong quá trình tìm kiếm ể
đ tìm kiếm các giải pháp tốt hơn.
“Quay lại một bước trước đó” có nghĩa là khi thuật toán đang trong một tình
huống mà nó đã chọn hoặc bỏ qua một món đồ, thuật toán sẽ quay lại và thử một lựa
chọn khác. Ví dụ, thay vì bỏ món đồ đó, thuật toán sẽ thử chọn để xem có tìm ra giá trị tốt hơn không.
2.2.2. Xây dựng giả mã
function knapsack_backtrack(W, items):
# W: Trọng lượng tối đa của balo
# items: Danh sách các đồ vật, mỗi đồ vật có ID, trọng lượng, giá trị
# Sắp xếp đồ vật theo giá trị/trọng lượng (v/w) theo thứ tự giảm dần sort items by v/w descending
# Khởi tạo các biến cần thiết vMax = 0
# Giá trị tối ưu hiện tại wMax = 0
# Trọng lượng tối đa mang được
current_weight = 0 # Trọng lượng hiện tại trong balo
current_value = 0 # Giá trị hiện tại trong balo solution = []
# Danh sách các đồ vật được chọn # Hàm tính cận trên
function upper_bound(index, current_weight, current_value):
remaining_weight = W - current_value bound = current_value
while index < len(items) and items[index].weight <= remaining_weight:
remaining_weight -= items[index].weight bound += items[index].weight index += 1
if index < len(items)
bound += items[index].value * (remaining_weight / items[index].weight) return bound 16
# Hàm backtracking
function backtrack(index, current_weight, current_value, solution): nonlocal vMax, wMax
# Cập nhật giá trị tối ưu nếu có
if current_value > vMax: vMax = current_value wMax = current_weight best_solution = solution[:]
# Nếu không thể chọn thêm đồ vật (quá trọng lượng hoặc hết đồ vật)
if index >= len(items) or current_weight >= W: return # Tính cận trên
bound = upper_bound(index, current_weight, current_value)
# Nếu cận trên không thể vượt qua giá trị tối ưu, quay lui if bound <= vMax: return
# Chọn đồ vật tại index
solution.append(items[index].ID)
backtrack(index + 1, current_weight + items[index].weight,
current_value + items[index].value, solution)
# Quay lại, không chọn đồ vật tại index solution.pop()
backtrack(index + 1, current_weight, current_value, solution)
# Bắt đầu thuật toán backtracking từ index 0 backtrack(0, 0, 0, [])
return vMax, wMax, best_solution 17
2.3. Ví dụ minh họa về tìm kiếm nhánh cận trên và thuật toán backtrack
Ảnh 3. Ví dụ minh họa tìm kiếm nhánh trên và thuật toán backtrack. 18
CHƯƠNG 3: CÀI ĐẶT THUẬT TOÁN BẰNG
NGÔN NGỮ LẬP TRÌNH PYTHON
3.1. Cài đặt thuật toán
3.1.1. Cài đặt class Item
Class Item bao gồm một phương thức khởi tạo (__init__), được gọi khi một
đối tượng mới của lớp Item được tạo ra. Phương thức này nhận vào ba tham số bao
gồm: ID, trọng lượng (w) và giá trị (v). Các tham số này lần lượt được gán cho các
thuộc tính tương ứng của đối tượng bao gồm self.ID, self.weight và self.value. Ngoài
ra, tỷ lệ giá t ịr = v cũng được tính toán và gán cho thuộc tính self.ratio. Tỷ lệ này trọng lượng w
dùng để sắp xếp danh sách các món đồ, phục vụ cho bài toán tối ưu hóa. class Item:
def __init__(self, ID, weight, value): self.ID = ID self.weight = weight self.value = value self.ratio = value / weight
3.1.2. Cài đặt đọc dữ liệu đầu vào từ tệp tin định dạng .xlsx
Một hàm knapsack_from_excel được định nghĩa để đọc dữ liệu từ một tệp tin
Excel có định dạng .xlsx và tạo ra các đối tượng Item từ dữ liệu đó. Đầu tiên, hàm mở
file Excel tại đường dẫn file_path thông qua việc sử dụng thư viện openpyxl, sau đó
lấy sheet đang hoạt động (active sheet) trong workbook. Sau đó, nó lấy giá trị tại ô
A1, trích xuất số nguyên từ giá trị của ô này và gán nó cho biến W (trọng lượng tối
đa của balo). Hàm lặp qua các dòng từ dòng thứ 6 đến dòng thứ n chứa thông tin các
đồ vật thông qua một vòng lặp for. Trong mỗi lần lặp, hàm lấy ra giá trị các cột A, B
và C tương ứng với ba thông tin của đồ vật bao gồm ID, trọng lượng và giá trị. Các
giá trị này được dùng để tạo ra các đối tượng Item mới với các thuộc tính ID, weight
và value. Nếu các giá trị này tồn tại, đối tượng Item sẽ được thêm vào lưu trữ trong
danh sách items. Danh sách items được khai báo sẽ chứa tất cả đối tượng Item được
tạo ra từ dữ liệu của tệp tin Excel.
# Hàm đọc dữ liệu từ file Excel
def knapsack_from_excel(file_path):
# Đọc dữ liệu từ file Excel
wb = openpyxl.load_workbook(file_path) sheet = wb.active
# Lấy trọng lượng tối đa từ ô A1
cell_value = sheet['A1'].value
W = int(re.search(r'\d+', cell_value).group())
# Đọc các đồ vật từ các ô
items = [] # Mảng lưu trữ thông tin đồ vật
for row in range(6, sheet.max_row + 1): ID = sheet[f'A{row}'].value
weight = sheet[f'B{row}'].value
value = sheet[f'C{row}'].value
if ID and weight and value: 19
items.append(Item(ID, weight, value))
3.1.3. Cài đặt tìm kiếm cận trên
Ý tưởng xây dựng hàm upper_bound là để tính toán giá trị cận trên của bài
toán. Đầu tiên hàm nhận vào các tham số: - w
: Trọng lượng hiện tại của các đồ vật đã chọn. - v
: Giá trị hiện tại của các đồ vật đã chọn. - index
: Chỉ số bắt đầu để xem xét các đồ vật tiếp theo. - items
: Danh sách các đồ vật, mỗi ồ
đ vật là một đối tượng Item. - W
: Trọng lượng tối đa của balo.
Sau đó hàm kiểm tra ràng buộc, nếu trọng lượng hiện tại đã vượt quá trọng
lượng tối đa (w ≥ W) thì trả về 0, vì không thể lấy thêm đồ vật nào nữa. Nếu chưa
vượt quá (w < W), hàm khởi tạo biến: - bound = v
: Khởi tạo giá trị tối đa bằng giá trị hiện tại v.
- total_weight = w : Khởi tạo tổng trọng lượng bằng trọng lượng hiện tại w.
Tiếp theo hàm sử dụng một vòng lặp for để duyệt qua tất cả các đồ vật có chỉ
số từ index đến hết danh sách items. Trong mỗi lần lặp, hàm thực hiện so sánh giữa
trọng lượng hiện tại cộng trọng lượng đồ vật tại vị trí đang xét trong danh sách với
trọng lượng tối đa của balo. Nếu tổng trên vẫn nhỏ hơn trọng lượng tối đa của balo,
trọng lượng của vật đó được cập nhật thêm vào trọng lượng hiện tại và giá trị của vật
đó cũng được cập nhật thêm vào giá trị hiện tại. Nếu tổng trên lớn hơn trọng lượng
tối đa của balo, hàm sẽ dừng vì không thể thêm đồ vật nào nữa. # Hàm tính cận trên
def upper_bound(w, v, index, items, W): if w > W: return 0 bound = v total_weight = w
# Tính giá trị tối đa có thể đạt được từ index hiện tại
for i in range(index, len(items)):
if total_weight + items[i].weight <= W:
total_weight += items[i].weight bound = items[i].value else: break return bound
3.1.4. Cài đặt thuật toán backtrack
Hàm knapsack_backtrack nhận vào các tham số bao gồm: - index : Chỉ số hiện tại. - current_weight
: Trọng lượng hiện tại. 20 - current_value : Giá trị hiện tại. - items
: Danh sách các đồ vật. - W
: Trọng lượng tối đa của balo.
- current_solution : Danh sách các đồ vật hiện đã được chọn.
Hai biến toàn cục vMax (lưu trữ giá trị tối đa tìm được) và best_solution (lưu
trữ giải pháp tốt nhất tìm được).
Đầu tiên, thuật toán kiểm tra nếu trọng lượng hiện tại không vượt quá trọng
lượng tối đa của balo và tổng giá trị của giải pháp hiện tại lớn hơn giá trị tối đa đã
biết (vMax). Nếu điều đó đúng, thuật toán cập nhật giá trị tối đa mới cho vMax và sao
chép giải pháp hiện tại vào best_solution.
Tiếp theo, nếu đã xét hết tất cả các đồ vật trong danh sách, tức là chỉ số index
lúc này vượt quá độ dài của danh sách items, thuật toán sẽ dừng lại. Trước khi thử xét
nhánh hiện tại, thuật toán tính cận trên bằng cách gọi hàm upper_bound. Nếu cận trên
của nhánh hiện tại không lớn hơn vMax, thuật toán sẽ cắt bỏ nhánh này vì không thể
mang lại giá trị tốt hơn.
Sau đó, thuật toán sẽ thử hai lựa chọn:
- Lựa chọn 1: Không chọn đồ vật hiện tại và gọi đệ quy để tiếp tục xét các đồ
vật tiếp theo mà không thay đổi trọng lượng và giá trị.
- Lựa chọn 2: Chọn đồ vật hiện tại nếu không vượt quá trọng lượng tối đa ủ c a
balo. Trong trường hợp này, thuật toán sẽ cập nhật trọng lượng và giá trị, sau đó tiếp
tục gọi đệ quy để tìm xem liệu còn giải pháp tốt hơn không.
# Hàm backtracking
def knapsack_backtrack(index, current_weight, current_value, items, W, current_solution):
global vMax, best_solution
# Cập nhật giá trị tối ưu nếu tìm được giải pháp tốt hơn
if current_weight <= W and current_value > vMax: vMax = current_value
best_solution = current_solution[:]
# Nếu đã xét hết các đồ vật, dừng lại
if index >= len(items): return
# Tính cận trên để kiểm tra xem có nên tiếp tục không
if upper_bound(current_weight, current_value, index, items, W) <= vMax:
return # Cắt nhánh vì không thể đạt giá trị tốt hơn vMax
# Bỏ qua đồ vật hiện tại
knapsack_backtrack(index + 1, current_weight, current_value, items, W, current_solution) 21
# Thêm đồ vật hiện tại nếu không vượt quá trọng lượng tối đa
if current_weight + items[index].weight <= W:
current_solution.append(items[index])
knapsack_backtrack(index + 1, current_weight +
items[index].weight, current_value + items[index].value, items, W, current_solution) current_solution.pop()
3.1.5. Cài đặt chương trình hoàn chỉnh
import re import time import openpyxl start = time.time()
# Khởi tạo lớp Item đại diện cho mỗi đồ vật class Item:
def __init__(self, ID, weight, value): self.ID = ID self.weight = weight self.value = value self.ratio = value / weight # Hàm tính cận trên
def upper_bound(w, v, index, items, W): if w >= W: return 0 bound = v total_weight = w
# Tính giá trị tối đa có thể đạt được từ index hiện tại
for i in range(index, len(items)):
if total_weight + items[i].weight <= W:
total_weight += items[i].weight bound += items[i].value else: break return bound
# Hàm backtracking
def knapsack_backtrack(index, current_weight, current_value, items, W, current_solution):
global vMax, best_solution
# Cập nhật giá trị tối ưu nếu tìm được giải pháp tốt hơn
if current_weight <= W and current_value > vMax: vMax = current_value
best_solution = current_solution[:]
# Nếu đã xét hết các đồ vật, dừng lại
if index >= len(items): return
# Tính cận trên để kiểm tra xem có nên tiếp tục không
if upper_bound(current_weight, current_value, index, items, W) <= vMax: 22