Uniform Cost Search
def uniform_cost_search(start):
cost_to_get_to_node = {}
frontier = PriorityQueue() // Higher Priority equals Lower Cost
expanded_nodes = Set()
cost_to_get_to_node[start] = 0
frontier.push((start, 0))
expanded_nodes.add(start)
while front.has_elements():
u = frontier.pop()
if is_goal(u):
return path(u)
for v in u.successors:
if v not in expanded_nodes:
if v in frontier:
# if we already have it in the frontier, but we found a cheaper way to get there, update the cost to get here.
cost_to_get_to_node[v] = min(cost_to_get_to_node[v], cost_to_get_to_node[u] + u.action_cost(v))
else:
frontier.push(v)
cost_to_get_to_node[v] = cost_to_get_to_node[u] + u.action_cost(v))
return Failure
In general UCS is not complete, because given an infinite number of zero-weight edges deviating away from the goal state, then UCS will follow this path forever and never find the goal. On the other hand, if there exists some such that every edge has at least this weight, then UCS will find the minimum cost solution, this is because it explores nodes in the search space in non-decreasing order of cost so it must find a minimum cost path to a goal before finding any higher cost paths leading to a goal.
| path | frontier | about to expand |
| (S, 0) | S | |
| S | (SP, 1), (SD, 3), (SE, 9) | (SP, 1) |
| (SP, 1) | (SD, 3), (SE, 9), (SPQ, 16) | (SD, 3) |
| (SD, 3) | (SE, 9), (SPQ, 16), (SDE, 5) | (SDE, 5) |
| (SDE, 5) | (SE, 9), (SPQ, 16), (SDEH, 6) | (SDEH, 6) |
| (SDEH, 6) | (SE, 9), (SPQ, 16), (SDEHQ, 10) | (SE, 6) |
| (SE, 9) | (SPQ, 16), (SDEHQ, 10), (SEH, 10) | (SEH, 10) |
| (SEH, 10) | (SPQ, 16), (SDEHQ, 10), (SEHQ, 14) | (SDEHQ, 10) |
| (SDEHQ, 10) | (SPQ, 16), (SEHQ, 14), (SDEHQG, 11) | (SDEHQG, 11) |
| (SDEHQG, 11) | (SPQ, 16), (SEHQ, 14) |
Note that if we don't have the lower bound assumption, then there could exist some negative edge weights meaning that there is potential for a high cost path, to then eventually become a very low cost before getting to which would mean that UCS would find some other higher cost path to before making it over the "hump" of the other path to with lower cost. This is similar to escaping a local minimum.
Suppose that is the first path that UCS produces that leads to some node , then assume for the sake of contradiction that this path is not cost optimal, therefore there exists some other path that has lower cost, but then would be a path leading to that gets explored before but was the first one, so this is a contradiction, therefore is the minimal cost path to .
By the above lemmas we know that UCS will expand all nodes with a cost less than and potentially (in the worst case) all nodes with cost equal to . If we know that the minimal cost to get from to is , and each edge has weight at least , then the maximal number of edges in this path is given by , to understand why this is true, suppose that and , , but clearly we're going to need at least two edges to reach our goal, because if we had just one, we would have cost , but this path couldn't reach because any path that reaches must have cost at least , this is why the equation is , which our our specific case would be equal to which makes sense.
If we isolate our attention to the subtree with depth , then in the worst case every edge inside this subtree has weight , and therefore every node in a higher level in the tree has a lower cost and must be explored before we explore node .
Everything at layer has the same cost of . Then bringing our attention back to the whole tree, any node that is outside of this subtree will have cost greater than , and thus none of them will be expanded by while trying to find .
Let's now calculate how many nodes UCS actually has to expand Since UCS explores nodes with a lower path cost first, then the entire subtree of depth will be explored before , then at depth in the worst case, every other node is expanded before , therefore in total we have to explore nodes, therefore our runtime is given by