DSA DSA In Depth
0%
lesson branch-and-boundoptimizationrelaxation

Branch and Bound: Best-First / DFS Relaxation Bounds for Combinatorial Optimization

7 min read Module 6.1: Recursion, Backtracking, Pruning & Greedy Exchange Arguments Updated Oct 23, 2026
📄 Cheat Sheet
Branch and Bound Combinatorial Optimization: FIFO, LIFO & Least-Cost Bounding
Abdul Bari Branch and Bound Combinatorial Optimization: FIFO, LIFO & Least-Cost Bounding Watch on YouTube ↗

Demonstrates LP relaxation bounds for 0/1 Knapsack and reduced cost matrices for TSP under Best-First (LC) and DFS Branch and Bound.

0:00 / --:--

1. Optimality Pruning via Admissible Relaxations

While backtracking targets feasibility (finding one or all valid configurations), Branch and Bound (B&B) targets combinatorial optimization: $\min_{x \in \mathcal{F}} f(x)$ (or $\max$). B&B maintains two quantities:

  1. The Incumbent ($z^*$): The objective value of the best feasible complete solution found so far (initialized to $+\infty$ for minimization, or seeded via a fast greedy heuristic).
  2. The Admissible Bound ($B(u)$): For any partial state (subproblem) $u$, we solve a relaxed problem $\mathcal{R}(u) \supseteq \mathcal{F}(u)$ that drops hard integrality or degree constraints so it can be solved in polynomial time.

Core Invariant — Admissibility & Pruning Condition

For a minimization problem, a lower bound function $LB(u)$ is admissible if $LB(u) \le \min_{x \in \mathcal{F}(u)} f(x)$ for every subtree $u$. Whenever $LB(u) \ge z^*$, no completion of $u$ can strictly improve upon the incumbent $z^*$, so $u$'s entire subtree can be pruned without loss of optimality. (For maximization, we use an admissible upper bound $UB(u) \ge \max_{x \in \mathcal{F}(u)} f(x)$ and prune when $UB(u) \le z^*$.)

2. Classic Relaxations & Search Strategies (DFS vs. Best-First)

  • 0/1 Knapsack Upper Bound (Dantzig's LP Relaxation / Fractional Knapsack): Sort items by value-to-weight density $v_i / w_i$ descending. At node $u$ (having decided items $0..i-1$ with remaining capacity $C_{rem}$ and current value $V_{curr}$), greedily pack whole items $i, i+1, \dots$ until an item $k$ does not fit, then add the fractional slice $C_{rem} \cdot (v_k / w_k)$. This takes $O(\log N)$ time with prefix sums + binary search (or $O(N)$ linearly) and gives the exact Linear Programming relaxation upper bound $UB(u)$.
  • Metric / General TSP Lower Bound:
    • Row/Column Matrix Reduction: Subtract the minimum of each uncovered row and column from the cost matrix; the sum of subtracted minimums is a valid lower bound on completing the tour.
    • Held-Karp 1-Tree Relaxation: Remove vertex $1$, compute a Minimum Spanning Tree (MST) on vertices $\{2..n\}$ plus the two cheapest edges incident to vertex $1$. Every Hamiltonian cycle is a 1-tree, so $\text{cost}(\text{1-Tree}) \le \text{OPT}_{\text{TSP}}$.
  • Best-First (Least-Cost) vs. Depth-First B&B:
    • Best-First B&B: Expands the active node with the best bound $B(u)$ using a priority queue. Expands the minimum number of nodes with $B(u) < \text{OPT}$, but requires $O(|\text{Tree}|)$ heap memory and only finds complete solutions near the end.
    • DFS + Greedy Seeding: Uses $O(d)$ stack memory, updates the incumbent $z^*$ rapidly by exploring the greedy branch first, and avoids priority queue allocations.
flowchart TD R["Root: W=0, V=0, UB=265
Incumbent z* = 235 (Greedy)"] --> L["Include Item 0 (w=10, v=100)
W=10, V=100, UB=265"] R --> R1["Exclude Item 0
W=0, V=0, UB=220"] R1 -. "PRUNED: UB (220) <= z* (235)" .-> Dead["Subtree Skipped"] L --> LL["Include Item 1...
Updates Incumbent z* = 250"]

Hardware Reality — Why Priority-Queue Best-First B&B Runs Out of RAM Before CPU

In textbook Best-First B&B (like $A^*$ on combinatorial trees), every unpruned frontier node must be stored simultaneously on the binary heap along with its state vector. At 10 million nodes/sec, a priority queue fills 16 GB of RAM in under 15 seconds and suffers severe TLB and L3 cache misses on every push/pop. Production MIP solvers (Gurobi, CPLEX) and tournament solvers therefore use Plunging DFS (or IDA*) seeded with a greedy initial incumbent: stack space stays $O(N)$ inside L1 cache, and with prefix-sum + std::upper_bound fractional relaxation, each node's LP bound is computed in $O(\log N)$ time.

3. Complexity Matrix

N-Queens Bitmask Pruning & Knuth Dancing Links (DLX)
O(1) Bitmask / Exact Cover

Step through O(1) bitmask attack propagation (cols, ld, rd) for N-Queens, Knuth Algorithm X circular doubly-linked cover/uncover, and Meet-in-the-Middle subset search.

Step 1 / 9
Custom Input:
Timeline

Invariant & State

Row 0 Queen at c=1 sets cols=0100₂, ld=1000₂, rd=0010₂. Row 1 avail = ~(cols|ld|rd) & 1111₂ = 0001₂ (only col 3 safe).
♛✕✕✕
cols Mask
0100₂
ld / rd Masks
1000₂ / 0010₂
Next Row Avail
0001₂ (c=3)
[Step 1] Placed Queen at (0, 1); bitwise OR prunes 3 of 4 columns on Row 1 in O(1).
B&B Strategy / Relaxation Bound Evaluation Cost Worst-Case Time Auxiliary Memory Primary Strength
0/1 Knapsack DFS + Prefix-Sum Fractional UB O(log N) O(2^N) O(N) Fits in L1 cache; solves random $N=1000$ in ms
0/1 Knapsack Best-First (Priority Queue) O(log N) O(2^N log(2^N)) O(2^N) Minimizes number of expanded nodes
TSP Row/Col Reduced Matrix B&B O(N^2) O(N! * N^2) O(N^3) (DFS) Works on asymmetric directed graphs (ATSP)
TSP 1-Tree MST Relaxation B&B O(N^2) O(N! * N^2) O(N^2) (DFS) Much tighter lower bound for symmetric TSP

4. Reference C++20 Implementation

#include <algorithm>
#include <cstdint>
#include <vector>

struct KnapsackItem {
    long long weight;
    long long value;
};

// Exact 0/1 Knapsack via DFS Branch-and-Bound with O(log N) Fractional LP Upper Bound
long long knapsack_branch_and_bound(std::vector<KnapsackItem> items, long long capacity) {
    // 1. Sort items by value/weight ratio descending (cross-multiply to avoid float imprecision)
    std::sort(items.begin(), items.end(), [](const KnapsackItem& a, const KnapsackItem& b) {
        return static_cast<__int128>(a.value) * b.weight > static_cast<__int128>(b.value) * a.weight;
    });

    const int n = static_cast<int>(items.size());
    std::vector<long long> pref_w(n + 1, 0), pref_v(n + 1, 0);
    for (int i = 0; i < n; ++i) {
        pref_w[i + 1] = pref_w[i] + items[i].weight;
        pref_v[i + 1] = pref_v[i] + items[i].value;
    }

    // O(log N) Dantzig Fractional Knapsack Upper Bound from index idx
    auto fractional_upper_bound = [&](int idx, long long curr_w, long long curr_v) -> double {
        long long rem_w = capacity - curr_w;
        long long target_pref_w = pref_w[idx] + rem_w;
        auto it = std::upper_bound(pref_w.begin() + idx, pref_w.end(), target_pref_w);
        int split = static_cast<int>(it - pref_w.begin()) - 1;

        double bound = static_cast<double>(curr_v + (pref_v[split] - pref_v[idx]));
        long long leftover_w = rem_w - (pref_w[split] - pref_w[idx]);
        if (split < n && leftover_w > 0) {
            bound += static_cast<double>(leftover_w) * items[split].value / items[split].weight;
        }
        return bound;
    };

    long long best_val = 0;

    auto dfs = [&](auto& self, int idx, long long curr_w, long long curr_v) -> void {
        if (curr_v > best_val) best_val = curr_v;
        if (idx == n) return;

        // Admissible Pruning: Since all values are integers, floor(UB) <= best_val cannot beat best_val
        if (static_cast<long long>(fractional_upper_bound(idx, curr_w, curr_v)) <= best_val) {
            return;
        }

        // Branch 1 (Greedy-First): Include items[idx] if it fits to raise best_val early
        if (curr_w + items[idx].weight <= capacity) {
            self(self, idx + 1, curr_w + items[idx].weight, curr_v + items[idx].value);
        }
        // Branch 2: Exclude items[idx] (re-check bound since best_val may have increased in Branch 1!)
        if (static_cast<long long>(fractional_upper_bound(idx + 1, curr_w, curr_v)) > best_val) {
            self(self, idx + 1, curr_w, curr_v);
        }
    };

    dfs(dfs, 0, 0, 0);
    return best_val;
}

Production Pitfall — Non-Admissible Bounds & Floating-Point Truncation

If your bound is even slightly inadmissible (e.g., underestimating the true upper bound in maximization due to float rounding or integer division v / w before multiplying), Branch and Bound will silently prune the subtree containing the true global optimum! When objectives are integers, compute the fractional slack in IEEE-754 double (53-bit mantissa) plus a small $\epsilon = 10^{-9}$ before taking floor(), or cross-multiply in 64/128-bit integers: $\text{leftover\_w} \cdot v_{\text{split}} \le (z^* - V_{\text{base}}) \cdot w_{\text{split}}$.

Check yourself Question 1 of 4
Concept Check Admissibility Condition for Branch and Bound recall

When solving a **maximization** problem using Branch and Bound with current best feasible solution value (incumbent) $z^*$, what property must the bounding function $UB(u)$ satisfy at node $u$ so that pruning whenever $UB(u) \le z^*$ is guaranteed to preserve global optimality?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

In production Mixed-Integer Programming (MIP) and combinatorial Branch-and-Bound solvers, why is Best-First Search (always expanding the node with the globally best relaxation bound via a priority queue) rarely used in pure form compared to Plunging DFS + Greedy Seeding?

Key Takeaways

Branch and Bound prunes any subtree whose admissible relaxation bound cannot strictly improve upon the current incumbent z*.
Always explore the greedy branch first in DFS Branch and Bound so the incumbent z* tightens immediately, maximizing pruning in sibling branches.
Dantzig's Fractional Knapsack relaxation combined with prefix sums and binary search computes an exact LP upper bound in O(log N) per node.
Re-evaluate the pruning condition against z* before entering the second branch, because the first branch may have improved z*.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue