Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Structures & Algorithms (DSA)  Module 20 – Randomisation and hard problems

Coping with NP-hard problems

Exact search made faster with meet in the middle and branch and bound, approximation algorithms with proven factors, and local search measured honestly.

  • Advanced
  • 35 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free core, Pro depth: 11 of 35 minutes free
Free: Three routes, and what each one guarantees · Exact search on 36 numbers: meet in the middle · Practise: the closest subset sum. With a pass: 3 in-depth parts and 3 interview answers, about 24 minutes.

What you will learn

  • Improve exponential searches with meet in the middle and branch and bound
  • Apply approximation algorithms with proven guarantees to vertex cover and set cover
  • Use heuristics and local search with honest expectations
  • State which guarantee an answer has, exact, within a factor or none

Before you start

Coping with an NP-hard problem means giving up one of three things, because no known algorithm is exact, fast on every input and general at the same time. Keep the exact answer and accept exponential time on small inputs: meet in the middle and branch and bound push that limit far past plain brute force. Keep the exact answer and the speed for the inputs you really have, when they have structure such as a tree, a DAG or small numbers. Or keep the speed and give up the optimum: an approximation algorithm proves that its answer is within a stated factor of the best, such as twice the smallest vertex cover, while a heuristic such as local search usually finds good answers and proves nothing. The worked example takes the first route, subset sum over 36 numbers, with 524,288 half-sums instead of 68,719,476,736 subsets. Whichever route you take, state the guarantee you have.

Three routes, and what each one guarantees

The first question is always whether n is small enough for exponential work: 2²⁰ is about a million, 2⁴⁰ about a trillion. The next is whether your inputs are a special case with a polynomial algorithm. Only then do you trade the optimum for speed, with or without a proof:

Three questions for an NP-hard problem, leading to exact search, a structure-specific algorithm, approximation or a heuristic.n small enough for2ⁿ or 2ⁿᐟ² steps?Exact search:the optimumyesnoSpecial structure:tree, DAG, bipartiteor small numbers?An algorithm forthat structure:the optimumyesnoMust the answer comewith a proven bound?Approximation:within a statedfactoryesnoHeuristic or local search:no guarantee; measure each answeragainst a lower bound

Choosing how to cope with an NP-hard problem

Text description of the diagram

The questions run down the left in order; each yes leads to the route on its right, and each no to the next question.

  1. Is n small enough for 2ⁿ or 2ⁿᐟ² steps? Yes: exact search, such as bitmask dynamic programming, meet in the middle or branch and bound, which returns the optimum.
  2. If not, do the inputs have special structure, such as a tree, a DAG, a bipartite graph or small numbers? Yes: an algorithm for that structure, which returns the optimum in polynomial or pseudo-polynomial time.
  3. If not, must the answer come with a proven bound? Yes: an approximation algorithm, whose answer is within a stated factor of the optimum.
  4. If not: a heuristic or local search, which guarantees nothing, so its answers are measured against a lower bound.
Route and what it promises When it fits
Exact search: the optimum, in exponential time at worst n up to about 20 to 50, or easy inputs
Approximation algorithm: within a proven factor of the optimum, in polynomial time A bound on the answer must be promised
Heuristic or local search: nothing proved in advance Large inputs, with each answer measured against a lower bound

The tools of this lesson, one sentence each:

  • Meet in the middle lists the subset sums of each half of the input and pairs them, so 2ⁿ work becomes about 2ⁿᐟ² for each half.
  • Branch and bound backtracks like a plain search but drops any branch whose optimistic bound cannot beat the best answer found so far.
  • Dynamic programming over subsets, such as the Held–Karp algorithm, solves the travelling salesperson problem exactly in O(2ⁿ n²) time.
  • The matching algorithm for vertex cover takes both ends of every edge still uncovered, and its cover is never more than twice the smallest.
  • Greedy set cover keeps taking the set that covers the most uncovered elements, and stays within about ln n + 1 times the optimum.
  • 2-opt removes two edges of a tour and reconnects the two pieces the shorter way, again and again, until no such move helps.
  • Integer programming states a problem as linear constraints on whole-number variables and leaves the search to a general solver.

Exact search on 36 numbers: meet in the middle

Subset sum asks for the subset of n numbers whose total comes closest to a target. Deciding whether some subset hits the target exactly is NP-complete, and the O(nW) table of dynamic programming helps only when the numbers are small: with 36 numbers near a billion, W, the target, is close to eight billion. Trying every subset costs 2ⁿ sums. Meet in the middle, from Ellis Horowitz and Sartaj Sahni, splits the numbers into two halves of n/2, lists the 2ⁿᐟ² subset sums of each half, and sorts one list. For every sum s of the other half, the best partner is the sorted sum closest to target − s, which a binary search finds. The program runs both methods at n = 20, where they must agree, and meet in the middle alone at n = 36:

Every subset against meet in the middle, at n = 20 and n = 36 Python · mitm/subset_sum.py
# Subset sum over 36 numbers near a billion: which subset sum comes closest to a target? Trying every subset means
# 2^36 sums. Meet in the middle lists the 2^18 sums of each half, sorts one list, and for every sum of the other
# half looks up the best partner with a binary search. At n = 20 both methods run, and they must agree.
import random
from bisect import bisect_left


def all_sums(items):
    """The sums of all 2^len(items) subsets; bit j of an index says whether item j is in that subset."""
    sums = [0]
    for x in items:
        sums += [s + x for s in sums]
    return sums


def better(s, best, target):
    """Closer to the target wins; on a tie, the smaller sum."""
    return best is None or (abs(s - target), s) < (abs(best - target), best)


def every_subset(nums, target):
    best = None
    for s in all_sums(nums):
        if better(s, best, target):
            best = s
    return best, 2 ** len(nums)


def meet_in_the_middle(nums, target):
    half = len(nums) // 2
    left, right = all_sums(nums[:half]), sorted(all_sums(nums[half:]))
    best = None
    for s in left:
        i = bisect_left(right, target - s)  # the first right sum that reaches the target with s
        for j in (i - 1, i):  # the best partner is just below or at that point
            if 0 <= j < len(right) and better(s + right[j], best, target):
                best = s + right[j]
    return best, len(left) + len(right)


rng = random.Random(36)
nums = [rng.randrange(100_000_000, 1_000_000_000) for _ in range(36)]
for n in (20, 36):
    part = nums[:n]
    target = sum(part) * 2 // 5
    print(f"n = {n}, target {target:,}")
    best, listed = meet_in_the_middle(part, target)
    print(f"  meet in the middle: {listed:,} half-sums listed, closest sum {best:,}, off by {abs(best - target):,}")
    if n <= 20:
        best, tried = every_subset(part, target)
        print(f"  every subset:       {tried:,} sums tried, closest sum {best:,}, off by {abs(best - target):,}")
    else:
        print(f"  every subset would need {2 ** n:,} sums")

Output

n = 20, target 4,122,734,372
  meet in the middle: 2,048 half-sums listed, closest sum 4,122,734,277, off by 95
  every subset:       1,048,576 sums tried, closest sum 4,122,734,277, off by 95
n = 36, target 7,773,804,694
  meet in the middle: 524,288 half-sums listed, closest sum 7,773,804,694, off by 0
  every subset would need 68,719,476,736 sums

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 subset_sum.py

At n = 20, every subset meant 1,048,576 sums, and meet in the middle listed 2,048 half-sums; both found the same closest sum, 95 below the target. At n = 36, meet in the middle listed 524,288 half-sums and hit the target exactly, where trying every subset would need 68,719,476,736 sums. The cost is O(2ⁿᐟ² n) time for the listing and the sort, and memory for 2ⁿᐟ² sums, which is what limits the method: about 40 to 50 numbers, against about 25 for every subset.

Practise: the closest subset sum

Write meet in the middle for the closest subset sum. The sample tests include 34 numbers near a trillion, too many for a search of every subset, and compare your answers with a slow search on 300 small lists.

Exercise · Medium · Python, JavaScript

The subset sum closest to a target, by meet in the middle

Write closest_subset_sum(nums, target) in closest.py (JavaScript: closestSubsetSum(nums, target) in closest.mjs). nums holds up to 40 positive whole numbers, each below 10¹², and target is a whole number of 0 or more. Return the sum of the subset of nums that comes closest to target. The empty subset counts, with sum 0. When two sums are equally close, return the smaller one.

Trying all 2ⁿ subsets is too slow for 34 numbers, which the sample tests include. Split the numbers into two halves, list the sums of all subsets of each half, sort one list, and for every sum of the other half find its best partner with a binary search. The sample tests also compare your answers with a slow search on 300 small random lists.

Python · Starter code · closest.py

from bisect import bisect_left


def closest_subset_sum(nums, target):
    """The sum of some subset of nums (the empty subset counts) closest to target; on a tie, the smaller sum."""
    # Write meet in the middle here.
    return 0
The sample tests · test_closest.py
import random
from itertools import combinations

from closest import closest_subset_sum

BIG = [
    493113784116, 132580534659, 352730161662, 133924420423, 528990724597, 172794715421, 474977204024, 740356142812,
    203744176332, 479134929061, 102622444117, 270838240370, 678758273429, 980839394515, 503989920585, 912844967050,
    875464764886, 168027315632, 528639733939, 284399052225, 442036977465, 517865774538, 333276218569, 594806016691,
    295572531361, 244342064118, 888440858074, 698684003136, 359518668886, 852686342057, 223760290864, 836934969835,
    184820004716, 588817237807,
]


def slow(nums, target):
    best = None
    for size in range(len(nums) + 1):
        for subset in combinations(nums, size):
            s = sum(subset)
            if best is None or (abs(s - target), s) < (abs(best - target), best):
                best = s
    return best


def test_small_cases():
    """small lists: exact hits, everything, nothing, and the empty list"""
    assert closest_subset_sum([3, 34, 4, 12, 5, 2], 9) == 9
    assert closest_subset_sum([3, 34, 4, 12, 5, 2], 100) == 60
    assert closest_subset_sum([1, 2, 4, 8, 16, 32, 64], 77) == 77
    assert closest_subset_sum([], 7) == 0
    assert closest_subset_sum([10], 6) == 10


def test_ties_go_to_the_smaller_sum():
    """when two sums are equally close, the smaller one wins"""
    assert closest_subset_sum([3, 34, 4, 12, 5, 2], 1) == 0
    assert closest_subset_sum([7, 9], 8) == 7
    assert closest_subset_sum([10], 5) == 0


def test_against_a_slow_search():
    """300 random lists of up to 12 numbers give the same answer as trying every subset"""
    rng = random.Random(5)
    for _ in range(300):
        nums = [rng.randint(1, 60) for _ in range(rng.randint(0, 12))]
        target = rng.randint(0, 400)
        assert closest_subset_sum(nums, target) == slow(nums, target), (nums, target)


def test_thirty_four_big_numbers():
    """34 numbers near a trillion: 2^34 subsets are too many, so this needs meet in the middle"""
    assert closest_subset_sum(BIG, 5360110952657) == 5360110952810
    assert closest_subset_sum(BIG, sum(BIG) + 5) == sum(BIG)
    assert closest_subset_sum(BIG, 0) == 0

JavaScript · Starter code · closest.mjs

/** The sum of some subset of nums (the empty subset counts) closest to target; on a tie, the smaller sum. */
export function closestSubsetSum(nums, target) {
  // Write meet in the middle here.
  return 0;
}
The sample tests · closest.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { closestSubsetSum } from './closest.mjs';

const BIG = [
  493113784116, 132580534659, 352730161662, 133924420423, 528990724597, 172794715421, 474977204024, 740356142812,
  203744176332, 479134929061, 102622444117, 270838240370, 678758273429, 980839394515, 503989920585, 912844967050,
  875464764886, 168027315632, 528639733939, 284399052225, 442036977465, 517865774538, 333276218569, 594806016691,
  295572531361, 244342064118, 888440858074, 698684003136, 359518668886, 852686342057, 223760290864, 836934969835,
  184820004716, 588817237807,
];

function slow(nums, target) {
  let best = null;
  for (let mask = 0; mask < 1 << nums.length; mask++) {
    let s = 0;
    for (let i = 0; i < nums.length; i++) if (mask & (1 << i)) s += nums[i];
    if (best === null || Math.abs(s - target) < Math.abs(best - target) || (Math.abs(s - target) === Math.abs(best - target) && s < best)) best = s;
  }
  return best;
}

test('small lists: exact hits, everything, nothing, and the empty list', () => {
  assert.equal(closestSubsetSum([3, 34, 4, 12, 5, 2], 9), 9);
  assert.equal(closestSubsetSum([3, 34, 4, 12, 5, 2], 100), 60);
  assert.equal(closestSubsetSum([1, 2, 4, 8, 16, 32, 64], 77), 77);
  assert.equal(closestSubsetSum([], 7), 0);
  assert.equal(closestSubsetSum([10], 6), 10);
});

test('when two sums are equally close, the smaller one wins', () => {
  assert.equal(closestSubsetSum([3, 34, 4, 12, 5, 2], 1), 0);
  assert.equal(closestSubsetSum([7, 9], 8), 7);
  assert.equal(closestSubsetSum([10], 5), 0);
});

test('300 random lists of up to 12 numbers give the same answer as trying every subset', () => {
  let seed = 5;
  const next = (n) => {
    seed = (seed * 48271) % 2147483647;
    return seed % n;
  };
  for (let k = 0; k < 300; k++) {
    const nums = Array.from({ length: next(13) }, () => 1 + next(60));
    const target = next(401);
    assert.equal(closestSubsetSum(nums, target), slow(nums, target), JSON.stringify([nums, target]));
  }
});

test('34 numbers near a trillion: 2^34 subsets are too many, so this needs meet in the middle', () => {
  const total = BIG.reduce((a, b) => a + b, 0);
  assert.equal(closestSubsetSum(BIG, 5360110952657), 5360110952810);
  assert.equal(closestSubsetSum(BIG, total + 5), total);
  assert.equal(closestSubsetSum(BIG, 0), 0);
});
A hint

For a left sum s, the best partner from the sorted right sums is next to the position where target - s would be inserted: check the right sum just before that position and the one at it (Python: bisect_left(right, target - s); JavaScript: write the binary search yourself). In JavaScript, sort numbers with (a, b) => a - b: the default sort compares them as text, so 10 would come before 9.

The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 You must find a subset of 40 numbers, each up to 10¹², whose sum is as close as possible to a target. Which exact method fits?

    Choose one answer.

    Show the answer to question 1

    Answer: Meet in the middle, with about 2 × 2²⁰ half-sums

    2²⁰ is about a million, so two lists of half-sums, a sort and a binary search per sum take seconds. Every subset is 2⁴⁰ ≈ 1.1 × 10¹² sums, far too many. A table indexed by sums up to 10¹³ is far too large, and greedy gives no guarantee at all.

  2. Question 2 of 5 How many half-sums does meet in the middle list for 36 numbers, both halves together?

    Type a number.

    Show the answer to question 2

    Answer: 524288 half-sums

    Each half has 18 numbers and 2¹⁸ = 262,144 subsets, so 2 × 262,144 = 524,288 sums in all, against 2³⁶ = 68,719,476,736 subsets for a search of every subset, as the lesson's run printed.

  3. Question 3 of 5 A branch-and-bound search maximises value. What must its bound promise for each branch?

    Choose one answer.

    Show the answer to question 3

    Answer: It is never below the best value the branch can still reach

    A branch is dropped when its bound is no better than the best answer found so far. That is safe only if the bound is optimistic: if it could be below what the branch really reaches, the search might drop the branch that holds the optimum.

  4. Question 4 of 5 An algorithm returns a vertex cover of 14 vertices and is guaranteed to be within a factor of 2 of optimal. What do you know about the smallest cover?

    Choose one answer.

    Show the answer to question 4

    Answer: It has at least 7 vertices and at most 14

    The guarantee says 14 ≤ 2 × optimum, so the optimum is at least 7, and the 14 found is itself a cover, so the optimum is at most 14. A factor-2 algorithm may return anything from the optimum up to twice it, never more.

  5. Question 5 of 5 2-opt stops with a tour that no single 2-opt move can shorten. What can you claim about it?

    Choose one answer.

    Show the answer to question 5

    Answer: It is a local optimum; how far it is from the best tour is unknown unless you compare it with a lower bound

    2-opt guarantees only that no single move helps, and different starting tours can stop at different local optima. Only a lower bound on the optimum turns the result into a checked claim: the tour is at most so many times the optimum.

Branch and bound measured, and the inputs where its bound fails Pro: opens with a Pro, Premium or Ultimate pass

Branch and bound is backtracking with a test that throws hopeless branches away early. This part counts what that test saves on 0/1 knapsack, proves that it never throws away the optimum, reproduces the inputs on which it stops helping, and compares the exact methods by what limits each one.

In this part:

  • Branch and bound on 0/1 knapsack, counted node by node
  • Why the fractional bound never drops the optimum
  • A failure reproduced: when every item has the same value per kilogram
  • Choosing an exact method by n and by the numbers

1 runnable example (Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Approximation with proofs: vertex cover and set cover, measured Pro: opens with a Pro, Premium or Ultimate pass

An approximation algorithm comes with a proof, and the proof is what you are paying for. This part proves the factor 2 of the matching algorithm, measures it against the exact optimum on 300 graphs, shows that a rule that looks better has no such promise, and runs greedy set cover against its bound.

In this part:

  • The matching algorithm, and the proof of its factor 2
  • Measured on 300 graphs against the exact optimum
  • A guarantee is not typical quality: greedy by degree
  • Greedy set cover within H(d), and the input that stretches it

2 runnable examples (Python) · about 3 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Local search with honest expectations: 2-opt on 200 points Pro: opens with a Pro, Premium or Ultimate pass

Local search solves most large instances in practice, and on its own it proves nothing. This part runs 2-opt on 200 points from 21 starting tours, adds a lower bound that turns its answer into a checked claim, and ends with the one-line statement of guarantee for every method of the lesson.

In this part:

  • 2-opt from 21 starting tours, measured
  • A lower bound that turns a heuristic answer into a checked one
  • Restarts, time limits and when to stop
  • Stating the guarantee you have, in one line
  • Integer programming: modelling instead of writing a search

1 runnable example (Python) · about 3 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

  1. Fresher to mid · Warm-up

    What is the difference between an approximation algorithm and a heuristic?

    An approximation algorithm runs in polynomial time and comes with a proof that its answer is within a stated factor of the optimum on every input: the matching algorithm for vertex cover never returns more than twice the smallest cover. A heuristic comes with no such proof. It may be excellent on typical inputs, as 2-opt usually is for tours, but some inputs can make it arbitrarily bad, and you learn how good an answer was only by comparing it with a lower bound or with the optimum. So the difference is the promise, not the quality you happen to observe: report a heuristic's answer with the bound you measured, and an approximation's with its factor.

  2. Mid to senior

    Given 40 positive integers up to 10¹² and a target, decide within a few seconds whether some subset sums exactly to the target. Which algorithm do you use, what does it cost, and what breaks first as n grows?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to staff

    A dispatch team wants the best delivery route through 150 stops every morning, computed in under 10 seconds on one machine. What do you build, and what do you promise them about the route?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A warehouse wants cameras at junctions so that every corridor is watched, using as few cameras as possible. The operations head wants a promise about how many cameras the plan might waste. What do you offer?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • An NP-hard problem forces a choice: exact but exponential, fast for a special structure, or fast with only an approximate or heuristic answer.
  • Meet in the middle turns 2ⁿ subsets into two lists of 2ⁿᐟ² sums: 524,288 half-sums instead of 68,719,476,736 subsets for 36 numbers.
  • Branch and bound, approximation algorithms and local search each come with a different kind of promise: the optimum, a factor, or nothing in advance.
  • State the guarantee with every answer: exact, within a proven factor, or measured against a lower bound.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.