Quy Hoạch Động Bitmask¶
Tác giả: FPTOJ Team
Nội dung tham khảo từ: VNOI Wiki, CP-Algorithms - DP with Bitmasks
1. Bản chất vấn đề¶
Bài toán: Người du lịch (Bitmask TSP)¶
Cho \(N\) thành phố và ma trận khoảng cách \(C[N][N]\). Tìm đường đi ngắn nhất đi qua tất cả thành phố đúng 1 lần và quay về thành phố xuất phát.
Duyệt thường: \(O(N!)\) — quá chậm!
Bitmask DP: \(O(2^N \cdot N^2)\) — chấp nhận được với \(N \le 20\).
Ý tưởng¶
Dùng bitmask \(S\) biểu diễn tập thành phố đã thăm. \(dp[S][v]\) = chi phí nhỏ nhất để đi qua tất cả thành phố trong \(S\), đang ở thành phố \(v\).
2. Tư duy cốt lõi¶
Công thức truy hồi¶
Khởi tạo: \(dp[\{v\}][v] = C[0][v]\) (đi trực tiếp từ thành phố 0 đến \(v\)).
Kết quả: \(\min_{v=1}^{N-1} dp[\{0, 1, \ldots, N-1\}][v] + C[v][0]\) (quay về 0).
Trace chi tiết¶
3 thành phố: \(C = \begin{pmatrix} 0 & 10 & 15 \\ 10 & 0 & 20 \\ 15 & 20 & 0 \end{pmatrix}\)
| \(S\) (mask) | \(v\) | \(dp[S][v]\) | Tính toán |
|---|---|---|---|
| \(\{0\}\) (001) | 0 | 0 | Khởi tạo |
| \(\{1\}\) (010) | 1 | 10 | \(C[0][1] = 10\) |
| \(\{2\}\) (100) | 2 | 15 | \(C[0][2] = 15\) |
| \(\{0,1\}\) (011) | 0 | \(dp[\{1\}][1] + C[1][0] = 10 + 10 = 20\) | |
| \(\{0,1\}\) (011) | 1 | \(dp[\{0\}][0] + C[0][1] = 0 + 10 = 10\) | |
| \(\{0,2\}\) (101) | 0 | \(dp[\{2\}][2] + C[2][0] = 15 + 15 = 30\) | |
| \(\{0,2\}\) (101) | 2 | \(dp[\{0\}][0] + C[0][2] = 0 + 15 = 15\) | |
| \(\{1,2\}\) (110) | 1 | \(dp[\{2\}][2] + C[2][1] = 15 + 20 = 35\) | |
| \(\{1,2\}\) (110) | 2 | \(dp[\{1\}][1] + C[1][2] = 10 + 20 = 30\) | |
| \(\{0,1,2\}\) (111) | 0 | \(\min(dp[\{1,2\}][1]+C[1][0], dp[\{1,2\}][2]+C[2][0]) = \min(45, 45) = 45\) | |
| \(\{0,1,2\}\) (111) | 1 | \(\min(dp[\{0,2\}][0]+C[0][1], dp[\{0,2\}][2]+C[2][1]) = \min(40, 35) = 35\) | |
| \(\{0,1,2\}\) (111) | 2 | \(\min(dp[\{0,1\}][0]+C[0][2], dp[\{0,1\}][1]+C[1][2]) = \min(35, 30) = 30\) |
Kết quả: \(\min(45 + 0, 35 + 20, 30 + 15) = \min(45, 55, 45) = 45\)
3. Phân tích tính đúng đắn¶
Tại sao bitmask DP đúng?¶
Bất biến: Khi tính \(dp[S][v]\), tất cả \(dp[S'][u]\) với \(|S'| < |S|\) đã được tính (duyệt mask tăng dần theo số bit 1).
Tại sao duyệt mask tăng dần đảm bảo đúng?
Số bit 1 trong \(S\) = số thành phố đã thăm. Khi chuyển từ \(S\) sang \(S' = S \cup \{u\}\), số bit 1 tăng đúng 1. Do đó, duyệt mask từ \(0\) đến \(2^N - 1\) đảm bảo mọi tập con \(S'\) đã được xử lý trước \(S\).
Tại sao công thức truy hồi đúng?
Mọi đường đi Hamilton kết thúc tại \(v\) với tập thành phố đã thăm = \(S\) phải có đỉnh cuối trước \(v\) là một đỉnh \(u \in S \setminus \{v\}\). Do đó:
Công thức này xét tất cả khả năng cho đỉnh cuối trước \(v\) → không bỏ sót.
Tại sao \(O(2^N \cdot N)\) trạng thái đủ?
Mỗi trạng thái \((S, v)\) với \(v \in S\) là duy nhất. Số cặp \((S, v)\) hợp lệ = \(\sum_{k=1}^{N} \binom{N}{k} \cdot k = N \cdot 2^{N-1} = O(2^N \cdot N)\).
4. Đánh giá độ phức tạp¶
| Thao tác | Thời gian | Không gian |
|---|---|---|
| Bitmask TSP | \(O(2^N \cdot N^2)\) | \(O(2^N \cdot N)\) |
| Bitmask DP nói chung | \(O(2^N \cdot N \cdot f(N))\) | \(O(2^N \cdot N)\) |
\(f(N)\) = chi phí chuyển trạng thái.
4. Các thao tác bitmask quan trọng¶
Trong bitmask DP, những thao tác bit sau được dùng liên tục. Học thuộc lòng!
| Thao tác | Cú pháp | Ý nghĩa |
|---|---|---|
| Kiểm tra bit thứ \(k\) | mask & (1 << k) |
true nếu \(k \in S\) |
| Bật bit thứ \(k\) | mask \| (1 << k) |
\(S \cup \{k\}\) |
| Tắt bit thứ \(k\) | mask & ~(1 << k) |
\(S \setminus \{k\}\) |
| Đảo bit thứ \(k\) | mask ^ (1 << k) |
toggle \(k\) |
| Mask đầy đủ \(N\) bit | (1 << N) - 1 |
\(\{0, 1, \ldots, N-1\}\) |
| Lấy bit thấp nhất | mask & -mask |
\(k = \min(S)\) |
| Tắt bit thấp nhất | mask & (mask - 1) |
Xóa \(k = \min(S)\) |
| Đếm số bit 1 | __builtin_popcount(mask) (C++) hoặc bin(mask).count('1') (Python) |
\(\|S\|\) |
| Duyệt tất cả tập con của mask | for (int sub = mask; sub; sub = (sub-1) & mask) |
\(O(3^N)\) tổng thể |
| Kiểm tra tập con | (sub & mask) == sub |
\(sub \subseteq mask\) |
5. Ví dụ bổ sung: Bài toán Phân công¶
Bài toán¶
Cho \(N\) người và \(N\) công việc (\(N \le 20\)). Ma trận \(C[i][j]\) = chi phí giao việc \(j\) cho người \(i\). Mỗi người làm đúng 1 việc, mỗi việc có đúng 1 người làm. Tìm tổng chi phí nhỏ nhất.
Công thức DP¶
\(mask\) = tập các công việc đã được giao. Duyệt lần lượt từng người \(i\):
với \(i = \text{popcount}(mask)\) = số công việc đã giao = người thứ \(i\) sẽ nhận.
Trace chi tiết¶
\(N=3\), \(C = \begin{pmatrix} 3 & 2 & 7 \\ 5 & 1 & 3 \\ 2 & 4 & 1 \end{pmatrix}\)
| \(mask\) | \(i\) (số người đã giao) | Công thức | \(dp[mask]\) |
|---|---|---|---|
| 000 | 0 | Khởi tạo | 0 |
| 001 | 1 | \(dp[000] + C[0][0] = 0 + 3\) | 3 |
| 010 | 1 | \(dp[000] + C[0][1] = 0 + 2\) | 2 |
| 100 | 1 | \(dp[000] + C[0][2] = 0 + 7\) | 7 |
| 011 | 2 | \(\min(dp[001]+C[1][1], dp[010]+C[1][0]) = \min(3+1, 2+5)\) | 4 |
| 101 | 2 | \(\min(dp[001]+C[1][2], dp[100]+C[1][0]) = \min(3+3, 7+5)\) | 6 |
| 110 | 2 | \(\min(dp[010]+C[1][2], dp[100]+C[1][1]) = \min(2+3, 7+1)\) | 5 |
| 111 | 3 | \(\min(dp[011]+C[2][2], dp[101]+C[2][1], dp[110]+C[2][0]) = \min(4+1, 6+4, 5+2)\) | 5 |
Kết quả: \(dp[111] = 5\) (người 0 làm việc 1, người 1 làm việc 2, người 2 làm việc 0).
6. Các bẫy thường gặp¶
Bẫy 1: Quên khởi tạo INT_MAX / inf¶
Luôn khởi tạo toàn bộ mảng dp với giá trị vô cùng. Sai sót phổ biến là chỉ khởi tạo dp[0][0] = 0 mà quên các trạng thái khác → truy xuất giá trị rác.
Bẫy 2: Duyệt mask sai thứ tự¶
Bitmask DP yêu cầu duyệt mask tăng dần. Nếu duyệt ngược, trạng thái con chưa được tính → kết quả sai.
Bẫy 3: Overflow khi \(N > 20\)¶
Với \(N > 20\), \(2^N \cdot N^2\) có thể vượt giới hạn. Mẹo tối ưu:
- Nếu đồ thị đối xứng: giảm \(N\) bằng cách cố định đỉnh xuất phát
- Dùng unsigned thay vì int để tiết kiệm bộ nhớ (nếu giá trị nhỏ)
- Cân nhắc chuyển sang Branch & Bound + heuristic
Bẫy 4: Quên đường về trong TSP¶
Sau khi tính \(dp[\text{full}][v]\), cần cộng thêm \(C[v][0]\) (quay về thành phố 0). Rất nhiều người quên bước này.
Bẫy 5: Nhầm \(mask\) là tập hợp hay chỉ số¶
mask & (1 << v)kiểm tra \(v\) có trong \(S\) hay khôngmask | (1 << u)thêm \(u\) vào \(S\)
Đừng nhầm: mask biểu diễn tập thành phố đã thăm, không phải vị trí hiện tại.
Code minh họa¶
Người du lịch (TSP)¶
Bài toán Phân công (Assignment Problem)¶
Bài tập luyện tập trên FPTOJ¶
| # | Bài | Điểm | Độ khó |
|---|---|---|---|
| 1 | bsm-01 - Chia Kẹo Thành Hai Nhóm | 15 | ⭐⭐ |
| 2 | bsm-02 - Đếm Cách Chọn Thùng Hàng Có Tổng \(S\) | 15 | ⭐⭐ |
| 3 | bsm-03 - Hành Trình Thu Mua Nông Sản | 20 | ⭐⭐⭐ |
| 4 | bsm-04 - Phân Công Công Nhân Kho | 20 | ⭐⭐⭐ |
| 5 | bsm-05 - Tuyến Xe Buýt Khép Kín | 25 | ⭐⭐⭐ |
| 6 | bsm-06 - Đếm Hành Trình Giao Hàng | 25 | ⭐⭐⭐⭐ |
| 7 | bsm-07 - Chia Quà Cho Nhiều Lớp | 30 | ⭐⭐⭐⭐ |
| 8 | bsm-08 - Xếp Cá Trên Bàn Cờ \(N \times N\) | 30 | ⭐⭐⭐⭐ |