Skip to content

NP-Hard: TSP & Hamiltonian Path/Cycle — Practice Tasks

All tasks must be solved in Go, Java, and Python. Each task ships with a clear spec, constraints, hints, and evaluation criteria. Verify exact solvers against brute force on small n, and verify heuristics by their length never exceeding the construction they started from.


Beginner Tasks (5)

Task 1 — Brute-force TSP over all permutations

Problem. Given an n×n distance matrix (n ≤ 10), compute the shortest tour starting and ending at city 0 by trying every permutation of the other cities.

Input / Output. - Input: integer n, then n rows of n integers. - Output: the minimum tour length.

Constraints. - 1 ≤ n ≤ 10. - Weights non-negative, fit in 32-bit int.

Hint. Fix city 0 as start; permute cities 1..n-1; for each permutation sum consecutive distances plus the closing edge back to 0.

Evaluation criteria. - Correct minimum on all provided cases. - Used as the oracle to validate Held–Karp in Task 2.

Starter — Go.

package main

import "fmt"

func bruteTSP(d [][]int) int {
    n := len(d)
    perm := make([]int, 0, n-1)
    for i := 1; i < n; i++ {
        perm = append(perm, i)
    }
    best := 1 << 30
    var rec func(k int)
    rec = func(k int) {
        if k == len(perm) {
            cost, prev := 0, 0
            for _, c := range perm {
                cost += d[prev][c]
                prev = c
            }
            cost += d[prev][0]
            if cost < best {
                best = cost
            }
            return
        }
        for i := k; i < len(perm); i++ {
            perm[k], perm[i] = perm[i], perm[k]
            rec(k + 1)
            perm[k], perm[i] = perm[i], perm[k]
        }
    }
    rec(0)
    return best
}

func main() {
    d := [][]int{{0, 10, 15, 20}, {10, 0, 35, 25}, {15, 35, 0, 30}, {20, 25, 30, 0}}
    fmt.Println(bruteTSP(d)) // 80
}

Starter — Java.

public class Task1 {
    static int best;
    static int[][] d;
    static void rec(int[] perm, int k) {
        if (k == perm.length) {
            int cost = 0, prev = 0;
            for (int c : perm) { cost += d[prev][c]; prev = c; }
            cost += d[prev][0];
            best = Math.min(best, cost);
            return;
        }
        for (int i = k; i < perm.length; i++) {
            int t = perm[k]; perm[k] = perm[i]; perm[i] = t;
            rec(perm, k + 1);
            t = perm[k]; perm[k] = perm[i]; perm[i] = t;
        }
    }
    static int bruteTSP(int[][] dist) {
        d = dist; best = Integer.MAX_VALUE;
        int n = dist.length;
        int[] perm = new int[n - 1];
        for (int i = 1; i < n; i++) perm[i - 1] = i;
        rec(perm, 0);
        return best;
    }
    public static void main(String[] args) {
        System.out.println(bruteTSP(new int[][]{{0,10,15,20},{10,0,35,25},{15,35,0,30},{20,25,30,0}}));
    }
}

Starter — Python.

from itertools import permutations


def brute_tsp(d):
    n = len(d)
    best = float("inf")
    for perm in permutations(range(1, n)):
        tour = (0,) + perm
        cost = sum(d[tour[i]][tour[i + 1]] for i in range(n - 1)) + d[tour[-1]][0]
        best = min(best, cost)
    return best


print(brute_tsp([[0,10,15,20],[10,0,35,25],[15,35,0,30],[20,25,30,0]]))  # 80


Task 2 — Held–Karp shortest tour length

Problem. Solve TSP exactly with the bitmask DP for n ≤ 16. Return only the optimal tour length.

Constraints. - 1 ≤ n ≤ 16. - O(2ⁿ·n²) time.

Hint. dp[mask][i] = shortest path from 0 covering mask ending at i. Iterate masks ascending, skip masks without bit 0, close with + d[i][0].

Evaluation criteria. - Matches Task 1's brute force for every n ≤ 10. - Runs n = 16 within a second.

Starter — Python.

import math


def held_karp(d):
    n = len(d)
    INF = math.inf
    full = (1 << n) - 1
    dp = [[INF] * n for _ in range(1 << n)]
    dp[1][0] = 0
    for mask in range(1, full + 1):
        if not (mask & 1):
            continue
        for i in range(n):
            if dp[mask][i] == INF or not (mask & (1 << i)):
                continue
            for j in range(n):
                if mask & (1 << j):
                    continue
                nm = mask | (1 << j)
                dp[nm][j] = min(dp[nm][j], dp[mask][i] + d[i][j])
    return min(dp[full][i] + d[i][0] for i in range(1, n))

(Go and Java versions mirror interview.md Challenge 1.)


Task 3 — Hamiltonian path existence (small graph)

Problem. Given an undirected graph (n ≤ 12) as an edge list, decide whether a Hamiltonian path exists using backtracking (no DP yet — that's Task 8).

Constraints. - 1 ≤ n ≤ 12.

Hint. DFS from each start vertex, marking visited; succeed when all n vertices are on the path. Backtrack on dead ends.

Evaluation criteria. - Correct yes/no on connected and disconnected graphs. - Returns true for a path graph, false for a star with ≥ 4 leaves.

Starter — Python.

def has_ham_path(n, edges):
    adj = [set() for _ in range(n)]
    for a, b in edges:
        adj[a].add(b)
        adj[b].add(a)

    def dfs(v, visited, count):
        if count == n:
            return True
        for w in adj[v]:
            if w not in visited:
                visited.add(w)
                if dfs(w, visited, count + 1):
                    return True
                visited.discard(w)
        return False

    return any(dfs(s, {s}, 1) for s in range(n))


print(has_ham_path(4, [(0, 1), (1, 2), (2, 3)]))  # True (path)
print(has_ham_path(4, [(0, 1), (0, 2), (0, 3)]))  # False (star)

Starter — Go.

package main

import "fmt"

func hasHamPath(n int, edges [][2]int) bool {
    adj := make([][]int, n)
    for _, e := range edges {
        adj[e[0]] = append(adj[e[0]], e[1])
        adj[e[1]] = append(adj[e[1]], e[0])
    }
    visited := make([]bool, n)
    var dfs func(v, count int) bool
    dfs = func(v, count int) bool {
        if count == n {
            return true
        }
        for _, w := range adj[v] {
            if !visited[w] {
                visited[w] = true
                if dfs(w, count+1) {
                    return true
                }
                visited[w] = false
            }
        }
        return false
    }
    for s := 0; s < n; s++ {
        for i := range visited {
            visited[i] = false
        }
        visited[s] = true
        if dfs(s, 1) {
            return true
        }
    }
    return false
}

func main() {
    fmt.Println(hasHamPath(4, [][2]int{{0, 1}, {1, 2}, {2, 3}})) // true
}

Starter — Java.

import java.util.*;

public class Task3 {
    static List<Integer>[] adj;
    static boolean[] visited;
    static int n;
    static boolean dfs(int v, int count) {
        if (count == n) return true;
        for (int w : adj[v]) if (!visited[w]) {
            visited[w] = true;
            if (dfs(w, count + 1)) return true;
            visited[w] = false;
        }
        return false;
    }
    @SuppressWarnings("unchecked")
    static boolean hasHamPath(int nn, int[][] edges) {
        n = nn;
        adj = new List[n];
        for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();
        for (int[] e : edges) { adj[e[0]].add(e[1]); adj[e[1]].add(e[0]); }
        for (int s = 0; s < n; s++) {
            visited = new boolean[n];
            visited[s] = true;
            if (dfs(s, 1)) return true;
        }
        return false;
    }
    public static void main(String[] args) {
        System.out.println(hasHamPath(4, new int[][]{{0,1},{1,2},{2,3}})); // true
    }
}


Task 4 — Tour length and validation

Problem. Given a candidate tour (a permutation of 0..n-1) and a distance matrix, compute its length and validate it is a legal tour (each city exactly once).

Constraints. - 1 ≤ n ≤ 1000.

Hint. Validate by checking the sorted tour equals 0..n-1. Length sums consecutive edges plus the closing edge.

Evaluation criteria. - Rejects a tour with a repeated or missing city. - Correct length including the wrap-around edge.

Starter — Python.

def tour_length(tour, d):
    n = len(d)
    assert sorted(tour) == list(range(n)), "not a valid permutation"
    return sum(d[tour[i]][tour[(i + 1) % n]] for i in range(n))


d = [[0, 10, 15, 20], [10, 0, 35, 25], [15, 35, 0, 30], [20, 25, 30, 0]]
print(tour_length([0, 2, 3, 1], d))  # 80


Task 5 — Nearest-neighbor construction

Problem. Build a tour with the nearest-neighbor heuristic from a given start, returning the tour and its length.

Constraints. - 1 ≤ n ≤ 2000. - O(n²).

Hint. From the last city, scan all unvisited cities for the nearest; append; repeat; close the loop.

Evaluation criteria. - Visits every city exactly once. - Length ≥ optimal (it is a heuristic), and consistent across runs from the same start.

(Reference solution mirrors interview.md Challenge 3.)


Intermediate Tasks (5)

Task 6 — Held–Karp with tour reconstruction

Problem. Extend Task 2 to also return the optimal tour (not just its length) via a parent[mask][i] table.

Constraints. - n ≤ 16.

Hint. When relaxing dp[nm][j], record parent[nm][j] = i. After choosing the best closing city, walk parents backward, clearing the current bit, then reverse and append 0.

Evaluation criteria. - The reconstructed tour's length equals the DP length. - The tour is a valid permutation starting at 0.

Starter — Python.

import math


def held_karp_tour(d):
    n = len(d)
    INF = math.inf
    full = (1 << n) - 1
    dp = [[INF] * n for _ in range(1 << n)]
    par = [[-1] * n for _ in range(1 << n)]
    dp[1][0] = 0
    for mask in range(1, full + 1):
        if not (mask & 1):
            continue
        for i in range(n):
            if dp[mask][i] == INF or not (mask & (1 << i)):
                continue
            for j in range(n):
                if mask & (1 << j):
                    continue
                nm = mask | (1 << j)
                if dp[mask][i] + d[i][j] < dp[nm][j]:
                    dp[nm][j] = dp[mask][i] + d[i][j]
                    par[nm][j] = i
    best, last = INF, -1
    for i in range(1, n):
        if dp[full][i] + d[i][0] < best:
            best, last = dp[full][i] + d[i][0], i
    tour, mask, cur = [], full, last
    while cur != -1:
        tour.append(cur)
        p = par[mask][cur]
        mask ^= (1 << cur)
        cur = p
    tour.reverse()
    tour.append(0)
    return best, tour

(Go and Java mirror junior.md Code Examples.)


Task 7 — 2-opt local search to a local optimum

Problem. Given an initial tour and a distance function, apply improving 2-opt moves until none remains. Return the improved tour and its length.

Constraints. - 1 ≤ n ≤ 2000. - O(n²) per pass.

Hint. For each pair (i, k), compute gain = d(a,b)+d(c,d) − d(a,c) − d(b,d); if positive, reverse the segment i+1..k. Loop until a full pass finds no improvement.

Evaluation criteria. - Final length ≤ initial length, always. - On a tour with a crossing, the crossing is removed.

(Reference mirrors interview.md Challenge 4 / middle.md Example A.)


Task 8 — Hamiltonian path existence via bitmask DP

Problem. Decide Hamiltonian-path existence for n ≤ 20 using the boolean reachability DP reach[mask][i].

Constraints. - n ≤ 20. - O(2ⁿ·n²).

Hint. Seed reach[1<<i][i] = true for every i. Extend along real edges only. Answer: any reach[FULL][i].

Evaluation criteria. - Matches Task 3's backtracking on all n ≤ 12 cases. - Handles n = 20 within a second.

Starter — Go.

package main

import "fmt"

func hasHamPathDP(adj [][]bool) bool {
    n := len(adj)
    reach := make([][]bool, 1<<n)
    for m := range reach {
        reach[m] = make([]bool, n)
    }
    for i := 0; i < n; i++ {
        reach[1<<i][i] = true
    }
    for mask := 0; mask < (1 << n); mask++ {
        for i := 0; i < n; i++ {
            if !reach[mask][i] {
                continue
            }
            for j := 0; j < n; j++ {
                if mask&(1<<j) == 0 && adj[i][j] {
                    reach[mask|(1<<j)][j] = true
                }
            }
        }
    }
    full := (1 << n) - 1
    for i := 0; i < n; i++ {
        if reach[full][i] {
            return true
        }
    }
    return false
}

func main() {
    adj := [][]bool{{false, true, false}, {true, false, true}, {false, true, false}}
    fmt.Println(hasHamPathDP(adj)) // true
}

Starter — Python.

def has_ham_path_dp(adj):
    n = len(adj)
    reach = [[False] * n for _ in range(1 << n)]
    for i in range(n):
        reach[1 << i][i] = True
    for mask in range(1 << n):
        for i in range(n):
            if not reach[mask][i]:
                continue
            for j in range(n):
                if not (mask & (1 << j)) and adj[i][j]:
                    reach[mask | (1 << j)][j] = True
    full = (1 << n) - 1
    return any(reach[full][i] for i in range(n))


Task 9 — MST-doubling 2-approximation (metric)

Problem. Given points in the plane (Euclidean = metric), build a 2-approximate tour: MST (Prim), DFS preorder of the tree (a valid shortcutting of the doubled-tree Euler walk), then close. Return the tour length and verify it is ≤ 2× the Held–Karp optimum for small n.

Constraints. - n ≤ 12 for the verification against Held–Karp; the algorithm itself scales to large n.

Hint. A DFS preorder traversal of the MST visits each vertex once and is exactly the shortcut of the Euler tour of the doubled tree — no need to materialize the doubling.

Evaluation criteria. - Tour visits every city once. - For n ≤ 12, length ≤ 2 × held_karp(d).

Starter — Python.

import math


def mst_double_tour(pts):
    n = len(pts)
    d = lambda a, b: math.dist(pts[a], pts[b])
    # Prim's MST -> children adjacency
    in_tree = [False] * n
    parent = [-1] * n
    key = [math.inf] * n
    key[0] = 0
    for _ in range(n):
        u = min((i for i in range(n) if not in_tree[i]), key=lambda i: key[i])
        in_tree[u] = True
        for v in range(n):
            if not in_tree[v] and d(u, v) < key[v]:
                key[v] = d(u, v)
                parent[v] = u
    children = [[] for _ in range(n)]
    for v in range(1, n):
        children[parent[v]].append(v)
    # DFS preorder = shortcut of doubled-tree Euler walk
    tour = []

    def dfs(u):
        tour.append(u)
        for c in children[u]:
            dfs(c)

    dfs(0)
    length = sum(d(tour[i], tour[(i + 1) % n]) for i in range(n))
    return tour, length


pts = [(0, 0), (0, 3), (4, 3), (4, 0)]
print(mst_double_tour(pts))  # a 4-cycle around the rectangle

Starter — Go.

package main

import (
    "fmt"
    "math"
)

func mstDoubleTour(pts [][2]float64) ([]int, float64) {
    n := len(pts)
    d := func(a, b int) float64 { return math.Hypot(pts[a][0]-pts[b][0], pts[a][1]-pts[b][1]) }
    inTree := make([]bool, n)
    parent := make([]int, n)
    key := make([]float64, n)
    for i := range key {
        key[i] = math.Inf(1)
        parent[i] = -1
    }
    key[0] = 0
    for c := 0; c < n; c++ {
        u, bu := -1, math.Inf(1)
        for i := 0; i < n; i++ {
            if !inTree[i] && key[i] < bu {
                bu, u = key[i], i
            }
        }
        inTree[u] = true
        for v := 0; v < n; v++ {
            if !inTree[v] && d(u, v) < key[v] {
                key[v], parent[v] = d(u, v), u
            }
        }
    }
    children := make([][]int, n)
    for v := 1; v < n; v++ {
        children[parent[v]] = append(children[parent[v]], v)
    }
    tour := []int{}
    var dfs func(u int)
    dfs = func(u int) {
        tour = append(tour, u)
        for _, ch := range children[u] {
            dfs(ch)
        }
    }
    dfs(0)
    length := 0.0
    for i := 0; i < n; i++ {
        length += d(tour[i], tour[(i+1)%n])
    }
    return tour, length
}

func main() {
    fmt.Println(mstDoubleTour([][2]float64{{0, 0}, {0, 3}, {4, 3}, {4, 0}}))
}

Starter — Java.

import java.util.*;

public class Task9 {
    static double[][] pts;
    static double d(int a, int b) { return Math.hypot(pts[a][0]-pts[b][0], pts[a][1]-pts[b][1]); }
    static List<Integer> tour;
    static List<Integer>[] children;
    static void dfs(int u) {
        tour.add(u);
        for (int c : children[u]) dfs(c);
    }
    @SuppressWarnings("unchecked")
    static double mstDoubleTour(double[][] p) {
        pts = p; int n = p.length;
        boolean[] in = new boolean[n];
        int[] parent = new int[n];
        double[] key = new double[n];
        Arrays.fill(key, Double.MAX_VALUE); Arrays.fill(parent, -1); key[0] = 0;
        for (int c = 0; c < n; c++) {
            int u = -1; double bu = Double.MAX_VALUE;
            for (int i = 0; i < n; i++) if (!in[i] && key[i] < bu) { bu = key[i]; u = i; }
            in[u] = true;
            for (int v = 0; v < n; v++) if (!in[v] && d(u, v) < key[v]) { key[v] = d(u, v); parent[v] = u; }
        }
        children = new List[n];
        for (int i = 0; i < n; i++) children[i] = new ArrayList<>();
        for (int v = 1; v < n; v++) children[parent[v]].add(v);
        tour = new ArrayList<>();
        dfs(0);
        double len = 0;
        for (int i = 0; i < n; i++) len += d(tour.get(i), tour.get((i + 1) % n));
        return len;
    }
    public static void main(String[] args) {
        System.out.println(mstDoubleTour(new double[][]{{0,0},{0,3},{4,3},{4,0}}));
    }
}


Task 10 — Open vs fixed-endpoint shortest Hamiltonian path

Problem. Implement two Held–Karp variants: (a) shortest open path with any endpoints; (b) shortest path from a fixed start s to a fixed end t.

Constraints. - n ≤ 16.

Hint. (a) seed dp[1<<i][i]=0 for all i, answer min_i dp[FULL][i]. (b) seed only dp[1<<s][s]=0, answer dp[FULL][t].

Evaluation criteria. - (a) ≤ any specific tour minus its largest edge. - (b) equals brute force over permutations with fixed endpoints.

(Reference for (a) mirrors middle.md Dynamic Programming Integration.)


Advanced Tasks (5)

Task 11 — Branch-and-bound exact TSP

Problem. Solve TSP exactly with branch-and-bound, pruning partial tours whose lower bound (sum of cheapest remaining incident edges) exceeds the best complete tour. Seed the incumbent with a nearest-neighbor tour. Target n ≤ 30.

Constraints. - n ≤ 30 (typical instances).

Hint. Maintain bestSoFar from nearest-neighbor. At each node, lower bound = current path cost + ½·Σ(two cheapest incident edges of unvisited vertices). Prune when LB ≥ bestSoFar.

Evaluation criteria. - Matches Held–Karp on all n ≤ 16. - Solves n = 25–30 instances far faster than Held–Karp's memory allows.

Starter — Python.

import math


def branch_and_bound(d):
    n = len(d)
    # cheapest two incident edges per vertex for the bound
    cheapest2 = []
    for i in range(n):
        edges = sorted(d[i][j] for j in range(n) if j != i)
        cheapest2.append(edges[0] + (edges[1] if len(edges) > 1 else edges[0]))
    min_remaining = lambda unvisited: sum(cheapest2[v] for v in unvisited) / 2

    best = [math.inf]
    visited = [False] * n
    visited[0] = True

    def rec(cur, count, cost):
        if cost >= best[0]:
            return
        if count == n:
            best[0] = min(best[0], cost + d[cur][0])
            return
        unvisited = [v for v in range(n) if not visited[v]]
        if cost + min_remaining(unvisited) >= best[0]:
            return
        for v in sorted(unvisited, key=lambda x: d[cur][x]):
            visited[v] = True
            rec(v, count + 1, cost + d[cur][v])
            visited[v] = False

    rec(0, 1, 0)
    return best[0]


print(branch_and_bound([[0,10,15,20],[10,0,35,25],[15,35,0,30],[20,25,30,0]]))  # 80

Starter — Go.

package main

import (
    "fmt"
    "sort"
)

func branchAndBound(d [][]int) int {
    n := len(d)
    cheapest2 := make([]int, n)
    for i := 0; i < n; i++ {
        var e []int
        for j := 0; j < n; j++ {
            if j != i {
                e = append(e, d[i][j])
            }
        }
        sort.Ints(e)
        s := e[0]
        if len(e) > 1 {
            s += e[1]
        } else {
            s += e[0]
        }
        cheapest2[i] = s
    }
    best := 1 << 30
    visited := make([]bool, n)
    visited[0] = true
    var rec func(cur, count, cost int)
    rec = func(cur, count, cost int) {
        if cost >= best {
            return
        }
        if count == n {
            if cost+d[cur][0] < best {
                best = cost + d[cur][0]
            }
            return
        }
        lb := cost
        for v := 0; v < n; v++ {
            if !visited[v] {
                lb += cheapest2[v] / 2
            }
        }
        if lb >= best {
            return
        }
        for v := 0; v < n; v++ {
            if !visited[v] {
                visited[v] = true
                rec(v, count+1, cost+d[cur][v])
                visited[v] = false
            }
        }
    }
    rec(0, 1, 0)
    return best
}

func main() {
    fmt.Println(branchAndBound([][]int{{0, 10, 15, 20}, {10, 0, 35, 25}, {15, 35, 0, 30}, {20, 25, 30, 0}}))
}

Starter — Java.

import java.util.*;

public class Task11 {
    static int n, best;
    static int[][] d;
    static int[] cheapest2;
    static boolean[] visited;
    static void rec(int cur, int count, int cost) {
        if (cost >= best) return;
        if (count == n) { best = Math.min(best, cost + d[cur][0]); return; }
        int lb = cost;
        for (int v = 0; v < n; v++) if (!visited[v]) lb += cheapest2[v] / 2;
        if (lb >= best) return;
        for (int v = 0; v < n; v++) if (!visited[v]) {
            visited[v] = true;
            rec(v, count + 1, cost + d[cur][v]);
            visited[v] = false;
        }
    }
    static int solve(int[][] dist) {
        d = dist; n = dist.length; best = Integer.MAX_VALUE;
        cheapest2 = new int[n];
        for (int i = 0; i < n; i++) {
            List<Integer> e = new ArrayList<>();
            for (int j = 0; j < n; j++) if (j != i) e.add(d[i][j]);
            Collections.sort(e);
            cheapest2[i] = e.get(0) + (e.size() > 1 ? e.get(1) : e.get(0));
        }
        visited = new boolean[n]; visited[0] = true;
        rec(0, 1, 0);
        return best;
    }
    public static void main(String[] args) {
        System.out.println(solve(new int[][]{{0,10,15,20},{10,0,35,25},{15,35,0,30},{20,25,30,0}}));
    }
}


Task 12 — Christofides (metric) end-to-end

Problem. Implement Christofides: MST, identify odd-degree vertices, compute a minimum-weight perfect matching on them (a simple greedy matching is acceptable for this task — note it loses the 1.5× proof but still produces a valid tour), combine, Eulerian circuit, shortcut. Verify the tour length ≤ 1.5 × held_karp for small n only when an exact matching is used.

Constraints. - n ≤ 12 for verification.

Hint. Odd-degree count is even. With a true minimum matching (e.g. brute force on the small odd set) the 1.5× bound holds; greedy may slightly exceed it.

Evaluation criteria. - Produces a valid Hamiltonian cycle. - With exact matching, length ≤ 1.5 × held_karp(d) for n ≤ 10.

Hint (structure, Python pseudocode).

# 1. mst (Prim) -> tree edges
# 2. degree parity -> odd = [v for v if deg[v] odd]
# 3. min-weight perfect matching on `odd` (brute force for small |odd|)
# 4. multigraph = tree edges + matching edges
# 5. Eulerian circuit (Hierholzer) -> walk
# 6. shortcut walk (skip seen) -> tour


Task 13 — Or-opt move (relocate a chain)

Problem. Implement an Or-opt local search: try relocating chains of length 1, 2, and 3 to a better position. Combine with 2-opt for a stronger local optimum.

Constraints. - n ≤ 2000.

Hint. For each chain start i and length L, removing the chain saves d(prev,first)+d(last,next)−d(prev,next); inserting between a and b costs d(a,first)+d(last,b)−d(a,b). Apply if net gain > 0.

Evaluation criteria. - Final length ≤ the 2-opt-only result on the same seed. - Tour remains a valid permutation.


Task 14 — Asymmetric TSP with Held–Karp

Problem. Confirm Held–Karp solves asymmetric TSP (where d[i][j] ≠ d[j][i]) unchanged. Build a random asymmetric matrix and verify against brute force for n ≤ 9.

Constraints. - n ≤ 9 for brute-force verification.

Hint. No code change is needed — Held–Karp never assumes symmetry. The only subtlety: 2-opt and Christofides would not work here.

Evaluation criteria. - Held–Karp == brute force on random asymmetric matrices. - A note explaining why metric approximations do not apply.


Task 15 — Quality study: heuristics vs optimum

Problem. On random Euclidean instances (n = 10), measure the average ratio of nearest-neighbor, NN+2-opt, and MST-doubling tour lengths to the Held–Karp optimum.

Constraints. - n = 10, ≥ 100 random instances.

Hint. For each instance compute OPT (Held–Karp), then each heuristic's length; report mean(heuristic/OPT).

Evaluation criteria. - NN ratio typically ~1.2–1.3; NN+2-opt ~1.03–1.05; MST-doubling ~1.3–1.5 but always ≤ 2.0. - A short written interpretation of the results.

Starter — Python.

import math, random
from itertools import permutations


def opt(d):
    n = len(d)
    best = math.inf
    for p in permutations(range(1, n)):
        t = (0,) + p
        best = min(best, sum(d[t[i]][t[(i + 1) % n]] for i in range(n)))
    return best


random.seed(42)
ratios_nn = []
for _ in range(100):
    n = 10
    pts = [(random.random(), random.random()) for _ in range(n)]
    d = [[math.dist(pts[i], pts[j]) for j in range(n)] for i in range(n)]
    # nearest neighbor
    seen, tour, cur = [True] + [False] * (n - 1), [0], 0
    for _ in range(n - 1):
        nxt = min((j for j in range(n) if not seen[j]), key=lambda j: d[cur][j])
        seen[nxt] = True
        tour.append(nxt)
        cur = nxt
    nn_len = sum(d[tour[i]][tour[(i + 1) % n]] for i in range(n))
    ratios_nn.append(nn_len / opt(d))
print("avg NN / OPT:", round(sum(ratios_nn) / len(ratios_nn), 3))


Benchmark Task

Benchmark — Exact vs heuristic across growing n

Goal. Measure how Held–Karp, branch-and-bound, and NN+2-opt scale, and where exact methods become infeasible.

Method. 1. Generate random Euclidean instances for n ∈ {8, 10, 12, 14, 16, 18, 20}. 2. Time Held–Karp (exact), branch-and-bound (exact), and NN+2-opt (heuristic). 3. Record wall-clock time and, for the heuristic, the gap to the exact optimum (where computable). 4. Note the n at which Held–Karp's memory (2ⁿ·n ints) becomes a problem.

Expected shape of results.

n Held–Karp time B&B time NN+2-opt time 2-opt gap
8 <1 ms <1 ms <0.1 ms ~3%
12 ~5 ms ~2 ms <0.1 ms ~4%
16 ~150 ms ~10 ms <0.5 ms ~4%
20 ~3 s, ~160 MB ~100 ms–s ~1 ms ~5%
24 OOM varies ~2 ms

Starter — Python (timing harness).

import math, random, time
from itertools import permutations


def held_karp(d):
    n = len(d)
    INF = math.inf
    full = (1 << n) - 1
    dp = [[INF] * n for _ in range(1 << n)]
    dp[1][0] = 0
    for mask in range(1, full + 1):
        if not (mask & 1):
            continue
        for i in range(n):
            if dp[mask][i] == INF or not (mask & (1 << i)):
                continue
            for j in range(n):
                if mask & (1 << j):
                    continue
                nm = mask | (1 << j)
                dp[nm][j] = min(dp[nm][j], dp[mask][i] + d[i][j])
    return min(dp[full][i] + d[i][0] for i in range(1, n))


random.seed(7)
for n in (8, 10, 12, 14, 16):
    pts = [(random.random(), random.random()) for _ in range(n)]
    d = [[math.dist(pts[i], pts[j]) for j in range(n)] for i in range(n)]
    t0 = time.perf_counter()
    best = held_karp(d)
    dt = time.perf_counter() - t0
    print(f"n={n:2d}  held_karp={best:.3f}  time={dt*1000:.1f} ms")

Evaluation criteria. - Held–Karp and branch-and-bound agree on every n where both run. - The heuristic's gap stays small (~5%) even as exact methods become infeasible. - A written conclusion identifying the practical n ceiling for each method (brute force ~12, Held–Karp ~20, B&B ~40–60, heuristics unbounded).


Submission Checklist

  • Every task solved in Go, Java, and Python.
  • Exact solvers (Tasks 2, 6, 11) verified against brute force (Task 1) for n ≤ 10.
  • Heuristics (Tasks 5, 7, 9, 13) produce valid tours never longer than their seed/construction.
  • Path-vs-cycle and symmetric-vs-asymmetric conventions documented per task.
  • Benchmark table filled with your own measured numbers and a written conclusion.