Metaheuristics for the Capacitated Vehicle Routing Problem (CVRP) in plain Java: two construction heuristics (nearest neighbour, Clarke & Wright savings), a Tabu Search, an Ant Colony System with local search and a hybrid of the two, all sharing one granular neighbourhood (relocate, Or-opt, swap, 2-opt, cross-exchange). Includes a reader for the TSPLIB / CVRPLIB instance format, support for the distance-constrained Golden / Li instances and irace scenarios for parameter tuning.
The Vehicle Routing Problem is a well known combinatorial optimization problem and a generalization of the travelling salesman problem: a fleet of vehicles based at a single depot has to deliver goods to a set of customers at minimal cost. Finding the optimal solution is NP-hard, so heuristics are used to approximate it. See https://en.wikipedia.org/wiki/Vehicle_routing_problem.
The variant solved here is the CVRP. Let G = (V, A) be a graph where V is the vertex set and A the arc set. One vertex is the depot, at which a fleet of identical vehicles of capacity Q is based; the other vertices are customers with a demand qi. Each arc (vi, vj) has a cost cij (here: the Euclidean distance). The CVRP consists in finding a set of routes such that:
- each route begins and ends at the depot;
- each customer is visited exactly once by exactly one route;
- the total demand of the customers assigned to each route does not exceed Q;
- (distance-constrained variant) the length of each route does not exceed a limit D;
- the total cost of the routes is minimized.
Requirements: JDK 21+ and Maven 3.6.3+. There are no runtime dependencies besides JCommander (command line parsing), and the build only needs Maven Central.
mvn package # compiles, runs the tests, builds target/vrp-2.0.0.jar
java -jar target/vrp-2.0.0.jar -alg tabu -i datasets/small/A-n32-k5.vrp --seed 1
java -jar target/vrp-2.0.0.jar -alg acs -i datasets/small/A-n32-k5.vrp --seed 1
java -jar target/vrp-2.0.0.jar -alg hybrid -i datasets/big/X-n101-k25.vrp --seed 1
java -jar target/vrp-2.0.0.jar --helpExample output:
Instance: A-n32-k5 (31 customers, capacity 100, at least 5 vehicles)
Algorithm: tabu
Seed: 1
Execution Time: 2296 ms
=========================================================
Vehicle 0: 0 -> 21 -> 31 -> 19 -> 17 -> 13 -> 7 -> 26 -> 0
Vehicle 1: 0 -> 24 -> 27 -> 0
Vehicle 2: 0 -> 30 -> 16 -> 1 -> 12 -> 0
...
Best Value: 784.0000
The Best Value: line is what the irace scripts parse; its format is stable.
| Option | Applies to | Default | Meaning |
|---|---|---|---|
-alg, --algorithm |
all | required | greedy, savings, tabu, acs or hybrid (ACS followed by tabu search) |
-i, --instance |
all | required | path to a .vrp file |
--seed |
all | random | random seed; the seed used is always printed, so any run can be reproduced |
--distances |
all | auto |
auto (rounded for the A/B/P/M/X sets, exact for Golden/Li), exact or rounded (see below) |
--granularity |
tabu, acs, hybrid | 30 | size of the candidate lists restricting the neighbourhood; 0 = complete neighbourhood |
--iterations |
tabu, acs, hybrid | 10000 / 100 | tabu, hybrid: consecutive non-improving iterations before stopping; acs: colony iterations |
--tabu |
tabu, hybrid | 5 | base tabu tenure in iterations (a random 0–4 is added) |
--penalty |
tabu, hybrid | 0.1 | frequency penalty factor λ of the long-term memory; 0 disables it |
--init |
tabu | savings |
initial solution: greedy or savings |
--alpha |
acs, hybrid | 1.0 | exponent of the pheromone trail τ |
--beta |
acs, hybrid | 2.0 | exponent of the visibility η = 1/d |
--rho |
acs, hybrid | 0.1 | global pheromone evaporation / deposit rate, in (0, 1) |
--omega |
acs, hybrid | 0.1 | local pheromone update rate, in [0, 1] |
--q0 |
acs, hybrid | 0.9 | probability of the greedy (exploitation) step, in [0, 1] |
--ants |
acs, hybrid | 10 | ants per iteration; 0 = one ant per customer |
--local-search |
acs, hybrid | full |
local search applied to every ant: none, two_opt or full |
--acs-iterations |
hybrid | 100 | colony iterations of the ACS phase |
Exit codes: 0 success, 1 the instance could not be read or a parameter value is invalid,
2 the command line could not be parsed.
VrpInstance instance = VRPLibReader.read(Path.of("datasets/small/A-n32-k5.vrp")); // DistanceMode.AUTO
CandidateLists candidates = new CandidateLists(instance, 30);
Solution tabu = new TabuSearchSolver(instance, 5, 10_000, 0.1, new Random(1), candidates).solve();
Solution acs = new AntColonySystemSolver(instance, AcsParameters.DEFAULT, new Random(1), candidates).solve();
Solution polished = new TabuSearchSolver(instance, 5, 10_000, 0.1, new Random(1), candidates).solveFrom(acs);
tabu.getCost(); // 784.0
tabu.getRoutes(); // [[21, 31, 19, 17, 13, 7, 26], [24, 27], [30, 16, 1, 12], ...] (customers only; the depot is node 0)
// the building blocks are reusable: a plain local-search descent from any feasible solution
List<List<Integer>> improved = new LocalSearch(instance, candidates).improve(instance, acs.getRoutes());Solution.of(instance, routes) validates that every customer is served exactly once and no route
exceeds the capacity or the maximum route length, so a solver can never return an infeasible
solution unnoticed.
src/main/java/ro/uaic/info/
VRPRunner.java command line entry point
VRPLibReader.java TSPLIB / CVRPLIB parser (both dialects, distance modes)
VrpInstance.java immutable problem data (demands, distances, capacity, route-length limit)
Solution.java routes + cost, feasibility checks, output formatting
Solver.java the interface every algorithm implements
greedy/GreedySolver.java nearest-neighbour construction heuristic
greedy/ClarkeWrightSolver.java Clarke & Wright parallel savings heuristic
tabu/TabuSearchSolver.java tabu search (short- and long-term memory, perturbation)
acs/AntColonySystemSolver.java ant colony system with candidate lists and local search
acs/AcsParameters.java ACS parameter record with validation
local/Neighbourhood.java granular relocate / Or-opt / swap / 2-opt / cross-exchange scanner
local/RouteState.java mutable working solution (routes, loads, lengths, position index)
local/Move.java move records carrying the arcs they create and destroy
local/LocalSearch.java best-improvement descent over the neighbourhood
local/CandidateLists.java k nearest neighbours of every node
local/TwoOpt.java stand-alone intra-route 2-opt (cheap ACS local search)
src/test/java/ JUnit 6 test suite (run with `mvn test`)
datasets/small/ Augerat A, B and P instances (16–101 nodes)
datasets/big/ Christofides M, Uchoa X, Golden and Li instances (101–1200 nodes)
tuning/{tabu,acs}/ irace scenarios
Instances come from CVRPLIB in the TSPLIB95 format. Two dialects are supported:
- the standard one (A, B, P, M, X sets): all nodes, depot included, are listed in
NODE_COORD_SECTIONand the depot is node 1; - the Golden / Li one:
EDGE_WEIGHT_TYPE: FUNCTIONwithEDGE_WEIGHT_FORMAT: EUC_2D, only the customers are listed and the depot is given as anx ypair inDEPOT_SECTION.
Internally the depot is always node 0 and customers are 1 .. n-1.
Distance rounding. TSPLIB defines EUC_2D distances as Euclidean distances rounded to the
nearest integer, and the best known values of the A, B, P, M and X sets use that convention. The
Golden and Li sets are evaluated with real-valued distances. --distances auto (the default) applies
the convention matching the dialect, so costs are directly comparable to CVRPLIB; exact and
rounded force one or the other.
Route length limits. Twenty of the Golden and Li instances declare a maximum route length
(DISTANCE). It is enforced by every construction heuristic, by every move of the neighbourhood
and by Solution.of, so the solvers handle the distance-constrained CVRP as well.
Greedy (nearest neighbour): starting at the depot, the vehicle repeatedly drives to the closest unserved customer that still fits. When nothing fits, the route is closed and a new vehicle starts. Its cost is the Cnn that initialises the pheromone trails of the ACS.
Clarke & Wright savings (-alg savings): every customer starts in its own route; merging the
routes ending in i and starting in j saves d(0,i) + d(0,j) − d(i,j). Merges are performed in
decreasing order of savings while capacity and route length allow. It is typically 10–15% better than
nearest neighbour and is the default starting point of the Tabu Search.
Both metaheuristics work on the same move set (local/Neighbourhood.java), evaluated in O(1) per
move from the arcs it changes:
- relocate / Or-opt: move a segment of 1–3 consecutive customers anywhere (including into a new empty route), in either orientation;
- swap: exchange two customers, within a route or across routes;
- 2-opt: reverse a segment of a route;
- cross-exchange (2-opt*): exchange the tails of two routes.
Capacity and route-length limits are checked for every move. The neighbourhood is granular
(Toth & Vigo): only moves that create at least one arc between a node and one of its
--granularity nearest neighbours are generated (plus every move touching the depot), which turns a
scan from O(n²) into O(n·k). --granularity 0 scans the complete neighbourhood.
Tabu search is a metaheuristic that drives a local search beyond local optima: at every step the best move of the neighbourhood is applied even if it worsens the solution, and recently reversed attributes are declared tabu (forbidden) for a number of iterations so that the search does not immediately come back.
Implementation (tabu/TabuSearchSolver.java):
- Start: the savings solution (
--init greedyfor nearest neighbour). - Neighbourhood: the shared granular neighbourhood above, scanned completely at every iteration.
- Short-term memory (tabu attributes): a moved customer may not be moved again; a customer that
left a route may not re-enter it; a broken arc may not be rebuilt. Tenure =
--tabu+ random(0..4) iterations. Because each iteration freezes two or three customers, short tenures (5–10) work best. - Aspiration: a tabu move is accepted when it yields a new best solution.
- Long-term memory: a worsening move is penalised in proportion to how often the customers it
moves have been moved already (frequency-based diversification, Taillard): a customer moved at the
average rate adds
--penalty× (average cost per customer) to the move's score. - Perturbation: after 50 consecutive non-improving iterations the search restarts from the best
solution kicked by
max(3, n/10)random moves, whose attributes are made tabu so the kick cannot be undone right away. The same kick is used when every move is tabu. - Stopping:
--iterationsconsecutive iterations without improving the best solution.
s ← savings solution; best ← s; tabu ← ∅; freq ← 0
while iterations without improvement < maxIterations
m ← argmin over N(s) of delta(m) + penalty(freq, m), skipping tabu moves unless they improve best
s ← s ⊕ m; mark the attributes reversed by m tabu for tenure iterations; freq[moved customers]++
if cost(s) < cost(best): best ← s
every 50 stagnating iterations: s ← perturb(best)
return best
Ant Colony Optimization is inspired by the foraging behaviour of real ants. Artificial ants build solutions step by step, guided by pheromone trails τ(i, j) that summarise how good arc (i, j) was in previous solutions, and by the visibility η(i, j) = 1 / d(i, j).
The implementation (acs/AntColonySystemSolver.java) follows the Ant Colony System of Dorigo &
Gambardella (1997), adapted to the CVRP as in Bullnheimer, Hartl & Strauss, Applying the Ant System
to the Vehicle Routing Problem:
- Construction. An ant starts at the depot and repeatedly chooses the next customer among the
unserved ones that still fit in the vehicle (capacity and route length); when nothing fits it
returns to the depot and starts a new route. The choice is restricted to the candidate list of the
current node (
--granularitynearest neighbours) and falls back to all feasible customers when no candidate is feasible. Ant k is forced to start with customer k (rotating across iterations). - Transition rule (pseudo-random proportional). With probability q0 the ant takes the arc maximising τα·ηβ (exploitation); otherwise it draws the next customer with probability proportional to τα·ηβ (biased exploration).
- Local update. Every traversed arc is immediately updated, τ ← (1 − ω)·τ + ω·τ0, which makes already used arcs less attractive to the next ants.
- Local search. Each ant's routes are improved before competing for the global update:
--local-search full(default) runs the complete descent over the shared neighbourhood,two_optonly uncrosses routes,nonekeeps the raw construction. The full descent is what makes the ACS competitive (it is the ACS + local search scheme of Dorigo & Gambardella). - Global update. After every iteration, τ ← (1 − ρ)·τ + ρ / Lbest on the arcs of the best solution found so far.
- Initialisation. τ0 = 1 / (n · Cnn) where Cnn is the cost of the
greedy solution; 10 ants (
--ants 0for one per customer);--iterationscolony iterations.
Runs are reproducible for a given --seed. With the full local search each ant costs a descent, so
on the largest instances reduce --iterations or use --local-search two_opt.
-alg hybrid runs the ACS for --acs-iterations iterations and then the Tabu Search from the
ACS's best solution. It combines the diversity of the colony with the intensification of the tabu
search and is the strongest option on large instances.
irace scenarios for both
metaheuristics live in tuning/tabu and tuning/acs. They train on datasets/small and read the
Best Value: line of the program output; the seed irace draws is passed to the program with
--seed, so every experiment is reproducible.
mvn package
cd tuning/tabu # or tuning/acs
irace # needs R with the irace package installedparameters.txt describes the search space (tenure, budget, penalty and granularity for the tabu
search; α, β, ρ, ω, q0, ants and iterations for the ACS), default.txt the current
defaults, scenario.txt the budget. The parameter values tuned for the original implementation
(tabu 7 / iterations 19 and alpha 1.95, beta 4.76, rho 0.66, omega 0.85, q0 0.48) are no longer
meaningful: the algorithms changed substantially in version 2.0 (see below) and --iterations now
counts non-improving iterations for the Tabu Search. Re-run irace to tune version 2.0.
Default parameters, --seed 1, --distances auto (rounded for A, B, P and X, exact for Golden), one
run per instance on one core of the CI container. "Best known" is the CVRPLIB value, so the gap is
directly comparable. Golden_1 is distance-constrained (route length ≤ 650). On the two Golden
instances the ACS ran 20 iterations (--iterations 20) and the hybrid's colony 10
(--acs-iterations 10); everything else used the defaults.
| Instance | Customers | Greedy | Savings | ACS | Tabu Search | Hybrid | Best known |
|---|---|---|---|---|---|---|---|
| A-n32-k5 | 31 | 1145 | 842 | 784 (3.2 s) | 784 (2.1 s) | 784 (4.9 s) | 784 |
| A-n60-k9 | 59 | 1837 | 1408 | 1354 (9.6 s) | 1354 (7.1 s) | 1354 (12.7 s) | 1354 |
| A-n80-k10 | 79 | 2348 | 1840 | 1775 (14.0 s) | 1780 (6.7 s) | 1766 (22.0 s) | 1763 |
| B-n78-k10 | 77 | 1758 | 1262 | 1229 (11.6 s) | 1221 (9.4 s) | 1223 (16.5 s) | 1221 |
| P-n101-k4 | 100 | 1021 | 746 | 681 (22.7 s) | 682 (9.1 s) | 681 (29.4 s) | 681 |
| X-n101-k25 | 100 | 41944 | 28986 | 27868 (25.2 s) | 27793 (18.3 s) | 27665 (33.3 s) | 27591 |
| Golden_1 | 240 | 7667.34 | 6113.89 | 5788.21 (33.1 s) | 5762.72 (59.4 s) | 5760.27 (73.8 s) | 5623.47 |
| Golden_20 | 420 | 2234.21 | 1964.38 | 1945.37 (67.7 s) | 1938.38 (35.6 s) | 1935.50 (67.6 s) | 1820.09 |
Timings in seconds. For comparison, the original implementation reported 822.86 / 1460.51 / 1833.87 / 1421.52 / 757.24 for the Tabu Search on the five small instances and 863.15 / 1629.55 / 2043.01 / 1479.05 / 883.51 for the ACS (real-valued distances), and 2502.39 / 2282.83 on Golden_20. Those numbers are not reproducible (the random generator was not seeded) and some of them were produced by code with the defects listed below; its Golden_20 figures were computed with customer 1 mistaken for the depot.
- With a rich neighbourhood, a proper memory structure and a few seconds of CPU, the Tabu Search finds the optimum of the small Augerat instances and stays within 1–2% of the best known values on 100-customer instances.
- The ACS lives or dies by its local search: construction alone is 15–20% off, construction + 2-opt 10–15%, construction + the full descent reaches the same quality as the Tabu Search at a higher cost per iteration. The hybrid (ACS, then Tabu) is the most robust option on large instances.
- Clarke & Wright is a far better starting point than nearest neighbour and costs nothing.
- Granular neighbourhoods (30 candidates) lose nothing measurable on these instances and make the larger ones tractable: the 420-customer Golden_20 is solved to 6% in under a minute.
- There is no guarantee of optimality; results vary with the seed. Report the seed with any result.
mvn test # 94 unit tests: parser (both dialects, distance modes, error cases), instance
# validation, solution feasibility (capacity and route length), construction
# heuristics, candidate lists, every move type of the neighbourhood (bookkeeping
# invariants on real instances), local search, tabu search, ACS, command line
mvn verify # tests + executable jarGitHub Actions (.github/workflows/ci.yml) builds the project on JDK 21, runs the tests and smoke-runs
every algorithm, including the hybrid on a distance-constrained Golden instance.
The implementation was reviewed, rewritten around an immutable VrpInstance / Solution model,
and covered with tests. Defects found in the original code:
- Tabu Search corrupted vehicle loads. The demand subtracted from the source route after a move was the demand of the last evaluated candidate, not of the moved customer, so capacity checks drifted and infeasible solutions could be produced (and reported as improvements).
- Tabu Search skipped most of its neighbourhood. A
breakinstead ofcontinueon a tabu move abandoned the rest of the target route. - Tabu Search could crash or add
Double.MAX_VALUEto the cost when every move was tabu, had no aspiration criterion, could never open a new route, and used an unseededRandom, so irace's seeds were ignored and runs were not reproducible. - ACS never reset the ant's load between iterations (
currentCapacityonly grew), so first routes became shorter and shorter; the ant placed at the depot built a tour starting0 -> 0. - Golden / Li instances were read with the wrong depot: the depot coordinates in
DEPOT_SECTIONwere ignored and customer 1 was used as the depot. - The ACS came from the unmaintained
jacoflibrary pinned to a JitPackmaster-SNAPSHOT(non-reproducible builds, one thread per ant, log4j 1.x). It has been reimplemented in this repository with the same rules; the project now depends only on Maven Central. - The greedy solver called
System.exitfrom library code; the instance file was parsed twice; the parser had dead code for pickup / time-window sections and no error reporting.
Build and tooling: standard Maven layout, Java 21, JCommander 3.0, JUnit 6, maven-shade executable
jar, GitHub Actions CI, --help, --seed, --distances, --ants, --local-search.
- One granular neighbourhood (relocate / Or-opt, swap, 2-opt, cross-exchange;
local/) with incremental O(1) move evaluation, shared by a plain local-search descent, the ACS and the Tabu Search. Candidate lists (--granularity) make each scan O(n·k). - Clarke & Wright savings construction (
-alg savings), now the Tabu Search's starting point. - ACS: candidate lists in the construction, full local search on each ant (
--local-search), 10 ants by default;-alg hybridchains ACS and Tabu Search. - Tabu Search: frequency-based long-term memory (
--penalty), kick instead of stopping when every move is tabu. - Maximum route length of the Golden / Li instances enforced in every heuristic, every move and in
Solution.of. --distances autoapplies the TSPLIB rounding convention for the A/B/P/M/X sets, so results are comparable to CVRPLIB out of the box.
- Faster descents for the ACS: first-improvement instead of best-improvement, or a "don't look bits" scheme, would cut the cost of the full local search several-fold and allow more ants.
- Parallel ants: the ants of one iteration are independent and could be built on several cores.
- Infeasible intermediate solutions with a self-adjusting capacity penalty (Gendreau, Hertz & Laporte) would let the Tabu Search cross infeasible regions on the tightest instances.
- Service times and time windows (VRPTW): the parser already skips those sections; the neighbourhood would need time-window feasibility checks.
- Multi-run statistics: a small driver that runs several seeds and reports mean / best / standard deviation would make comparisons more honest than the single-seed table above.
MIT, see LICENSE.

