🎯 Branch-and-Bound và tìm kiếm tăng dần
Branch-and-Bound dùng nghiệm tốt nhất hiện có và các cận để loại cả cây con không thể cải thiện đáp án. Tìm kiếm tăng dần giới hạn tài nguyên theo từng vòng, như DLS, IDDFS và IDA*, nhằm kết hợp ưu điểm của DFS với khả năng tìm nghiệm nông hoặc chi phí nhỏ.
🎯 Mục tiêu bài học
Cần hiểu
- Incumbent, cận trên và cận dưới.
- Điều kiện cắt nhánh trong bài tối đa hóa/tối thiểu hóa.
- DFS Branch-and-Bound và Best-first B&B.
- Depth-Limited Search, IDDFS và IDA*.
- Khác nhau giữa pruning đúng và heuristic.
Cần làm được
- Xây cận khả thi cho bài tối ưu.
- Dùng nghiệm hiện có để cắt nhánh.
- Sắp thứ tự nhánh để tìm nghiệm tốt sớm.
- Viết IDDFS trên cây trạng thái.
- Nhận biết khi nào IDA* phù hợp hơn A*.
1. Đặt vấn đề
Có n món đồ, mỗi món có trọng lượng và giá trị. Chọn một số món sao cho tổng trọng lượng không vượt W và tổng giá trị lớn nhất.
Duyệt toàn bộ có 2ⁿ tập con. Tuy nhiên tại một trạng thái, nếu ngay cả trong trường hợp lạc quan nhất, cây con đó vẫn không thể vượt nghiệm tốt nhất đã có, ta có thể bỏ toàn bộ cây con.
2. Branch-and-Bound
Incumbent
Nghiệm khả thi tốt nhất đã tìm thấy. Với tối đa hóa, incumbent là giá trị lớn nhất hiện có.
Upper bound
Giá trị tốt nhất mà một cây con có thể đạt trong trường hợp lạc quan.
Lower bound
Trong bài tối thiểu hóa, lower bound là chi phí nhỏ nhất có thể của cây con.
| Bài toán | Incumbent | Cận của node | Điều kiện cắt |
|---|---|---|---|
| Tối đa hóa | Giá trị khả thi lớn nhất | Upper bound | bound ≤ incumbent |
| Tối thiểu hóa | Chi phí khả thi nhỏ nhất | Lower bound | bound ≥ incumbent |
3. Mô phỏng Branch-and-Bound cho ba lô
4. DFS B&B và Best-first B&B
| Chiến lược | Node tiếp theo | Ưu điểm | Nhược điểm |
|---|---|---|---|
| DFS Branch-and-Bound | Node sâu nhất | Bộ nhớ nhỏ, dễ cài. | Có thể tìm incumbent tốt muộn. |
| Best-first B&B | Node có bound hứa hẹn nhất | Thường tập trung vào nhánh tốt. | Priority Queue có thể chứa rất nhiều node. |
| Branch ordering | Thử nhánh tốt trước trong DFS | Cải thiện incumbent sớm. | Không thay đổi worst-case. |
5. Depth-Limited Search và IDDFS
DLS
DFS nhưng không đi sâu hơn giới hạn L. Kết quả có thể là FOUND, NOT_FOUND hoặc CUTOFF.
IDDFS
Chạy DLS với L=0,1,2,... cho đến khi tìm thấy đích.
6. Mô phỏng IDDFS
7. IDA*
IDA* thay giới hạn độ sâu bằng ngưỡng f(n)=g(n)+h(n). Mỗi vòng chỉ mở rộng node có f không vượt ngưỡng; ngưỡng sau là f nhỏ nhất đã vượt ngưỡng ở vòng trước.
g(n)
Chi phí thật từ trạng thái đầu tới n.
h(n)
Ước lượng chi phí còn lại; muốn tối ưu cần heuristic admissible.
Ngưỡng
Bắt đầu từ h(start), tăng dần theo cận f.
8. Phân tích thuật toán
| Thuật toán | Thời gian điển hình | Bộ nhớ | Điểm mạnh |
|---|---|---|---|
| DFS B&B | Hàm mũ trong worst-case | O(depth) | Cận tốt có thể giảm mạnh số node. |
| Best-first B&B | Hàm mũ trong worst-case | Có thể hàm mũ | Mở rộng node hứa hẹn trước. |
| DLS | O(b^L) | O(L) | Chặn độ sâu. |
| IDDFS | O(b^d) | O(d) | Tìm nghiệm nông, ít bộ nhớ. |
| IDA* | Phụ thuộc heuristic | O(d) | Tiết kiệm bộ nhớ hơn A*. |
9. Code mẫu
struct Item {
int weight;
int value;
double ratio;
};
vector- items;
int capacity;
int bestValue = 0;
double upperBound(
int index,
int currentWeight,
int currentValue
) {
if (currentWeight > capacity) {
return -1;
}
double bound = currentValue;
int remaining = capacity - currentWeight;
for (int i = index;
i < (int)items.size() && remaining > 0;
i++) {
if (items[i].weight <= remaining) {
remaining -= items[i].weight;
bound += items[i].value;
} else {
bound += items[i].ratio * remaining;
break;
}
}
return bound;
}
void branchAndBound(
int index,
int currentWeight,
int currentValue
) {
if (currentWeight > capacity) return;
bestValue = max(bestValue, currentValue);
if (index == (int)items.size()) return;
if (upperBound(index, currentWeight, currentValue)
<= bestValue) {
return;
}
branchAndBound(
index + 1,
currentWeight + items[index].weight,
currentValue + items[index].value
);
branchAndBound(
index + 1,
currentWeight,
currentValue
);
}
struct Node {
int level;
int weight;
int value;
double bound;
bool operator<(const Node& other) const {
return bound < other.bound;
}
};
priority_queue pq;
pq.push(root);
while (!pq.empty()) {
Node current = pq.top();
pq.pop();
if (current.bound <= bestValue) {
continue;
}
// Sinh node chọn và không chọn.
} enum Result {
FOUND,
NOT_FOUND,
CUTOFF
};
Result depthLimitedSearch(
int node,
int target,
int remainingDepth
) {
if (node == target) return FOUND;
if (remainingDepth == 0) return CUTOFF;
bool hadCutoff = false;
for (int child : children[node]) {
Result result = depthLimitedSearch(
child,
target,
remainingDepth - 1
);
if (result == FOUND) return FOUND;
if (result == CUTOFF) hadCutoff = true;
}
return hadCutoff ? CUTOFF : NOT_FOUND;
}
bool iddfs(int start, int target) {
for (int limit = 0; ; limit++) {
Result result = depthLimitedSearch(
start,
target,
limit
);
if (result == FOUND) return true;
if (result == NOT_FOUND) return false;
}
}const int FOUND = -1;
const int INF = 1e9;
int dfs(State state, int g, int threshold) {
int f = g + heuristic(state);
if (f > threshold) return f;
if (isGoal(state)) return FOUND;
int nextThreshold = INF;
for (State next : neighbors(state)) {
if (onCurrentPath(next)) continue;
mark(next);
int result = dfs(
next,
g + moveCost(state, next),
threshold
);
unmark(next);
if (result == FOUND) return FOUND;
nextThreshold = min(nextThreshold, result);
}
return nextThreshold;
}
bool idaStar(State start) {
int threshold = heuristic(start);
while (true) {
int result = dfs(start, 0, threshold);
if (result == FOUND) return true;
if (result == INF) return false;
threshold = result;
}
}10. Giải thích code
10.1. Vì sao sắp đồ vật theo ratio trước khi tính bound?
Fractional knapsack tối ưu khi lấy tỉ lệ value/weight giảm dần. Kết quả phân số là upper bound hợp lệ cho ba lô 0/1.
10.2. Vì sao cập nhật incumbent trước khi kiểm tra bound?
Trạng thái hiện tại đã là một nghiệm khả thi nếu không vượt sức chứa. Cập nhật sớm giúp cắt nhiều nhánh hơn.
10.3. DLS cần phân biệt NOT_FOUND và CUTOFF vì sao?
CUTOFF nghĩa là chưa thể kết luận vì giới hạn sâu đã chặn tìm kiếm; IDDFS cần tăng giới hạn. NOT_FOUND nghĩa là cây con đã hết thật sự.
10.4. IDA* chọn ngưỡng mới thế nào?
Chọn giá trị f nhỏ nhất đã vượt ngưỡng cũ để không bỏ qua mức cận cần thiết.
11. Lỗi thường gặp
- Cắt nhánh bằng cận không an toàn.
- Không cập nhật incumbent khi gặp nghiệm tốt hơn.
- Dùng bound yếu và kỳ vọng giảm mạnh thời gian.
- Best-first nhưng comparator Priority Queue bị đảo.
- IDDFS không phân biệt CUTOFF và NOT_FOUND.
- IDA* dùng heuristic đánh giá quá cao khi cần tối ưu.
- Không tránh chu kỳ trên đường đi hiện tại.
- Không ước lượng bộ nhớ của Best-first.
12. Quiz và bài tập
📘 Cơ bản
- DLS trên cây.
- IDDFS tìm node đích.
- Ba lô B&B nhỏ.
📙 Nâng cao
- TSP Branch-and-Bound.
- Best-first B&B.
- IDA* cho 8-puzzle.
💳 Quét mã ủng hộ tuỳ tâm nhé!