Створення візуалізатора пошуку шляху
З нуля на чистому JavaScript: сітка на canvas, де ви можете малювати стіни, обирати точки старту/фінішу та спостерігати, як Дейкстра чи A* досліджують лабіринт комірка за коміркою. Дорогою ви реалізуєте чергу з пріоритетами на основі бінарної мінімальної купи, допустимі евристики та плавну покрокову анімацію.
- Масиви, об'єкти та класи в JavaScript
-
Основи Canvas 2D API (
fillRect,clearRect) - Базове розуміння графів (вузли + ребра)
Структура даних сітки та її рендеринг
Сітка — це плоский Uint8Array для ефективного
використання кешу. Кожна комірка: 0 = відкрита, 1 = стіна, 2 = старт,
3 = фініш, 4 = відвідана, 5 = шлях:
const COLS = 40, ROWS = 30;
const CELL = 20; // пікселів на комірку
const canvas = document.getElementById('grid');
const ctx = canvas.getContext('2d');
canvas.width = COLS * CELL;
canvas.height = ROWS * CELL;
const grid = new Uint8Array(COLS * ROWS); // плоский масив
const idx = (c, r) => r * COLS + c;
const COLORS = {
0: '#1a1a2e', // відкрита
1: '#334155', // стіна
2: '#22c55e', // старт
3: '#ef4444', // фініш
4: '#1e40af', // відвідана
5: '#facc15', // шлях
};
function render() {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
ctx.fillStyle = COLORS[grid[idx(c, r)]];
ctx.fillRect(c * CELL + 1, r * CELL + 1, CELL - 2, CELL - 2);
}
}
}
Черга з пріоритетами на бінарній мінімальній купі
І Дейкстрі, і A* потрібно ефективно діставати вузол з мінімальною вартістю. Бінарна мінімальна купа дає push/pop за O(log N):
class MinHeap {
constructor() { this.data = []; }
push(item) {
this.data.push(item);
this._bubbleUp(this.data.length - 1);
}
pop() {
const top = this.data[0];
const last = this.data.pop();
if (this.data.length) { this.data[0] = last; this._sinkDown(0); }
return top;
}
get size() { return this.data.length; }
_bubbleUp(i) {
while (i > 0) {
const parent = (i - 1) >> 1;
if (this.data[parent].f <= this.data[i].f) break;
[this.data[parent], this.data[i]] = [this.data[i], this.data[parent]];
i = parent;
}
}
_sinkDown(i) {
const n = this.data.length;
while (true) {
let smallest = i;
const l = 2*i+1, r = 2*i+2;
if (l < n && this.data[l].f < this.data[smallest].f) smallest = l;
if (r < n && this.data[r].f < this.data[smallest].f) smallest = r;
if (smallest === i) break;
[this.data[smallest], this.data[i]] = [this.data[i], this.data[smallest]];
i = smallest;
}
}
}
Алгоритм Дейкстри
Дейкстра гарантує найкоротший шлях на сітках з однаковою вартістю переходів. Він досліджує одночасно в усіх напрямках, наче вода, що розтікається назовні:
function dijkstra(startC, startR, endC, endR) {
const dist = new Float32Array(COLS * ROWS).fill(Infinity);
const prev = new Int32Array(COLS * ROWS).fill(-1);
const heap = new MinHeap();
const start = idx(startC, startR);
dist[start] = 0;
heap.push({ f: 0, i: start });
const steps = []; // для анімації
while (heap.size) {
const { i } = heap.pop();
if (i === idx(endC, endR)) break;
const c = i % COLS, r = Math.floor(i / COLS);
steps.push({ type: 'visit', i });
// Сусіди в 4 напрямках
for (const [dc, dr] of [[1,0],[-1,0],[0,1],[0,-1]]) {
const nc = c + dc, nr = r + dr;
if (nc < 0 || nc >= COLS || nr < 0 || nr >= ROWS) continue;
const ni = idx(nc, nr);
if (grid[ni] === 1) continue; // стіна
const newDist = dist[i] + 1;
if (newDist < dist[ni]) {
dist[ni] = newDist;
prev[ni] = i;
heap.push({ f: newDist, i: ni });
}
}
}
// Відновлюємо шлях
const path = [];
let cur = idx(endC, endR);
while (cur !== -1) { path.push(cur); cur = prev[cur]; }
return { steps, path: path.reverse() };
}
A* з манхеттенською евристикою
A* додає евристику, що скеровує розширення до цілі. На сітці без діагоналей манхеттенська відстань є допустимою (ніколи не завищує) та ефективною:
Дейкстра
- f = g (вартість від старту)
- Досліджує всі напрямки однаково
- Гарантований найкоротший шлях
- Повільніший на великих відкритих сітках
A*
- f = g + h (вартість + евристика)
- Зміщений у бік цілі
- Оптимальний, якщо h допустима
- Значно швидший на практиці
// Евристика манхеттенської відстані
function heuristic(c, r, endC, endR) {
return Math.abs(c - endC) + Math.abs(r - endR);
}
function aStar(startC, startR, endC, endR) {
const g = new Float32Array(COLS * ROWS).fill(Infinity);
const prev = new Int32Array(COLS * ROWS).fill(-1);
const heap = new MinHeap();
const steps = [];
const start = idx(startC, startR);
g[start] = 0;
heap.push({ f: heuristic(startC, startR, endC, endR), i: start });
while (heap.size) {
const { i } = heap.pop();
if (i === idx(endC, endR)) break;
const c = i % COLS, r = Math.floor(i / COLS);
steps.push({ type: 'visit', i });
for (const [dc, dr] of [[1,0],[-1,0],[0,1],[0,-1]]) {
const nc = c + dc, nr = r + dr;
if (nc < 0 || nc >= COLS || nr < 0 || nr >= ROWS) continue;
const ni = idx(nc, nr);
if (grid[ni] === 1) continue; // стіна
const ng = g[i] + 1;
if (ng < g[ni]) {
g[ni] = ng;
prev[ni] = i;
const h = heuristic(nc, nr, endC, endR);
heap.push({ f: ng + h, i: ni });
}
}
}
const path = [];
let cur = idx(endC, endR);
while (cur !== -1) { path.push(cur); cur = prev[cur]; }
return { steps, path: path.reverse() };
}
Покрокова анімація
function animate(steps, path, speed = 15) {
let stepIndex = 0;
function frame() {
// Обробляємо `speed` кроків за кадр
for (let s = 0; s < speed && stepIndex < steps.length; s++, stepIndex++) {
const { i } = steps[stepIndex];
if (grid[i] !== 2 && grid[i] !== 3) grid[i] = 4; // позначаємо як відвідану
}
render();
if (stepIndex < steps.length) {
requestAnimationFrame(frame);
} else {
// Малюємо шлях після завершення
for (const i of path) {
if (grid[i] !== 2 && grid[i] !== 3) grid[i] = 5;
}
render();
}
}
requestAnimationFrame(frame);
}
Взаємодія мишею — малювання стін
let painting = false;
let paintType = 1; // 1 = стіна, 0 = стерти
canvas.addEventListener('mousedown', e => {
painting = true;
paintType = e.button === 2 ? 0 : 1; // правий клік стирає
paint(e);
});
canvas.addEventListener('mousemove', e => { if (painting) paint(e); });
canvas.addEventListener('mouseup', () => painting = false);
canvas.addEventListener('contextmenu', e => e.preventDefault());
function paint(e) {
const rect = canvas.getBoundingClientRect();
const c = Math.floor((e.clientX - rect.left) / CELL);
const r = Math.floor((e.clientY - rect.top) / CELL);
if (c < 0 || c >= COLS || r < 0 || r >= ROWS) return;
const i = idx(c, r);
if (grid[i] === 2 || grid[i] === 3) return; // не перезаписуємо старт/фініш
grid[i] = paintType;
render();
}
Візуальне порівняння алгоритмів
Щоб запустити обидва й порівняти кількість відвіданих вузлів, запустіть кожен на копії сітки:
function runAndCompare() {
const startC = 1, startR = 1, endC = COLS-2, endR = ROWS-2;
const resD = dijkstra(startC, startR, endC, endR);
const resA = aStar(startC, startR, endC, endR);
console.log(`Дейкстра відвідано: ${resD.steps.length}, шлях: ${resD.path.length}`);
console.log(`A* відвідано: ${resA.steps.length}, шлях: ${resA.path.length}`);
// На відкритій сітці A* зазвичай відвідує на 60-80% менше вузлів, ніж Дейкстра
}
A* з манхеттенською евристикою на відкритих сітках зазвичай досліджує на 40-80% менше вузлів, ніж Дейкстра. Вони знаходять однаково оптимальні шляхи. У лабіринтах різниця зменшується, бо в обох випадках потрібне вимушене дослідження.