Skip to content

Commit

Permalink
bidirectional dijkstra and A*
Browse files Browse the repository at this point in the history
  • Loading branch information
JakubSchwenkbeck committed Feb 1, 2025
1 parent e6c9328 commit c897edf
Show file tree
Hide file tree
Showing 7 changed files with 395 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/main/scala/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@main
def main(): Unit = println("Hello World")
83 changes: 83 additions & 0 deletions src/main/scala/algorithms/dynamic/FileDiff.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package algorithms.dynamic

import algorithms.dynamic.LCS

/// Prints a git-style diff to transform file A into file B using LCS.
/// Shows context lines, added (`+`), and removed (`-`) lines.
///
/// @param a First file (original)
/// @param b Second file (modified)
def printGitDiff(a: Seq[String], b: Seq[String]): Unit = {
val (_, _, lcsStr) = LCS(a.mkString("\n").toArray, b.mkString("\n").toArray)
val lcsSet = lcsStr.split("\n").toSet

println("--- Original")
println("+++ Modified")

var i = 0
var j = 0

while (i < a.length || j < b.length) {
if (i < a.length && j < b.length && a(i) == b(j)) {
// Context line (unchanged)
println(s" ${a(i)}")
i += 1
j += 1
} else if (i < a.length && !lcsSet.contains(a(i))) {
// Removed line
println(s"- ${a(i)}")
i += 1
} else if (j < b.length && !lcsSet.contains(b(j))) {
// Added line
println(s"+ ${b(j)}")
j += 1
} else {
// Move to next match in LCS
i += 1
j += 1
}
}
}

@main def main(): Unit = {
// Longer C++ programs as strings
val cppProgram1 =
"""#include <iostream>
|using namespace std;
|
|void greet() {
| cout << "Hello, World!" << endl;
|}
|
|int main() {
| greet();
| return 0;
|}""".stripMargin

val cppProgram2 =
"""#include <iostream>
|#include <vector> // Added new header
|using namespace std;
|
|void greet() {
| cout << "Hello, Universe!" << endl; // Modified output
|}
|
|int main() {
| vector<int> nums = {1, 2, 3}; // Added new feature
| for (int num : nums) {
| cout << num << " ";
| }
| cout << endl;
|
| greet();
| return 0;
|}""".stripMargin

// Convert C++ programs into line sequences
val fileA = cppProgram1.split("\n").toSeq
val fileB = cppProgram2.split("\n").toSeq

// Compute and print the git diff-style output
printGitDiff(fileA, fileB)
}
36 changes: 36 additions & 0 deletions src/main/scala/algorithms/graph/Dijkstra.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,39 @@ def dijkstra[T](graph: Graph[T], start: T): Map[T, Int] = {
distances.toMap

}

/// Dijkstra's Algorithm (Targeted Version) - Now returning a Map instead of Option
/// Finds the shortest path from a starting node to a specific target node in a weighted graph
/// Uses a priority queue-based approach
/// Runs in O((V + E) log V) in the worst case

def dijkstra[T](graph: Graph[T], start: T, target: T): Map[T, Int] = {
val distances = mutable.Map[T, Int]().withDefaultValue(Int.MaxValue)
val visited = mutable.Set[T]()
val priorityQueue = PriorityQueue[(T, Int)]()(Ordering.by(-_._2))

distances(start) = 0
priorityQueue.enqueue((start, 0))
while (!priorityQueue.isEmpty) {
val (currentNode: T, currentDistance: Int) = priorityQueue.dequeue().get // unsafe getter for now
if (currentNode == target) {
return distances.toMap // Return the distances map if target is reached
}

if (!visited.contains(currentNode)) {
visited.add(currentNode)

for ((neighbor, weight) <- graph.getNeighbors(currentNode)) {
if (!visited.contains(neighbor)) {
val newDistance = currentDistance + weight
if (newDistance < distances(neighbor)) {
distances(neighbor) = newDistance.toInt
priorityQueue.enqueue((neighbor, newDistance.toInt))
}
}
}
}
}

distances.toMap // Return distances even if the target is unreachable
}
58 changes: 58 additions & 0 deletions src/main/scala/algorithms/graph/aStar.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package algorithms.graph

import datastructures.graph.Graph
import datastructures.basic.PriorityQueue
import scala.collection.mutable

/// A* Algorithm
/// Finds the shortest path from a starting node to a target node in a weighted graph.
/// Uses a heuristic function to guide the search towards the target node more efficiently.
/// Is proven to be the best algorithm
/// https://github.com/bb4/bb4-A-star/blob/master/scala-source/com/barrybecker4/search/AStarSearch.scala
/// Runs in O((V + E) log V) in the worst case, depending on the quality of the heuristic.

def aStar[T](graph: Graph[T], start: T, target: T, heuristic: T => Int): Option[Int] = {
// Check if start and target are the same, return 0 if true
if (start == target) return Some(0)

// Map to store the shortest known distance from the start node to each node
val gScores = mutable.Map[T, Int]().withDefaultValue(Int.MaxValue)
gScores(start) = 0

// Map to store the estimated distance from each node to the target
val fScores = mutable.Map[T, Int]().withDefaultValue(Int.MaxValue)
fScores(start) = heuristic(start)

// Set to keep track of visited nodes
val visited = mutable.Set[T]()

// Priority queue (min-heap) to select the node with the lowest f-score
val priorityQueue = PriorityQueue[(T, Int)]()(Ordering.by(-_._2))
priorityQueue.enqueue((start, fScores(start)))

// Process the queue until it is empty or the target is reached
while (!priorityQueue.isEmpty) {
val (currentNode, currentFScore) = priorityQueue.dequeue().get

// If we reach the target, return the cost to get here
if (currentNode == target) return Some(gScores(currentNode))

// If node has been visited, skip it
if (!visited.contains(currentNode)) {
visited.add(currentNode)

// For each neighbor of the current node, calculate the potential new g-score and f-score
for ((neighbor, weight) <- graph.getNeighbors(currentNode)) {
val tentativeGScore: Int = gScores(currentNode) + weight.toInt
if (tentativeGScore < gScores(neighbor)) {
gScores(neighbor) = tentativeGScore
fScores(neighbor) = tentativeGScore + heuristic(neighbor)
priorityQueue.enqueue((neighbor, fScores(neighbor)))
}
}
}
}

// Return None if the target is unreachable
None
}
70 changes: 70 additions & 0 deletions src/main/scala/algorithms/graph/biDirectionalDijkstra.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package algorithms.graph

import datastructures.graph.Graph
import datastructures.basic.PriorityQueue
import scala.collection.mutable

/// Bidirectional Dijkstra's Algorithm
/// Finds the shortest path between two nodes by running Dijkstra's algorithm from both the source and target simultaneously
/// This approach can be faster than standard Dijkstra when the target is known
/// Runs in O((V + E) log V) in the worst case, but may terminate earlier

def bidirectionalDijkstra[T](graph: Graph[T], start: T, target: T): Map[T, Int] = {
if (start == target) return Map(start -> 0)

val forwardDistances = mutable.Map[T, Int]().withDefaultValue(Int.MaxValue)
val backwardDistances = mutable.Map[T, Int]().withDefaultValue(Int.MaxValue)
val forwardVisited = mutable.Set[T]()
val backwardVisited = mutable.Set[T]()
val forwardQueue = PriorityQueue[(T, Int)]()(Ordering.by(-_._2))
val backwardQueue = PriorityQueue[(T, Int)]()(Ordering.by(-_._2))

forwardDistances(start) = 0
backwardDistances(target) = 0
forwardQueue.enqueue((start, 0))
backwardQueue.enqueue((target, 0))

def processQueue(
queue: PriorityQueue[(T, Int)],
distances: mutable.Map[T, Int],
visited: mutable.Set[T],
otherDistances: mutable.Map[T, Int]
): Option[Int] = {
queue.dequeue() match {
case Some((currentNode: T, currentDistance: Int)) =>
if (otherDistances.contains(currentNode)) return Some(currentDistance + otherDistances(currentNode))

if (!visited.contains(currentNode)) {
visited.add(currentNode)

for ((neighbor, weight) <- graph.getNeighbors(currentNode)) {
if (!visited.contains(neighbor)) {
val newDistance = currentDistance + weight
if (newDistance < distances(neighbor)) {
distances(neighbor) = newDistance.toInt
queue.enqueue((neighbor, newDistance.toInt))
}
}
}
}
case None =>
}
None
}

// While both queues are non-empty, continue processing
while (!forwardQueue.isEmpty && !backwardQueue.isEmpty) {
processQueue(forwardQueue, forwardDistances, forwardVisited, backwardDistances) match {
case Some(_) => // Found the shortest path, continue
case None =>
}
processQueue(backwardQueue, backwardDistances, backwardVisited, forwardDistances) match {
case Some(_) => // Found the shortest path, continue
case None =>
}
}

// Return the merged distances from both directions
// The final result will be the combination of forward and backward distances
forwardDistances.toMap ++ backwardDistances.toMap
}
70 changes: 70 additions & 0 deletions src/test/scala/algorithms/graph/AStarTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package algorithms.graph

import munit.FunSuite
import datastructures.graph.Graph

class AStarTest extends FunSuite {

// Example heuristic function (Manhattan Distance for grid-based problems)
def heuristic(node: String): Int = node match {
case "A" => 6
case "B" => 5
case "C" => 2
case "D" => 0 // Target node
case _ => Int.MaxValue
}

test("A* should find the shortest path in a small graph") {
val graph = new Graph[String](isDirected = true)
graph.addEdge("A", "B", 1)
graph.addEdge("A", "C", 4)
graph.addEdge("B", "C", 2)
graph.addEdge("B", "D", 5)
graph.addEdge("C", "D", 1)

val result = aStar(graph, "A", "D", heuristic)

assertEquals(result, Some(4)) // The shortest path from A to D is via B and C: A -> B -> C -> D (1 + 2 + 1)
}

test("A* should handle the case where the start is the same as the target") {
val graph = new Graph[String](isDirected = true)
graph.addEdge("A", "B", 1)

val result = aStar(graph, "A", "A", heuristic)

assertEquals(result, Some(0)) // If start and target are the same, the cost is 0
}

test("A* should return None if the target is unreachable") {
val graph = new Graph[String](isDirected = true)
graph.addEdge("A", "B", 1)
graph.addEdge("B", "C", 1)

val result = aStar(graph, "A", "D", heuristic)

assertEquals(result, None) // "D" is unreachable from "A"
}

test("A* should work with different heuristics") {
// Using a different heuristic function for this case
def alternativeHeuristic(node: String): Int = node match {
case "A" => 7
case "B" => 4
case "C" => 3
case "D" => 0
case _ => Int.MaxValue
}

val graph = new Graph[String](isDirected = true)
graph.addEdge("A", "B", 3)
graph.addEdge("A", "C")
graph.addEdge("B", "C", 7)
graph.addEdge("B", "D", 5)
graph.addEdge("C", "D", 2)

val result = aStar(graph, "A", "D", alternativeHeuristic)

assertEquals(result, Some(3)) // The shortest path from A to D is via C: A -> C -> D (1 + 2)
}
}
Loading

0 comments on commit c897edf

Please sign in to comment.