Skip to content

Commit c897edf

Browse files
bidirectional dijkstra and A*
1 parent e6c9328 commit c897edf

File tree

7 files changed

+395
-0
lines changed

7 files changed

+395
-0
lines changed

src/main/scala/Main.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@main
2+
def main(): Unit = println("Hello World")
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package algorithms.dynamic
2+
3+
import algorithms.dynamic.LCS
4+
5+
/// Prints a git-style diff to transform file A into file B using LCS.
6+
/// Shows context lines, added (`+`), and removed (`-`) lines.
7+
///
8+
/// @param a First file (original)
9+
/// @param b Second file (modified)
10+
def printGitDiff(a: Seq[String], b: Seq[String]): Unit = {
11+
val (_, _, lcsStr) = LCS(a.mkString("\n").toArray, b.mkString("\n").toArray)
12+
val lcsSet = lcsStr.split("\n").toSet
13+
14+
println("--- Original")
15+
println("+++ Modified")
16+
17+
var i = 0
18+
var j = 0
19+
20+
while (i < a.length || j < b.length) {
21+
if (i < a.length && j < b.length && a(i) == b(j)) {
22+
// Context line (unchanged)
23+
println(s" ${a(i)}")
24+
i += 1
25+
j += 1
26+
} else if (i < a.length && !lcsSet.contains(a(i))) {
27+
// Removed line
28+
println(s"- ${a(i)}")
29+
i += 1
30+
} else if (j < b.length && !lcsSet.contains(b(j))) {
31+
// Added line
32+
println(s"+ ${b(j)}")
33+
j += 1
34+
} else {
35+
// Move to next match in LCS
36+
i += 1
37+
j += 1
38+
}
39+
}
40+
}
41+
42+
@main def main(): Unit = {
43+
// Longer C++ programs as strings
44+
val cppProgram1 =
45+
"""#include <iostream>
46+
|using namespace std;
47+
|
48+
|void greet() {
49+
| cout << "Hello, World!" << endl;
50+
|}
51+
|
52+
|int main() {
53+
| greet();
54+
| return 0;
55+
|}""".stripMargin
56+
57+
val cppProgram2 =
58+
"""#include <iostream>
59+
|#include <vector> // Added new header
60+
|using namespace std;
61+
|
62+
|void greet() {
63+
| cout << "Hello, Universe!" << endl; // Modified output
64+
|}
65+
|
66+
|int main() {
67+
| vector<int> nums = {1, 2, 3}; // Added new feature
68+
| for (int num : nums) {
69+
| cout << num << " ";
70+
| }
71+
| cout << endl;
72+
|
73+
| greet();
74+
| return 0;
75+
|}""".stripMargin
76+
77+
// Convert C++ programs into line sequences
78+
val fileA = cppProgram1.split("\n").toSeq
79+
val fileB = cppProgram2.split("\n").toSeq
80+
81+
// Compute and print the git diff-style output
82+
printGitDiff(fileA, fileB)
83+
}

src/main/scala/algorithms/graph/Dijkstra.scala

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,39 @@ def dijkstra[T](graph: Graph[T], start: T): Map[T, Int] = {
3636
distances.toMap
3737

3838
}
39+
40+
/// Dijkstra's Algorithm (Targeted Version) - Now returning a Map instead of Option
41+
/// Finds the shortest path from a starting node to a specific target node in a weighted graph
42+
/// Uses a priority queue-based approach
43+
/// Runs in O((V + E) log V) in the worst case
44+
45+
def dijkstra[T](graph: Graph[T], start: T, target: T): Map[T, Int] = {
46+
val distances = mutable.Map[T, Int]().withDefaultValue(Int.MaxValue)
47+
val visited = mutable.Set[T]()
48+
val priorityQueue = PriorityQueue[(T, Int)]()(Ordering.by(-_._2))
49+
50+
distances(start) = 0
51+
priorityQueue.enqueue((start, 0))
52+
while (!priorityQueue.isEmpty) {
53+
val (currentNode: T, currentDistance: Int) = priorityQueue.dequeue().get // unsafe getter for now
54+
if (currentNode == target) {
55+
return distances.toMap // Return the distances map if target is reached
56+
}
57+
58+
if (!visited.contains(currentNode)) {
59+
visited.add(currentNode)
60+
61+
for ((neighbor, weight) <- graph.getNeighbors(currentNode)) {
62+
if (!visited.contains(neighbor)) {
63+
val newDistance = currentDistance + weight
64+
if (newDistance < distances(neighbor)) {
65+
distances(neighbor) = newDistance.toInt
66+
priorityQueue.enqueue((neighbor, newDistance.toInt))
67+
}
68+
}
69+
}
70+
}
71+
}
72+
73+
distances.toMap // Return distances even if the target is unreachable
74+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package algorithms.graph
2+
3+
import datastructures.graph.Graph
4+
import datastructures.basic.PriorityQueue
5+
import scala.collection.mutable
6+
7+
/// A* Algorithm
8+
/// Finds the shortest path from a starting node to a target node in a weighted graph.
9+
/// Uses a heuristic function to guide the search towards the target node more efficiently.
10+
/// Is proven to be the best algorithm
11+
/// https://github.com/bb4/bb4-A-star/blob/master/scala-source/com/barrybecker4/search/AStarSearch.scala
12+
/// Runs in O((V + E) log V) in the worst case, depending on the quality of the heuristic.
13+
14+
def aStar[T](graph: Graph[T], start: T, target: T, heuristic: T => Int): Option[Int] = {
15+
// Check if start and target are the same, return 0 if true
16+
if (start == target) return Some(0)
17+
18+
// Map to store the shortest known distance from the start node to each node
19+
val gScores = mutable.Map[T, Int]().withDefaultValue(Int.MaxValue)
20+
gScores(start) = 0
21+
22+
// Map to store the estimated distance from each node to the target
23+
val fScores = mutable.Map[T, Int]().withDefaultValue(Int.MaxValue)
24+
fScores(start) = heuristic(start)
25+
26+
// Set to keep track of visited nodes
27+
val visited = mutable.Set[T]()
28+
29+
// Priority queue (min-heap) to select the node with the lowest f-score
30+
val priorityQueue = PriorityQueue[(T, Int)]()(Ordering.by(-_._2))
31+
priorityQueue.enqueue((start, fScores(start)))
32+
33+
// Process the queue until it is empty or the target is reached
34+
while (!priorityQueue.isEmpty) {
35+
val (currentNode, currentFScore) = priorityQueue.dequeue().get
36+
37+
// If we reach the target, return the cost to get here
38+
if (currentNode == target) return Some(gScores(currentNode))
39+
40+
// If node has been visited, skip it
41+
if (!visited.contains(currentNode)) {
42+
visited.add(currentNode)
43+
44+
// For each neighbor of the current node, calculate the potential new g-score and f-score
45+
for ((neighbor, weight) <- graph.getNeighbors(currentNode)) {
46+
val tentativeGScore: Int = gScores(currentNode) + weight.toInt
47+
if (tentativeGScore < gScores(neighbor)) {
48+
gScores(neighbor) = tentativeGScore
49+
fScores(neighbor) = tentativeGScore + heuristic(neighbor)
50+
priorityQueue.enqueue((neighbor, fScores(neighbor)))
51+
}
52+
}
53+
}
54+
}
55+
56+
// Return None if the target is unreachable
57+
None
58+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package algorithms.graph
2+
3+
import datastructures.graph.Graph
4+
import datastructures.basic.PriorityQueue
5+
import scala.collection.mutable
6+
7+
/// Bidirectional Dijkstra's Algorithm
8+
/// Finds the shortest path between two nodes by running Dijkstra's algorithm from both the source and target simultaneously
9+
/// This approach can be faster than standard Dijkstra when the target is known
10+
/// Runs in O((V + E) log V) in the worst case, but may terminate earlier
11+
12+
def bidirectionalDijkstra[T](graph: Graph[T], start: T, target: T): Map[T, Int] = {
13+
if (start == target) return Map(start -> 0)
14+
15+
val forwardDistances = mutable.Map[T, Int]().withDefaultValue(Int.MaxValue)
16+
val backwardDistances = mutable.Map[T, Int]().withDefaultValue(Int.MaxValue)
17+
val forwardVisited = mutable.Set[T]()
18+
val backwardVisited = mutable.Set[T]()
19+
val forwardQueue = PriorityQueue[(T, Int)]()(Ordering.by(-_._2))
20+
val backwardQueue = PriorityQueue[(T, Int)]()(Ordering.by(-_._2))
21+
22+
forwardDistances(start) = 0
23+
backwardDistances(target) = 0
24+
forwardQueue.enqueue((start, 0))
25+
backwardQueue.enqueue((target, 0))
26+
27+
def processQueue(
28+
queue: PriorityQueue[(T, Int)],
29+
distances: mutable.Map[T, Int],
30+
visited: mutable.Set[T],
31+
otherDistances: mutable.Map[T, Int]
32+
): Option[Int] = {
33+
queue.dequeue() match {
34+
case Some((currentNode: T, currentDistance: Int)) =>
35+
if (otherDistances.contains(currentNode)) return Some(currentDistance + otherDistances(currentNode))
36+
37+
if (!visited.contains(currentNode)) {
38+
visited.add(currentNode)
39+
40+
for ((neighbor, weight) <- graph.getNeighbors(currentNode)) {
41+
if (!visited.contains(neighbor)) {
42+
val newDistance = currentDistance + weight
43+
if (newDistance < distances(neighbor)) {
44+
distances(neighbor) = newDistance.toInt
45+
queue.enqueue((neighbor, newDistance.toInt))
46+
}
47+
}
48+
}
49+
}
50+
case None =>
51+
}
52+
None
53+
}
54+
55+
// While both queues are non-empty, continue processing
56+
while (!forwardQueue.isEmpty && !backwardQueue.isEmpty) {
57+
processQueue(forwardQueue, forwardDistances, forwardVisited, backwardDistances) match {
58+
case Some(_) => // Found the shortest path, continue
59+
case None =>
60+
}
61+
processQueue(backwardQueue, backwardDistances, backwardVisited, forwardDistances) match {
62+
case Some(_) => // Found the shortest path, continue
63+
case None =>
64+
}
65+
}
66+
67+
// Return the merged distances from both directions
68+
// The final result will be the combination of forward and backward distances
69+
forwardDistances.toMap ++ backwardDistances.toMap
70+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package algorithms.graph
2+
3+
import munit.FunSuite
4+
import datastructures.graph.Graph
5+
6+
class AStarTest extends FunSuite {
7+
8+
// Example heuristic function (Manhattan Distance for grid-based problems)
9+
def heuristic(node: String): Int = node match {
10+
case "A" => 6
11+
case "B" => 5
12+
case "C" => 2
13+
case "D" => 0 // Target node
14+
case _ => Int.MaxValue
15+
}
16+
17+
test("A* should find the shortest path in a small graph") {
18+
val graph = new Graph[String](isDirected = true)
19+
graph.addEdge("A", "B", 1)
20+
graph.addEdge("A", "C", 4)
21+
graph.addEdge("B", "C", 2)
22+
graph.addEdge("B", "D", 5)
23+
graph.addEdge("C", "D", 1)
24+
25+
val result = aStar(graph, "A", "D", heuristic)
26+
27+
assertEquals(result, Some(4)) // The shortest path from A to D is via B and C: A -> B -> C -> D (1 + 2 + 1)
28+
}
29+
30+
test("A* should handle the case where the start is the same as the target") {
31+
val graph = new Graph[String](isDirected = true)
32+
graph.addEdge("A", "B", 1)
33+
34+
val result = aStar(graph, "A", "A", heuristic)
35+
36+
assertEquals(result, Some(0)) // If start and target are the same, the cost is 0
37+
}
38+
39+
test("A* should return None if the target is unreachable") {
40+
val graph = new Graph[String](isDirected = true)
41+
graph.addEdge("A", "B", 1)
42+
graph.addEdge("B", "C", 1)
43+
44+
val result = aStar(graph, "A", "D", heuristic)
45+
46+
assertEquals(result, None) // "D" is unreachable from "A"
47+
}
48+
49+
test("A* should work with different heuristics") {
50+
// Using a different heuristic function for this case
51+
def alternativeHeuristic(node: String): Int = node match {
52+
case "A" => 7
53+
case "B" => 4
54+
case "C" => 3
55+
case "D" => 0
56+
case _ => Int.MaxValue
57+
}
58+
59+
val graph = new Graph[String](isDirected = true)
60+
graph.addEdge("A", "B", 3)
61+
graph.addEdge("A", "C")
62+
graph.addEdge("B", "C", 7)
63+
graph.addEdge("B", "D", 5)
64+
graph.addEdge("C", "D", 2)
65+
66+
val result = aStar(graph, "A", "D", alternativeHeuristic)
67+
68+
assertEquals(result, Some(3)) // The shortest path from A to D is via C: A -> C -> D (1 + 2)
69+
}
70+
}

0 commit comments

Comments
 (0)