Chương 5.4 • Optimization & Incremental Search

🎯 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ỏ.

IncumbentNghiệm tốt nhất hiện có
BoundCận cho cây con
IDDFSTăng dần độ sâu
IDA*Tăng dần ngưỡng f=g+h

🎯 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 đề

Bài toán mở đầu – Ba lô 0/1.
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.

Nếu upperBound(state) ≤ incumbent thì cắt nhánh trong bài tối đa hóa
Đặt vấn đề thứ hai. Khi tìm một trạng thái đích trong cây rất sâu nhưng không biết độ sâu nghiệm, DFS có thể đi quá sâu; BFS lại tốn nhiều bộ nhớ. IDDFS tăng giới hạn sâu dần để cân bằng hai phía.

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ánIncumbentCận của nodeĐiều kiện cắt
Tối đa hóaGiá trị khả thi lớn nhấtUpper boundbound ≤ incumbent
Tối thiểu hóaChi phí khả thi nhỏ nhấtLower boundbound ≥ incumbent
Cận phải an toàn. Cận quá “bi quan” có thể cắt mất nghiệm tối ưu. Cận yếu vẫn đúng nhưng cắt được ít.

3. Mô phỏng Branch-and-Bound cho ba lô

level0
weight0
value0
upper bound0
incumbent0
if (upperBound(node) <= best) return;
try taking item[level];
try skipping item[level];
best = max(best, currentValue);
Cận dùng trong mô phỏng: cho phép lấy phân số món đồ còn lại theo tỉ lệ value/weight. Đây là upper bound của bài ba lô 0/1.

4. DFS B&B và Best-first B&B

Chiến lượcNode tiếp theoƯu điểmNhược điểm
DFS Branch-and-BoundNode sâu nhấtBộ nhớ nhỏ, dễ cài.Có thể tìm incumbent tốt muộn.
Best-first B&BNode có bound hứa hẹn nhấtThường tập trung vào nhánh tốt.Priority Queue có thể chứa rất nhiều node.
Branch orderingThử nhánh tốt trước trong DFSCả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.

IDDFS có bộ nhớ O(d) như DFS và tìm nghiệm nông như BFS khi chi phí cạnh bằng nhau
IDDFS duyệt lại các node gần gốc, nhưng trong cây phân nhánh lớn, phần lớn node nằm ở mức sâu nhất nên chi phí lặp lại thường chấp nhận được.

6. Mô phỏng IDDFS

depth limit0
currentA
visited0

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.

Khi phù hợp: không gian trạng thái lớn, A* tốn quá nhiều bộ nhớ, nhưng có heuristic tốt và đường đi tối ưu không quá sâu.

8. Phân tích thuật toán

Thuật toánThời gian điển hìnhBộ nhớĐiểm mạnh
DFS B&BHàm mũ trong worst-caseO(depth)Cận tốt có thể giảm mạnh số node.
Best-first B&BHàm mũ trong worst-caseCó thể hàm mũMở rộng node hứa hẹn trước.
DLSO(b^L)O(L)Chặn độ sâu.
IDDFSO(b^d)O(d)Tìm nghiệm nông, ít bộ nhớ.
IDA*Phụ thuộc heuristicO(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âu 1. Trong tối đa hóa, khi nào cắt node?
Câu 2. IDDFS tăng dần đại lượng nào?
Câu 3. IDA* dùng ngưỡng gì?

📘 Cơ bản

  1. DLS trên cây.
  2. IDDFS tìm node đích.
  3. Ba lô B&B nhỏ.

📙 Nâng cao

  1. TSP Branch-and-Bound.
  2. Best-first B&B.
  3. IDA* cho 8-puzzle.