<div class="hint">
<b>8 customers</b> (colored dots) are served by <b>3 trucks</b> from the depot (center).
Destroy some assignments and greedily repair them. Watch total distance drop.
</div>
<canvas id="canvas" width="300" height="300"></canvas>
<div class="stats">
<span>Total distance: <b id="dist">—</b></span>
<span>Step: <b id="step">0</b></span>
</div>
<div class="controls">
<label>Destroy size: <b id="kval">3</b>
<input type="range" id="kslider" min="1" max="6" value="3">
</label>
</div>
<div class="btns">
<button id="btnStep" type="button">Run one LNS step</button>
<button id="btnMany" type="button">Run 20 steps</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .5rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 10px;
background: #f5f8fb; margin-bottom: .5rem; max-width: 100%; }
.stats { font-size: .9rem; margin-bottom: .4rem; display: flex; gap: 1.2rem; }
.controls { font-size: .85rem; margin-bottom: .6rem; }
.controls input[type=range] { vertical-align: middle; margin-left: .4rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Large Neighborhood Search demo on a small VRP instance.
// 8 customers, 3 trucks, depot at center.
// Destroy: remove k customers at random from their trucks.
// Repair: greedily assign each removed customer to the nearest truck (by current last stop).
// Accept: if new total distance <= old, update solution.
const W = 300, H = 300;
const DEPOT = { x: 150, y: 150 };
const CUSTOMERS = [
{ x: 60, y: 70 },
{ x: 100, y: 50 },
{ x: 230, y: 80 },
{ x: 260, y: 140 },
{ x: 220, y: 240 },
{ x: 130, y: 260 },
{ x: 60, y: 210 },
{ x: 40, y: 140 }
];
const TRUCK_COLORS = ['#e63946', '#2a9d8f', '#e9c46a'];
const N = CUSTOMERS.length;
const T = 3;
// State: assignment[i] = truck index for customer i
let assignment = [];
let stepCount = 0;
function dist(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
// Total route distance: depot -> customers in truck order -> depot
function routeDistance(asgn) {
let total = 0;
for (let t = 0; t < T; t++) {
const stops = asgn.reduce((acc, tr, i) => { if (tr === t) acc.push(i); return acc; }, []);
if (stops.length === 0) continue;
let prev = DEPOT;
for (const i of stops) { total += dist(prev, CUSTOMERS[i]); prev = CUSTOMERS[i]; }
total += dist(prev, DEPOT);
}
return total;
}
function initAssignment() {
// Random initial assignment, ensuring each truck gets at least 1 customer
assignment = Array.from({ length: N }, (_, i) => i % T);
// shuffle
for (let i = N - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[assignment[i], assignment[j]] = [assignment[j], assignment[i]];
}
stepCount = 0;
}
function lnsStep(k) {
// Destroy: pick k distinct customers at random
const idx = Array.from({ length: N }, (_, i) => i);
for (let i = N - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [idx[i], idx[j]] = [idx[j], idx[i]]; }
const removed = idx.slice(0, k);
// Build tentative assignment without removed customers
const newAsgn = assignment.slice();
removed.forEach(i => { newAsgn[i] = -1; });
// Greedy repair: for each removed customer, assign to truck with nearest last stop
// Shuffle removed to avoid insertion-order bias
for (let i = removed.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[removed[i], removed[j]] = [removed[j], removed[i]];
}
for (const ci of removed) {
let bestTruck = 0, bestDist = Infinity;
for (let t = 0; t < T; t++) {
// Last stop in truck t under newAsgn
const stops = newAsgn.reduce((acc, tr, i) => { if (tr === t) acc.push(i); return acc; }, []);
const lastStop = stops.length > 0 ? CUSTOMERS[stops[stops.length - 1]] : DEPOT;
const d = dist(lastStop, CUSTOMERS[ci]) + dist(CUSTOMERS[ci], DEPOT);
if (d < bestDist) { bestDist = d; bestTruck = t; }
}
newAsgn[ci] = bestTruck;
}
// Accept if improvement (or accept degradation with small prob to escape traps)
const oldDist = routeDistance(assignment);
const newDist = routeDistance(newAsgn);
if (newDist <= oldDist * 1.05) {
assignment = newAsgn;
}
stepCount++;
}
function draw() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, W, H);
// Draw routes: for each truck, depot -> customers -> depot
for (let t = 0; t < T; t++) {
const stops = assignment.reduce((acc, tr, i) => { if (tr === t) acc.push(i); return acc; }, []);
if (stops.length === 0) continue;
ctx.beginPath();
ctx.strokeStyle = TRUCK_COLORS[t];
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
ctx.moveTo(DEPOT.x, DEPOT.y);
for (const i of stops) ctx.lineTo(CUSTOMERS[i].x, CUSTOMERS[i].y);
ctx.lineTo(DEPOT.x, DEPOT.y);
ctx.stroke();
ctx.setLineDash([]);
}
// Draw depot
ctx.beginPath();
ctx.arc(DEPOT.x, DEPOT.y, 8, 0, 2 * Math.PI);
ctx.fillStyle = '#1d3557';
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 9px system-ui';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('D', DEPOT.x, DEPOT.y);
// Draw customers
for (let i = 0; i < N; i++) {
const c = CUSTOMERS[i];
const t = assignment[i];
ctx.beginPath();
ctx.arc(c.x, c.y, 7, 0, 2 * Math.PI);
ctx.fillStyle = TRUCK_COLORS[t] || '#999';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 8px system-ui';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(i + 1, c.x, c.y);
}
// Update stats
const d = routeDistance(assignment);
document.getElementById('dist').textContent = fmt('%round% px', { round: Math.round(d) });
document.getElementById('step').textContent = stepCount;
}
function getK() { return parseInt(document.getElementById('kslider').value, 10); }
document.getElementById('kslider').addEventListener('input', function() {
document.getElementById('kval').textContent = this.value;
});
document.getElementById('btnStep').addEventListener('click', () => { lnsStep(getK()); draw(); });
document.getElementById('btnMany').addEventListener('click', () => { for (let i = 0; i < 20; i++) lnsStep(getK()); draw(); });
document.getElementById('btnReset').addEventListener('click', () => { initAssignment(); draw(); });
// Verify: routeDistance must return a finite positive number for a valid assignment
(function selfTest() {
const testAsgn = [0, 0, 1, 1, 2, 2, 0, 1];
const d = routeDistance(testAsgn);
if (!isFinite(d) || d <= 0) throw new Error(fmt('routeDistance selftest failed: %d%', { d: d }));
console.log('LNS selftest passed. Route distance for test assignment:', Math.round(d));
})();
initAssignment();
draw();
<div class="hint">
<b>8 customers</b> (colored dots) are served by <b>3 trucks</b> from the depot (center).
Destroy some assignments and greedily repair them. Watch total distance drop.
</div>
<canvas id="canvas" width="300" height="300"></canvas>
<div class="stats">
<span>Total distance: <b id="dist">—</b></span>
<span>Step: <b id="step">0</b></span>
</div>
<div class="controls">
<label>Destroy size: <b id="kval">3</b>
<input type="range" id="kslider" min="1" max="6" value="3">
</label>
</div>
<div class="btns">
<button id="btnStep" type="button">Run one LNS step</button>
<button id="btnMany" type="button">Run 20 steps</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .5rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 10px;
background: #f5f8fb; margin-bottom: .5rem; max-width: 100%; }
.stats { font-size: .9rem; margin-bottom: .4rem; display: flex; gap: 1.2rem; }
.controls { font-size: .85rem; margin-bottom: .6rem; }
.controls input[type=range] { vertical-align: middle; margin-left: .4rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Large Neighborhood Search demo on a small VRP instance.
// 8 customers, 3 trucks, depot at center.
// Destroy: remove k customers at random from their trucks.
// Repair: greedily assign each removed customer to the nearest truck (by current last stop).
// Accept: if new total distance <= old, update solution.
const W = 300, H = 300;
const DEPOT = { x: 150, y: 150 };
const CUSTOMERS = [
{ x: 60, y: 70 },
{ x: 100, y: 50 },
{ x: 230, y: 80 },
{ x: 260, y: 140 },
{ x: 220, y: 240 },
{ x: 130, y: 260 },
{ x: 60, y: 210 },
{ x: 40, y: 140 }
];
const TRUCK_COLORS = ['#e63946', '#2a9d8f', '#e9c46a'];
const N = CUSTOMERS.length;
const T = 3;
// State: assignment[i] = truck index for customer i
let assignment = [];
let stepCount = 0;
function dist(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
// Total route distance: depot -> customers in truck order -> depot
function routeDistance(asgn) {
let total = 0;
for (let t = 0; t < T; t++) {
const stops = asgn.reduce((acc, tr, i) => { if (tr === t) acc.push(i); return acc; }, []);
if (stops.length === 0) continue;
let prev = DEPOT;
for (const i of stops) { total += dist(prev, CUSTOMERS[i]); prev = CUSTOMERS[i]; }
total += dist(prev, DEPOT);
}
return total;
}
function initAssignment() {
// Random initial assignment, ensuring each truck gets at least 1 customer
assignment = Array.from({ length: N }, (_, i) => i % T);
// shuffle
for (let i = N - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[assignment[i], assignment[j]] = [assignment[j], assignment[i]];
}
stepCount = 0;
}
function lnsStep(k) {
// Destroy: pick k distinct customers at random
const idx = Array.from({ length: N }, (_, i) => i);
for (let i = N - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [idx[i], idx[j]] = [idx[j], idx[i]]; }
const removed = idx.slice(0, k);
// Build tentative assignment without removed customers
const newAsgn = assignment.slice();
removed.forEach(i => { newAsgn[i] = -1; });
// Greedy repair: for each removed customer, assign to truck with nearest last stop
// Shuffle removed to avoid insertion-order bias
for (let i = removed.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[removed[i], removed[j]] = [removed[j], removed[i]];
}
for (const ci of removed) {
let bestTruck = 0, bestDist = Infinity;
for (let t = 0; t < T; t++) {
// Last stop in truck t under newAsgn
const stops = newAsgn.reduce((acc, tr, i) => { if (tr === t) acc.push(i); return acc; }, []);
const lastStop = stops.length > 0 ? CUSTOMERS[stops[stops.length - 1]] : DEPOT;
const d = dist(lastStop, CUSTOMERS[ci]) + dist(CUSTOMERS[ci], DEPOT);
if (d < bestDist) { bestDist = d; bestTruck = t; }
}
newAsgn[ci] = bestTruck;
}
// Accept if improvement (or accept degradation with small prob to escape traps)
const oldDist = routeDistance(assignment);
const newDist = routeDistance(newAsgn);
if (newDist <= oldDist * 1.05) {
assignment = newAsgn;
}
stepCount++;
}
function draw() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, W, H);
// Draw routes: for each truck, depot -> customers -> depot
for (let t = 0; t < T; t++) {
const stops = assignment.reduce((acc, tr, i) => { if (tr === t) acc.push(i); return acc; }, []);
if (stops.length === 0) continue;
ctx.beginPath();
ctx.strokeStyle = TRUCK_COLORS[t];
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
ctx.moveTo(DEPOT.x, DEPOT.y);
for (const i of stops) ctx.lineTo(CUSTOMERS[i].x, CUSTOMERS[i].y);
ctx.lineTo(DEPOT.x, DEPOT.y);
ctx.stroke();
ctx.setLineDash([]);
}
// Draw depot
ctx.beginPath();
ctx.arc(DEPOT.x, DEPOT.y, 8, 0, 2 * Math.PI);
ctx.fillStyle = '#1d3557';
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 9px system-ui';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('D', DEPOT.x, DEPOT.y);
// Draw customers
for (let i = 0; i < N; i++) {
const c = CUSTOMERS[i];
const t = assignment[i];
ctx.beginPath();
ctx.arc(c.x, c.y, 7, 0, 2 * Math.PI);
ctx.fillStyle = TRUCK_COLORS[t] || '#999';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 8px system-ui';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(i + 1, c.x, c.y);
}
// Update stats
const d = routeDistance(assignment);
document.getElementById('dist').textContent = fmt('%round% px', { round: Math.round(d) });
document.getElementById('step').textContent = stepCount;
}
function getK() { return parseInt(document.getElementById('kslider').value, 10); }
document.getElementById('kslider').addEventListener('input', function() {
document.getElementById('kval').textContent = this.value;
});
document.getElementById('btnStep').addEventListener('click', () => { lnsStep(getK()); draw(); });
document.getElementById('btnMany').addEventListener('click', () => { for (let i = 0; i < 20; i++) lnsStep(getK()); draw(); });
document.getElementById('btnReset').addEventListener('click', () => { initAssignment(); draw(); });
// Verify: routeDistance must return a finite positive number for a valid assignment
(function selfTest() {
const testAsgn = [0, 0, 1, 1, 2, 2, 0, 1];
const d = routeDistance(testAsgn);
if (!isFinite(d) || d <= 0) throw new Error(fmt('routeDistance selftest failed: %d%', { d: d }));
console.log('LNS selftest passed. Route distance for test assignment:', Math.round(d));
})();
initAssignment();
draw();
Notice how the total distance drops with each accepted repair. A single step sometimes makes things worse (the algorithm can accept mild degradations to escape traps), but the trend is always downward. The destroy size slider controls how many customers are removed per step — too small and you can't escape local optima; too large and the repair problem becomes as hard as the original.
Comments
Loading comments...