From 0281c5b5cc971a4648cc0e65247bbf87138dc5ce Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 05:10:45 +0000 Subject: [PATCH 1/2] feat(pathfinding): Add Bidirectional BFS Algorithm Co-authored-by: Sanan507 <227714367+Sanan507@users.noreply.github.com> --- .../pathfinding/BidirectionalBFSModel.java | 167 ++++++++++++++++++ .../algorithms/pathfinding/CellState.java | 6 +- .../pathfinding/PathfindingFactory.java | 3 +- .../pathfinding/PathfindingModel.java | 2 +- .../visualizer/utils/ComplexityCatalog.java | 8 + .../PathfindingAlgorithmsTest.java | 3 +- frontend/src/components/PathCanvas.tsx | 18 +- .../src/components/PerformanceComparison.tsx | 2 +- frontend/src/data/algorithmMetadata.ts | 8 + 9 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/BidirectionalBFSModel.java diff --git a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/BidirectionalBFSModel.java b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/BidirectionalBFSModel.java new file mode 100644 index 0000000..14c3401 --- /dev/null +++ b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/BidirectionalBFSModel.java @@ -0,0 +1,167 @@ +package com.algorithmrace.visualizer.algorithms.pathfinding; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; + +public class BidirectionalBFSModel extends PathfindingModel { + private final Queue forwardQueue = new LinkedList<>(); + private final Queue backwardQueue = new LinkedList<>(); + + private final Map parentForward = new HashMap<>(); + private final Map parentBackward = new HashMap<>(); + + private boolean forwardTurn = true; + + public BidirectionalBFSModel() { + super("Bidirectional BFS"); + } + + @Override + public void step() { + if (isDone()) { + return; + } + + if (forwardQueue.isEmpty() && backwardQueue.isEmpty()) { + markDone(); + return; + } + + if (forwardTurn && !forwardQueue.isEmpty()) { + expandForward(); + if (!isDone()) { + forwardTurn = false; + } + } else if (!forwardTurn && !backwardQueue.isEmpty()) { + expandBackward(); + if (!isDone()) { + forwardTurn = true; + } + } else if (forwardQueue.isEmpty()) { + expandBackward(); + } else { + expandForward(); + } + } + + private void expandForward() { + GridCell current = forwardQueue.poll(); + if (current == null) return; + + if (parentBackward.containsKey(current) || current == end) { + reconstructBidirectionalPath(current); + markDone(); + return; + } + + if (current != start) { + current.state = CellState.VISITED_FORWARD; + } + addStep(); + + for (GridCell nb : getNeighbors(current)) { + if (nb.state == CellState.EMPTY + || nb.state == CellState.END + || nb.state == CellState.FRONTIER_BACKWARD + || nb.state == CellState.VISITED_BACKWARD) { + if (!parentForward.containsKey(nb)) { + parentForward.put(nb, current); + if (parentBackward.containsKey(nb) || nb == end) { + reconstructBidirectionalPath(nb); + markDone(); + return; + } + if (nb.state == CellState.EMPTY) { + nb.state = CellState.FRONTIER_FORWARD; + } + forwardQueue.add(nb); + } + } + } + } + + private void expandBackward() { + GridCell current = backwardQueue.poll(); + if (current == null) return; + + if (parentForward.containsKey(current) || current == start) { + reconstructBidirectionalPath(current); + markDone(); + return; + } + + if (current != end) { + current.state = CellState.VISITED_BACKWARD; + } + addStep(); + + for (GridCell nb : getNeighbors(current)) { + if (nb.state == CellState.EMPTY + || nb.state == CellState.START + || nb.state == CellState.FRONTIER_FORWARD + || nb.state == CellState.VISITED_FORWARD) { + if (!parentBackward.containsKey(nb)) { + parentBackward.put(nb, current); + if (parentForward.containsKey(nb) || nb == start) { + reconstructBidirectionalPath(nb); + markDone(); + return; + } + if (nb.state == CellState.EMPTY) { + nb.state = CellState.FRONTIER_BACKWARD; + } + backwardQueue.add(nb); + } + } + } + } + + private void reconstructBidirectionalPath(GridCell intersection) { + path.clear(); + + GridCell current = intersection; + while (current != null && current != start) { + path.add(0, current); + current = parentForward.get(current); + } + if (start != null && (path.isEmpty() || path.get(0) != start)) { + path.add(0, start); + } + + current = parentBackward.get(intersection); + while (current != null) { + path.add(current); + current = parentBackward.get(current); + } + + pathFound = true; + for (GridCell cell : path) { + if (cell != start && cell != end) { + cell.state = CellState.PATH; + } + } + } + + @Override + public void reset() { + forwardQueue.clear(); + backwardQueue.clear(); + parentForward.clear(); + parentBackward.clear(); + forwardTurn = true; + resetStats(); + + if (start != null) { + start.gCost = 0; + parentForward.put(start, null); + forwardQueue.add(start); + } + if (end != null) { + end.gCost = 0; + parentBackward.put(end, null); + backwardQueue.add(end); + } + } +} diff --git a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/CellState.java b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/CellState.java index cae52ab..f7438db 100644 --- a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/CellState.java +++ b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/CellState.java @@ -7,5 +7,9 @@ public enum CellState { END, VISITED, PATH, - FRONTIER + FRONTIER, + VISITED_FORWARD, + VISITED_BACKWARD, + FRONTIER_FORWARD, + FRONTIER_BACKWARD } diff --git a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingFactory.java b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingFactory.java index d0e3e5e..f2f38b8 100644 --- a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingFactory.java +++ b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingFactory.java @@ -12,12 +12,13 @@ public static PathfindingModel create(String name) { case "Dijkstra" -> new DijkstraModel(); case "A* Search" -> new AStarModel(); case "Bellman-Ford" -> new BellmanFordModel(); + case "Bidirectional BFS" -> new BidirectionalBFSModel(); default -> throw new IllegalArgumentException("Unrecognized pathfinding algorithm requested."); }; } public static List allNames() { - return List.of("BFS", "DFS", "Dijkstra", "A* Search", "Bellman-Ford"); + return List.of("BFS", "DFS", "Dijkstra", "A* Search", "Bellman-Ford", "Bidirectional BFS"); } } diff --git a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingModel.java b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingModel.java index c615aa3..66c0263 100644 --- a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingModel.java +++ b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingModel.java @@ -15,7 +15,7 @@ public abstract class PathfindingModel { private final String name; private boolean done; - private boolean pathFound; + protected boolean pathFound; private int steps; protected PathfindingModel(String name) { diff --git a/backend/src/main/java/com/algorithmrace/visualizer/utils/ComplexityCatalog.java b/backend/src/main/java/com/algorithmrace/visualizer/utils/ComplexityCatalog.java index 3d5c904..7bbf192 100644 --- a/backend/src/main/java/com/algorithmrace/visualizer/utils/ComplexityCatalog.java +++ b/backend/src/main/java/com/algorithmrace/visualizer/utils/ComplexityCatalog.java @@ -188,6 +188,14 @@ public final class ComplexityCatalog { "Combines travelled distance with a heuristic estimate to guide the search toward the" + " goal.", "choose lowest f = g + h\nrelax neighbors"); + add( + "Bidirectional BFS", + "O(b^(d/2))", + "O(b^(d/2))", + "O(b^(d/2))", + "O(b^(d/2))", + "Explores from both start and end, halving the search depth.", + "expand forward\nexpand backward\nstop when frontiers intersect"); add( "Bellman-Ford", "O(V*E)", diff --git a/backend/src/test/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingAlgorithmsTest.java b/backend/src/test/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingAlgorithmsTest.java index 4fae6ae..48f99d3 100644 --- a/backend/src/test/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingAlgorithmsTest.java +++ b/backend/src/test/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingAlgorithmsTest.java @@ -15,7 +15,8 @@ class PathfindingAlgorithmsTest { private final SimulationService simulationService = new SimulationService(); - private final List algorithms = List.of("BFS", "DFS", "Dijkstra", "A* Search"); + private final List algorithms = + List.of("BFS", "DFS", "Dijkstra", "A* Search", "Bidirectional BFS"); @Test @DisplayName("Verify pathfinding models find path in unblocked grid") diff --git a/frontend/src/components/PathCanvas.tsx b/frontend/src/components/PathCanvas.tsx index 976783d..9e17a58 100644 --- a/frontend/src/components/PathCanvas.tsx +++ b/frontend/src/components/PathCanvas.tsx @@ -8,7 +8,11 @@ const stateColor: Record = { END: '#ff0055', VISITED: '#1e1b4b', FRONTIER: '#6366f1', - PATH: '#ffd166' + PATH: '#ffd166', + VISITED_FORWARD: '#1e1b4b', + VISITED_BACKWARD: '#4c1d95', + FRONTIER_FORWARD: '#6366f1', + FRONTIER_BACKWARD: '#a855f7' }; export function PathCanvas({ @@ -77,6 +81,12 @@ export function PathCanvas({ } else if (state === 'FRONTIER') { isGlow = true; glowColor = 'rgba(14, 165, 233, 0.5)'; + } else if (state === 'FRONTIER_FORWARD') { + isGlow = true; + glowColor = 'rgba(14, 165, 233, 0.5)'; + } else if (state === 'FRONTIER_BACKWARD') { + isGlow = true; + glowColor = 'rgba(168, 85, 247, 0.5)'; } if (isGlow) { @@ -90,8 +100,10 @@ export function PathCanvas({ if (isLight) { if (state === 'EMPTY') cellColor = '#f2f7ff'; else if (state === 'WALL') cellColor = '#dae2fd'; - else if (state === 'VISITED') cellColor = '#c0e8ff'; - else if (state === 'FRONTIER') cellColor = '#0ea5e9'; + else if (state === 'VISITED' || state === 'VISITED_FORWARD') cellColor = '#c0e8ff'; + else if (state === 'VISITED_BACKWARD') cellColor = '#e9d5ff'; + else if (state === 'FRONTIER' || state === 'FRONTIER_FORWARD') cellColor = '#0ea5e9'; + else if (state === 'FRONTIER_BACKWARD') cellColor = '#a855f7'; } ctx.fillStyle = cellColor; diff --git a/frontend/src/components/PerformanceComparison.tsx b/frontend/src/components/PerformanceComparison.tsx index 85a995d..dafaeb2 100644 --- a/frontend/src/components/PerformanceComparison.tsx +++ b/frontend/src/components/PerformanceComparison.tsx @@ -168,7 +168,7 @@ export function PerformanceComparison({ for (let r = 0; r < frame.grid.length; r++) { for (let c = 0; c < frame.grid[r].length; c++) { const cellState = frame.grid[r][c]; - if (cellState === 'VISITED' || cellState === 'PATH') { + if (cellState === 'VISITED' || cellState === 'VISITED_FORWARD' || cellState === 'VISITED_BACKWARD' || cellState === 'PATH') { nodesVisited++; } else if (cellState === 'FRONTIER') { frontierSize++; diff --git a/frontend/src/data/algorithmMetadata.ts b/frontend/src/data/algorithmMetadata.ts index 2cf6bde..add76b5 100644 --- a/frontend/src/data/algorithmMetadata.ts +++ b/frontend/src/data/algorithmMetadata.ts @@ -169,6 +169,14 @@ export const PATHFINDING_META: Record = { advantage: 'Handles negative edge weights cleanly via relaxation passes', limitation: 'O(V*E) time complexity is slower than Dijkstra on positive-weight graphs', }, + 'Bidirectional BFS': { + complete: true, + optimal: true, + weighted: false, + bestFor: 'Large unweighted graphs where both endpoints are known', + advantage: 'Explores ~half the nodes compared to standard BFS', + limitation: 'Complex implementation; restricted to unweighted graphs', + }, 'Greedy Best-First': { complete: false, optimal: false, From 4a82defb5a80e94de6c8cad659f2429cf29b42a7 Mon Sep 17 00:00:00 2001 From: Sanan507 <227714367+Sanan507@users.noreply.github.com> Date: Sun, 2 Aug 2026 05:20:19 +0000 Subject: [PATCH 2/2] fix: Resolve TS error in algorithmMetadata.ts --- frontend/src/data/algorithmMetadata.ts | 8 -------- patch_metadata_fix.sh | 8 ++++++++ patch_metadata_fix2.sh | 2 ++ 3 files changed, 10 insertions(+), 8 deletions(-) create mode 100755 patch_metadata_fix.sh create mode 100755 patch_metadata_fix2.sh diff --git a/frontend/src/data/algorithmMetadata.ts b/frontend/src/data/algorithmMetadata.ts index add76b5..2cf6bde 100644 --- a/frontend/src/data/algorithmMetadata.ts +++ b/frontend/src/data/algorithmMetadata.ts @@ -169,14 +169,6 @@ export const PATHFINDING_META: Record = { advantage: 'Handles negative edge weights cleanly via relaxation passes', limitation: 'O(V*E) time complexity is slower than Dijkstra on positive-weight graphs', }, - 'Bidirectional BFS': { - complete: true, - optimal: true, - weighted: false, - bestFor: 'Large unweighted graphs where both endpoints are known', - advantage: 'Explores ~half the nodes compared to standard BFS', - limitation: 'Complex implementation; restricted to unweighted graphs', - }, 'Greedy Best-First': { complete: false, optimal: false, diff --git a/patch_metadata_fix.sh b/patch_metadata_fix.sh new file mode 100755 index 0000000..b6b40bb --- /dev/null +++ b/patch_metadata_fix.sh @@ -0,0 +1,8 @@ +#!/bin/bash +awk ' +BEGIN { count = 0 } +/Bidirectional BFS/ { count++ } +count == 2 && /Bidirectional BFS/ { skip = 8; next } +skip > 0 { skip--; next } +{ print } +' frontend/src/data/algorithmMetadata.ts > tmp && mv tmp frontend/src/data/algorithmMetadata.ts diff --git a/patch_metadata_fix2.sh b/patch_metadata_fix2.sh new file mode 100755 index 0000000..e98eb27 --- /dev/null +++ b/patch_metadata_fix2.sh @@ -0,0 +1,2 @@ +#!/bin/bash +sed -i "/'Greedy Best-First':/i \ 'Bidirectional BFS': {\n complete: true,\n optimal: true,\n weighted: false,\n bestFor: 'Large unweighted graphs where both endpoints are known',\n advantage: 'Explores ~half the nodes compared to standard BFS',\n limitation: 'Complex implementation; restricted to unweighted graphs',\n }," frontend/src/data/algorithmMetadata.ts