UCS and Dijkstra's algorithm are taught in different courses (AI vs. algorithms), use different vocabulary (frontier vs. priority queue of vertices, g(n) vs. dist[v]), and somehow end up feeling like two separate things. They are not. UCS is Dijkstra's algorithm, restricted to a search problem and stopped early. Everything else is the same machinery.
The core shift: Dijkstra answers "what's the shortest distance to every node?" UCS answers "what's the cheapest way to reach one specific goal?" — and stops the moment it knows.
| Dijkstra's Algorithm | Uniform Cost Search (UCS) | |
|---|---|---|
| Problem type | Single-source shortest path: find the shortest distance from one source to every other vertex | Search problem: find the cheapest path from a start state to a goal state |
| Is the graph known upfront? | Yes — usually a fixed, fully known graph (adjacency list/matrix) | Not necessarily — the graph can be implicit, generated on demand via a successors(state) function (e.g. legal moves in a puzzle) |
| When does it stop? | When the priority queue is empty — i.e. after computing distances to everything | The instant the goal node is popped from the frontier |
Same underlying optimization, different scope: Dijkstra computes the answer for the whole graph; UCS only computes as much of the graph as it needs to answer one question.
Both algorithms maintain a priority queue ordered by accumulated path cost — called dist[v] in Dijkstra, g(n) in UCS. The loop is identical in spirit:
Why the first pop of a node is guaranteed optimal: this relies on one assumption — all edge weights are non-negative. That means walking further down any path can only make it more expensive, never cheaper. So if some node X is popped with cost c, no path discovered later could possibly reach X more cheaply — any such path would have to pass through some frontier node with cost < c, which contradicts X being the smallest available. This is the entire proof of optimality, for both algorithms.
def uniform_cost_search(problem):
node = Node(state=problem.initial_state, cost=0)
frontier = PriorityQueue(order_by=lambda n: n.cost)
frontier.push(node, priority=0)
explored = set()
while True:
if frontier.is_empty():
return FAILURE
node = frontier.pop() # smallest g(n) first
if problem.is_goal(node.state):
return solution(node) # stop the instant goal is popped
if node.state not in explored:
explored.add(node.state)
for action, successor, step_cost in problem.successors(node.state):
child = Node(
state=successor,
parent=node,
action=action,
cost=node.cost + step_cost,
)
if child.state not in explored:
frontier.push(child, priority=child.cost)