CS 351: Analysis of Algorithms
Pathfinding algorithms find the optimal or near-optimal route between two points in a graph.
Applications:
Different algorithms make different trade-offs:
Today we’ll review the three fundamental approaches to pathfinding on the same problem.
We’ll use a simple graph with 6 nodes to illustrate each algorithm:
Goal: Find a path from S (start) to G (goal)
Nodes:
Edge Costs:
Heuristic Values h(n):
Edge | Cost | Node | h(n) |
---|---|---|---|
S → A | 2 | S | 6 |
S → B | 1 | A | 5 |
A → C | 3 | B | 4 |
B → C | 2 | C | 2 |
B → D | 4 | D | 3 |
C → G | 2 | G | 0 |
D → G | 1 |
Note: The heuristic h(n) represents an estimate of the remaining distance to the goal.
Strategy:
Key Characteristics:
Think of it like:
Analogy:
function GreedyBestFirstSearch(start, goal):
frontier = PriorityQueue()
frontier.add(start, h(start))
explored = empty set
while frontier is not empty:
current = frontier.pop() # Lowest h(n)
if current == goal:
return path
explored.add(current)
for each neighbor of current:
if neighbor not in explored:
frontier.add(neighbor, h(neighbor))
return failure
Starting State:
Current node: S (highlighted in gold)
Action:
Updates:
Action:
Updates:
Action:
C has the lowest heuristic (h=2)
Updates:
Result:
Path Found:
Path Cost:
Summary:
Observation:
Strategy:
Key Characteristics:
Think of it like:
Analogy:
function Dijkstra(start, goal):
frontier = PriorityQueue()
frontier.add(start, 0)
g_scores = {start: 0}
explored = empty set
while frontier is not empty:
current = frontier.pop() # Lowest g(n)
if current == goal:
return path
explored.add(current)
for each neighbor of current with cost:
tentative_g = g[current] + cost
if neighbor not in explored or
tentative_g < g[neighbor]:
g[neighbor] = tentative_g
frontier.add(neighbor, tentative_g)
return failure
Starting State:
All nodes start with g=∞ except start
Action:
g-score Updates:
New State:
Action:
g-score Updates:
New State:
Action:
g-score Updates:
New State:
Action:
g-score Updates:
New State:
Action:
g-score Updates:
New State:
Result:
Path Found:
Path Cost:
1 + 2 + 2 = 5
Summary:
Observation:
Alternative Path Analysis:
S → A → C → G = 2 + 3 + 2 = 7 (worse)
S → B → D → G = 1 + 4 + 1 = 6 (worse)
S → B → C → G = 1 + 2 + 2 = 5 (best!)
Dijkstra checked all possibilities systematically and confirmed 5 is the minimum cost.
Strategy:
Key Characteristics:
Think of it like:
Analogy:
function AStar(start, goal):
frontier = PriorityQueue()
frontier.add(start, h(start))
g_scores = {start: 0}
explored = empty set
while frontier is not empty:
current = frontier.pop() # Lowest f(n)
if current == goal:
return path
explored.add(current)
for each neighbor of current with cost:
tentative_g = g[current] + cost
if neighbor not in explored or
tentative_g < g[neighbor]:
g[neighbor] = tentative_g
f[neighbor] = g[neighbor] + h[neighbor]
frontier.add(neighbor, f[neighbor])
return failure
Evaluation Function:
Components:
Intuition:
Starting State:
Action:
f-score Updates:
New State:
Action:
f-score Updates:
New State:
Action:
f-score Updates:
New State:
Result:
Path Found:
Path Cost:
Summary:
Observation:
The Power of f(n):
Comparison:
This is why A* is often the best choice when a good heuristic is available.
Algorithm | Path | Cost | Nodes Explored | Optimal? |
---|---|---|---|---|
Greedy BFS | S→B→C→G | 5 | 4 | Yes* |
Dijkstra | S→B→C→G | 5 | 6 | Yes |
A* | S→B→C→G | 5 | 4 | Yes |
*Greedy BFS happened to find the optimal path but doesn’t guarantee it
Time Complexity:
Space Complexity:
Optimality:
Greedy Best-First Search:
Dijkstra’s Algorithm:
A* Search:
Admissible Heuristics:
Good Heuristics:
Poor Heuristics:
Three Different Strategies:
The Trade-off Triangle:
Real-World Impact:
Choosing the Right Algorithm: