For optimization (“marathon”) problems where no exact algorithm fits (AtCoder Heuristic Contest, Topcoder Marathon, Google Hash Code, partial-scoring tasks): search the solution space, mostly stochastically, and keep the best thing found.

current = initial_solution(); best = current
while time remains:
    candidate = perturb(current)
    if accept(candidate, current): current = candidate
    if score(current) > score(best): best = current

Three design decisions: the neighbourhood (what perturb does), the acceptance rule, and the evaluation (which must be fast, ideally incremental).

MethodAccepts
hill climbingonly improvements (accepting ties lets it drift across plateaus); steepest ascent evaluates all neighbours, first improvement takes the first improving random neighbour (usually faster)
random restartsimprovements; restart from a fresh random solution when stuck
simulated annealingimprovements always, worsenings with probability
iterated local searchhill-climb to a local optimum, apply a big random kick, hill-climb again, keep the better: remarkably strong for its simplicity, often beats plain annealing
tabu searchthe best neighbour, but forbids recently reversed moves
beam searchkeeps the best partial solutions per step
genetic algorithmspopulation, crossover, mutation
MCTStree search guided by rollout statistics: Game Search
threshold accepting, Great Delugedeterministic relatives: accept moves under a threshold instead of a probability
parallel tempering (replica exchange)several chains at different temperatures, periodically swapping states
adaptive coolingadjust the schedule from the observed acceptance rate
quantum annealingphysical analogue, tunnelling instead of thermal fluctuation

Simulated annealing is the default: short, robust, almost no tuning beyond the temperature schedule.

What actually matters (ranked by impact)

  1. Incremental evaluation. Computing in instead of rescoring in multiplies the iteration count by : nearly always the single biggest win.
  2. The neighbourhood. Moves must be small enough to evaluate cheaply but large enough to escape local optima; mixing several move types usually beats one.
  3. The time budget. Use the whole limit (about 90%, leave a margin); check the clock, not an iteration count (judges vary in speed), and only every 100–1000 iterations.
  4. The temperature schedule. Geometric cooling from (accepting about half of the worsenings initially) to (accepting almost none).
  5. The initial solution: a greedy start helps, but matters less than the above.
Problem typeMoves
permutation (TSP, scheduling)swap two, 2-opt (reverse a segment), Or-opt (move a segment)
assignmentreassign one item, swap two assignments
subset selectionadd, remove or swap one element
placement / layoutmove one object, swap two, rotate
graph partitionmove a vertex, swap a pair across the cut
continuous parametersGaussian perturbation with a shrinking radius
Escaping local optimaIdea
annealingaccept worsenings, decreasingly
random restartsstart over
perturbation / kicksiterated local search
tabu listforbid undoing recent moves
populationkeep diverse solutions

Checklist. Seed from the clock for submissions (fixed seeds are useful while debugging); track the best-ever solution separately (the current state may be worse when time runs out); make moves undoable in so rejection is free; precompute everything the evaluation needs; use a fast RNG (mt19937 or xorshift, never rand()); test several seeds (high variance means the search is not converging).

When heuristics are appropriate
the problem asks to maximize a score, partial / relative scoringyes
is large and the problem is NP-hard, long time limityes
the checker verifies validity, not optimalityyes
the problem has a unique correct answerno: a heuristic that is usually right is still WA

Code: generic annealing and TSP with 2-opt

// Generic simulated annealing (minimization) with geometric cooling per iteration.
template<class S, class Cost, class Neighbor>
S simulatedAnnealing(S cur, Cost cost, Neighbor neighbor, double T0, double rate, int iters, mt19937_64& rng) {
    S best = cur; double cc = cost(cur), bc = cc, T = T0;
    uniform_real_distribution<double> U(0, 1);
    for (int i = 0; i < iters && T > 1e-12; i++, T *= rate) {
        S nx = neighbor(cur, rng); double nc = cost(nx), d = nc - cc;
        if (d <= 0 || U(rng) < exp(-d / T)) { cur = nx; cc = nc; if (cc < bc) best = cur, bc = cc; }
    }
    return best;
}
 
double tourLength(const vector<vector<double>>& d, const vector<int>& t) {
    double s = 0; for (int i = 0; i < sz(t); i++) s += d[t[i]][t[(i + 1) % sz(t)]]; return s;
}
 
// 2-opt local search: reverse t[i+1..j] while it shortens the tour. Delta in O(1), application O(n).
void twoOpt(const vector<vector<double>>& d, vector<int>& t) {
    int n = sz(t); bool improved = true;
    while (improved) {
        improved = false;
        for (int i = 0; i + 1 < n; i++) for (int j = i + 2; j < n; j++) {
            int a = t[i], b = t[i + 1], c = t[j], e = t[(j + 1) % n];
            if (a == e) continue;                        // the two edges share a vertex
            double delta = d[a][c] + d[b][e] - d[a][b] - d[c][e];
            if (delta < -1e-12) { reverse(t.begin() + i + 1, t.begin() + j + 1); improved = true; }
        }
    }
}
 
// Time-based annealing for TSP with random 2-opt moves (the standard contest loop).
vector<int> annealTSP(const vector<vector<double>>& d, double seconds, double T0, double T1, mt19937_64& rng) {
    int n = sz(d); vector<int> t(n); iota(all(t), 0); shuffle(all(t), rng);
    double cur = tourLength(d, t), best = cur; vector<int> bt = t;
    auto start = chrono::steady_clock::now(); uniform_real_distribution<double> U(0, 1); double T = T0;
    for (long long it = 0;; it++) {
        if ((it & 255) == 0) {
            double el = chrono::duration<double>(chrono::steady_clock::now() - start).count() / seconds;
            if (el >= 1) break;
            T = T0 * pow(T1 / T0, el);                   // geometric cooling in time
        }
        int i = rng() % n, j = rng() % n; if (i > j) swap(i, j);
        if (j - i < 2 || (i == 0 && j == n - 1)) continue;
        int a = t[i], b = t[i + 1], c = t[j], e = t[(j + 1) % n];
        double delta = d[a][c] + d[b][e] - d[a][b] - d[c][e];
        if (delta <= 0 || U(rng) < exp(-delta / T)) {
            reverse(t.begin() + i + 1, t.begin() + j + 1); cur += delta;
            if (cur < best - 1e-12) best = cur, bt = t;
        }
    }
    return bt;
}