Local Search and Simulated Annealing › Formula

Michael BrenndoerferOctober 29, 202616 min read
Machine Learning from ScratchAll chapters
Part 15 of 15Heuristic and Evolutionary Optimization
Chapter 90Local Search and Simulated Annealing › Formula
Reading section 4 of 10 · Formula · 16 minSection 4 of 10 · 16 min

Section 4 of 10 · 16 min read

Local Search and Simulated Annealing › FormulaSection view · Sections in this chapter4 / 10 ↓
Reading tools Definitions, links & notes

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Notes & comments

There are four objects to define, and they fit together in a natural order:

  1. A representation says what a candidate solution is.
  2. An objective assigns a number to each candidate.
  3. A neighborhood says which small edits produce the candidates the search is allowed to see next.
  4. An acceptance rule decides whether to move to a proposed neighbor.

The order matters. The representation fixes the universe of possible answers; the objective ranks them; the neighborhood defines the local graph the search can traverse; and the acceptance rule determines whether the walk is greedy or stochastic. Each choice below is made with the same routing instance in mind, so that the formal definitions can be checked immediately against concrete numbers.

Solution representations and feasible neighborhoods

We encode a solution as a permutation π=(π1,π2,…,π8)\pi = (\pi_1, \pi_2, \dots, \pi_8) of the customer identifiers {1,…,8}\{1, \dots, 8\}. The decoder splits the permutation into two routes of four customers each, because the total demand is eight and each of the two vehicles has capacity four:

route1=(0,π1,π2,π3,π4,0),route2=(0,π5,π6,π7,π8,0),\text{route}_1 = (0, \pi_1, \pi_2, \pi_3, \pi_4, 0), \qquad \text{route}_2 = (0, \pi_5, \pi_6, \pi_7, \pi_8, 0),

where node 00 is the depot. Because each vehicle carries exactly four customers, every permutation of the eight customers automatically produces two capacity-feasible routes, so capacity alone never rules out a permutation. Time-window and depot-return feasibility are checked separately, and on this instance they are deliberately loose. Each customer has demand one, service takes one time unit, and all customer and depot windows are [0,80][0,80]. Travel time equals Euclidean distance in the chosen units. The coordinates fit in an 8-by-6 rectangle, so every arc is at most 10 units. Each four-customer route uses five arcs and four service periods, giving a return-time bound of 5(10)+4=54<805(10)+4=54<80. With window openings at zero, no waiting is needed; every valid permutation therefore satisfies these capacity and time limits. The loose windows isolate search behavior; this example does not test restrictive time-window handling.

The objective sums the Euclidean arc lengths over both closed tours:

f(π)=d(0,π1)+∑k=13d(πk,πk+1)+d(π4,0)+d(0,π5)+∑k=57d(πk,πk+1)+d(π8,0),\begin{aligned} f(\pi) &= d(0, \pi_1) + \sum_{k=1}^{3} d(\pi_k, \pi_{k+1}) + d(\pi_4, 0) \\ &\quad + d(0, \pi_5) + \sum_{k=5}^{7} d(\pi_k, \pi_{k+1}) + d(\pi_8, 0), \end{aligned}

where:

  • π\pi: a permutation of the eight customer identifiers
  • πk\pi_k: the customer visited at position kk in the permutation
  • d(a,b)d(a,b): the Euclidean distance (xa−xb)2+(ya−yb)2\sqrt{(x_a - x_b)^2 + (y_a - y_b)^2} between two coordinates
  • the first line traverses the first route's internal arcs (positions 11 to 44), and the two depot arcs d(0,π1)d(0, \pi_1) and d(π4,0)d(\pi_4, 0) close that tour
  • the second line traverses the second route's internal arcs (positions 55 to 88), and the two depot arcs d(0,π5)d(0, \pi_5) and d(π8,0)d(\pi_8, 0) close that tour

There is no lateness penalty and no service-time term in the objective; service times only constrain the schedule validation.

A representation by itself is not enough: the search needs a notion of "nearby." That is the neighborhood. Here the neighborhood is the set of all inclusive segment reversals. A move selects indices 0≤i<j<80 \le i < j < 8 and replaces the segment πi+1,…,πj+1\pi_{i+1}, \dots, \pi_{j+1} with its reverse. This move set is not arbitrary. A within-route reversal preserves the undirected internal-edge lengths and rewires two boundary arc positions. Reversing an entire route can leave its aggregate undirected edge multiset unchanged. A reversal crossing the split changes route assignments and route-labelled depot attachments, even when the aggregate depot-edge multiset is unchanged, so it must be scored through the full decoder here. When the reversed segment crosses the position-44 boundary, the move changes how customers are assigned to vehicles. Thus the neighborhood can reassign customers between routes.

This neighborhood has two properties we rely on. First, adjacent reversals (with j=i+1j = i+1) are simple swaps, and swaps alone connect the entire permutation space; the wider reversals only add shortcuts. Consequently the state graph is connected and every permutation is reachable from every other permutation. Second, the wider reversals are geometrically meaningful: reversing a contiguous piece of a route is the 2-opt move, which can uncross edges and shorten a tour. A neighborhood that only reversed within each vehicle would be disconnected across the route boundary and would forbid exactly the cross-split moves a routing search needs.

PythonIn [3]
CollapseExpand
import numpy as np

COORDS = np.array(
    [[0, 0], [1, 3], [2, 6], [4, 5], [7, 6], [8, 3], [6, 1], [3, 1], [5, 3]],
    dtype=float,
)
DEMANDS = np.array([0, 1, 1, 1, 1, 1, 1, 1, 1])
CAPACITY = 4
WINDOWS = [(0.0, 80.0)] * 9
SERVICE = np.array([0.0, 1, 1, 1, 1, 1, 1, 1, 1])

diff = COORDS[:, None, :] - COORDS[None, :, :]
D = np.sqrt((diff**2).sum(axis=-1))
PythonIn [4]
CollapseExpand
def tour_distance(perm):
    total = 0.0
    for route in (perm[:4], perm[4:]):
        prev = 0
        for c in route:
            total += D[prev, c]
            prev = c
        total += D[prev, 0]
    return total


def is_feasible(perm):
    values = np.asarray(perm)
    if (
        values.shape != (8,)
        or not np.issubdtype(values.dtype, np.integer)
        or set(values.tolist()) != set(range(1, 9))
    ):
        return False
    for route in (perm[:4], perm[4:]):
        load = 0
        t = 0.0
        prev = 0
        for c in route:
            load += DEMANDS[c]
            if load > CAPACITY:
                return False
            arrival = t + D[prev, c]
            start = max(arrival, WINDOWS[c][0])
            if start > WINDOWS[c][1]:
                return False
            t = start + SERVICE[c]
            prev = c
        if t + D[prev, 0] > WINDOWS[0][1]:
            return False
    return True


def objective(perm):
    return tour_distance(perm) if is_feasible(perm) else np.inf


def nearest_neighbor():
    remaining = list(range(1, 9))
    perm = []
    for _ in range(2):
        prev = 0
        for _ in range(4):
            best_c = min(remaining, key=lambda c: (D[prev, c], c))
            perm.append(best_c)
            remaining.remove(best_c)
            prev = best_c
    return tuple(perm)
PythonIn [5]
CollapseExpand
nn_perm = nearest_neighbor()
nn_length = objective(nn_perm)
ConsoleOut [6]
CollapseExpand
Nearest-neighbor permutation: (1, 7, 8, 3, 6, 5, 4, 2)
Nearest-neighbor tour length: 40.856347
Nearest-neighbor permutation distance summary: mean arc = 4.085635

The nearest-neighbor heuristic, run through the decoder and objective above, returns a warm-start tour of length described by the printed output above. Because the customer count is only eight, we can enumerate every permutation and obtain the true minimum, which we will use as an exact reference later.

PythonIn [7]
CollapseExpand
from itertools import permutations

best_exact, best_exact_perm = np.inf, None
for p in permutations(range(1, 9)):
    d = objective(p)
    if d < best_exact - 1e-15:
        best_exact, best_exact_perm = d, p
ConsoleOut [8]
CollapseExpand
Exact optimum:  37.35597706914
Achieved by:    (6, 5, 4, 8, 1, 2, 3, 7)
Nearest neighbor gap: 3.500370

Enumerating all 40,32040{,}320 permutations confirms the optimum is printed above, attained by the permutation printed above. This exhaustive reference search is outside the three budgeted heuristic runs, and we exclude it from every method's evaluation budget.

Hill climbing, local optima, and restarts

With the representation and neighborhood fixed, we can define an ideal steepest-descent rule: from the current permutation, choose a minimum-score neighbor and step only if it improves enough. The ideal strict-improvement rule is

πt+1=arg⁡min⁡π′∈N(πt)f(π′)providedmin⁡π′∈N(πt)f(π′)<f(πt)−ϵ,\pi_{t+1} = \arg\min_{\pi' \in \mathcal{N}(\pi_t)} f(\pi') \quad \text{provided} \quad \min_{\pi' \in \mathcal{N}(\pi_t)} f(\pi') < f(\pi_t) - \epsilon,

where:

  • πt\pi_t: the current permutation at iteration tt
  • N(πt)\mathcal{N}(\pi_t): the set of all segment reversals of πt\pi_t
  • ff: the objective (total tour length) defined above
  • ϵ>0\epsilon > 0: a small tolerance (here 10−1210^{-12}) to guard against floating-point noise. The tolerance is not decoration: two mathematically equal tour lengths may differ in their last bits, and without a margin round-off can cause unproductive moves between tours that are equal for all practical purposes. Deterministic strict descent still cannot cycle, because every accepted step lowers the fixed computed score. If no neighbor improves by more than ϵ\epsilon, the step is undefined: no improvement exceeding that tolerance is available in N\mathcal{N}. This is a tolerance-based stopping point, which need not be an exact mathematical local minimum.

The code below uses an ordered, tolerance-aware scan rather than an exact argmin. The first scored neighbor becomes the scan candidate; a later neighbor replaces it only when its score is lower by more than ϵ\epsilon. After a completed scan, the candidate is installed only if it improves the current score by more than ϵ\epsilon. Differences within ϵ\epsilon retain the earlier neighbor. The chosen candidate can therefore differ from the exact minimum; the final guard can even reject a scan when a later neighbor would have passed that guard. A budget-truncated scan returns the global best already scored, without installing a local step.

A local optimum is not a global one: there may exist a π∗\pi^\ast with f(π∗)<f(πt)f(\pi^\ast) < f(\pi_t) that is several reversals away. Restarts handle local stopping by abandoning the current epoch when the tolerance-aware scan refuses to step and beginning a fresh epoch from a new permutation. Within an epoch, every accepted step strictly lowers the current computed score. The scan stopping rule is not an exact local-optimality certificate. The restart initialization is usually a fresh random permutation, and the overall best seen across all epochs is what we report.

Two subtleties matter because they change reported comparisons. First, when we scan the full neighborhood to select a neighbor, every neighbor evaluation consumes budget, including the neighbors we reject and including equal-cost duplicates. When the budget runs out mid-scan, the honest thing to do is return the best permutation already scored, not to spend one extra evaluation to finish the scan. Second, the fixed budget must include the evaluations spent starting the epochs, not only the improvement steps. If restart-initialization evaluations are excluded, the reported count understates the true scoring cost and can make the method appear more efficient than it is.

Annealing acceptance and temperature schedules

Simulated annealing uses the same representation, objective, and neighborhood. It proposes one random reversal instead of scanning for the best neighbor, then applies a stochastic acceptance rule. From the current state xx with objective f(x)f(x), we propose a neighbor x′x' by sampling uniformly from the reversal neighborhood. We define the change in objective

Δ=f(x′)−f(x),\Delta = f(x') - f(x),

and accept the proposal with probability

Paccept={1Δ<0,exp⁡ ⁣(−ΔTk)Δ≥0,P_{\text{accept}} = \begin{cases} 1 & \Delta < 0, \\ \exp\!\left(-\dfrac{\Delta}{T_k}\right) & \Delta \ge 0, \end{cases}

where:

  • Δ\Delta: the change in objective, f(x′)−f(x)f(x') - f(x)
  • TkT_k: the temperature after kk evaluations (a positive scale that controls how freely uphill moves are accepted)

An improving move is always accepted; a worsening move is accepted with probability that falls as TkT_k shrinks. When TkT_k is large the search behaves like a random walk that pays little attention to the objective; as TkT_k approaches zero, the probability of accepting any fixed positive increase approaches zero. Equal-cost moves still have acceptance probability one, so this limit differs from strict-improvement hill climbing.

The exponential acceptance rule can be read directly as a family of curves: for a fixed worsening move, higher temperature means more exploration, while lower temperature rapidly suppresses uphill moves.

PythonIn [9]
CollapseExpand
T_GRID = np.geomspace(0.01, 5.0, 240)
DELTAS_TO_PLOT = np.array([0.1, 0.5, 1.0, 3.0])
ACCEPT_PROBS = np.exp(-DELTAS_TO_PLOT[:, None] / T_GRID[None, :])
VisualizationOut [10]
CollapseExpand
Four increasing curves against log temperature. The curve for Delta = 0.1 is highest and Delta = 3 is lowest.
Acceptance probability for four fixed worsening moves. For a fixed increase, acceptance rises with temperature; at any fixed temperature, a larger increase is less likely to be accepted.

The acceptance rule is not an arbitrary hack. It is the Metropolis-Hastings rule for the Boltzmann (Gibbs) target

pT(x)∝exp⁡ ⁣(−f(x)T).p_T(x) \propto \exp\!\left(-\frac{f(x)}{T}\right).

Here pT(x)p_T(x) assigns probability to a permutation xx at a fixed T>0T>0. The proportionality sign means dividing by Z(T)=∑yexp⁡(−f(y)/T)Z(T)=\sum_y\exp(-f(y)/T), with yy ranging over the finite permutation space. Every objective is finite on this deliberately feasible instance, so this normalizing sum is positive and finite.

To see why, note that if we propose x′x' symmetrically from xx (so q(x′∣x)=q(x∣x′)q(x' \mid x) = q(x \mid x')), then the ratio

pT(x′)pT(x)=exp⁡ ⁣(−f(x′)−f(x)T)=exp⁡ ⁣(−ΔT),\frac{p_T(x')}{p_T(x)} = \exp\!\left(-\frac{f(x') - f(x)}{T}\right) = \exp\!\left(-\frac{\Delta}{T}\right),

and the Metropolis rule accepts with probability min⁡(1,pT(x′)/pT(x))\min(1, p_T(x')/p_T(x)), which is exactly the formula above. This is why sampling two distinct indices, sorting them, and reversing the inclusive segment yields a symmetric proposal over all 2828 reversals: any reversal is as likely as its reverse.

The temperature must fall over time according to a cooling schedule. Here we choose geometric cooling:

Tk=T0(T1T0)k/(K−1),T_k = T_0 \left(\frac{T_1}{T_0}\right)^{k/(K-1)},

where:

  • T0T_0: the initial temperature (the hot start)
  • T1T_1: the final temperature (near-greedy for worsening moves large relative to T1T_1; ties and sufficiently small increases can still be accepted)
  • KK: the total evaluation budget
  • kk: the evaluation index, running from 00 to K−1K-1

For K≥2K\geq2 and 0<T1≤T00<T_1\leq T_0, this schedule has endpoints T0T_0 and T1T_1. Index zero is the initial nearest-neighbor score, where there is no acceptance test; the first proposal uses index one. Classical finite-state convergence results use schedules such as Tk=c/log⁡(k+2)T_k=c/\log(k+2) with a sufficiently large constant relative to the critical escape depth and suitable proposal conditions. Our finite geometric schedule has no certificate of global optimality by itself.

The schedule above has a simple shape that we can inspect before running the algorithm: most of the cooling happens early, and the final evaluations run at a near-greedy temperature.

PythonIn [11]
CollapseExpand
K_EVALS = 300
T0 = 2.0
T1 = 0.01
EVAL_INDEX = np.arange(K_EVALS)
COOLING_TEMPS = T0 * (T1 / T0) ** (EVAL_INDEX / (K_EVALS - 1))
VisualizationOut [12]
CollapseExpand
Temperature decreases along a straight line on a log vertical axis, from 2.0 at score one to 0.01 at score 300.
Geometric schedule across 300 score positions, from 2.0 to 0.01. Position one is the initial nearest-neighbor score, where no acceptance test occurs; proposals use the remaining 299 temperatures. The vertical axis is logarithmic.

The nearest-neighbor tour supplies the initial state. The initial temperature T0T_0 controls acceptance of worsening perturbations; it does not bound how far the current tour can deteriorate over several accepted moves. The separate incumbent retains the best tour already scored. A final temperature T1T_1 near zero favors polishing for worsening moves large relative to T1T_1. The temperature scale must be checked against actual objective changes; these two endpoint values are choices for this experiment.

Mathematical Properties

Several properties are worth recording so we do not overclaim what these algorithms achieve. First, the permutation graph under reversals is connected: any permutation can be transformed into any other by a finite sequence of inclusive segment reversals, because swaps (adjacent reversals) alone generate all permutations. This guarantees that with a sufficiently long search simulated annealing can in principle visit any solution, which a disconnected neighborhood would not allow.

Second, for a fixed temperature TT, the Metropolis chain with symmetric proposals has pTp_T as its stationary distribution. That gives simulated annealing a probabilistic meaning at a frozen temperature, where sufficiently equilibrated samples can approximate the Boltzmann distribution, but stationarity alone does not establish that a changing-temperature chain tracks those distributions. Decreasing temperature changes the transition rule and target distribution over time.

Third, under Hajek's conditions, a logarithmic cooling schedule that decreases slowly enough relative to the critical depth of the deepest nonglobal minimum makes the probability of being in the global-optimum set tend to one. This is an asymptotic statement under its hypotheses. It does not supply a finite-budget success probability for the schedule used here. In practice we measure quality empirically, across many seeds, and report the spread.

Finally, hill climbing with strict improvement terminates in finitely many steps: the objective strictly decreases by at least ϵ\epsilon at every accepted step, and the solution space is finite, so the loop cannot run forever. Hypothetically, independent uniform restart states followed by the same deterministic complete descent produce identically distributed completed-epoch outcomes. Our first epoch has a deterministic nearest-neighbor start, and a fixed budget can truncate the last epoch, so those outcomes do not all share that distribution. The best objective observed cannot increase as more candidates are scored.

The formulas now define a complete search process: a permutation is a solution, the objective ranks it, the reversal neighborhood supplies local moves, hill climbing accepts only strict improvements, and annealing accepts improvements always but worsening moves with a temperature-controlled probability. The next section makes the neighborhood visible, so we can see why these rules behave differently on the same eight-customer instance.

Key Parameters

The three algorithms have small parameter surfaces. Hill climbing has just two: the budget and the seed (and an implicit restart rule). Simulated annealing adds the initial and final temperatures T0T_0 and T1T_1, which set the geometric cooling curve. Random search adds nothing beyond a budget and a seed.

In our runs we hold the budget fixed at 300300 evaluations and run all three across the same 2020 seeds. Both hill climbing and annealing start from the nearest-neighbor solution, which costs one evaluation immediately. That common warm start controls initialization: different starts would confound the effect of the search with the choice of starting point.

For annealing, we choose T0=2.0T_0 = 2.0 and T1=0.01T_1 = 0.01. The initial temperature is chosen so that a typical +0.5+0.5 worsening move is accepted with e−0.25≈0.78e^{-0.25} \approx 0.78 probability early, but a +3+3 worsening move (an illustrative increase in these distance units) is only accepted with probability e−1.5≈0.22e^{-1.5} \approx 0.22. At a final temperature of T1=0.01T_1=0.01, a +0.5+0.5 increase has extremely small acceptance probability, although sufficiently small worsening moves can still be accepted. Finite positive temperature is not a strict-improvement rule.

Comments

Add to the conversation

A question, a correction, or an example from your own work.

No comments yet. Be the first to share your thoughts!

Newsletter

Stay up to date

Get new articles, book updates, and notes on AI and technology.

Unsubscribe at any time. Privacy policy