Problem Solving By Search
Uninformed Search
Uninformed vs Informed (Preview of Chapter 3)
Jr Codex AI Notes
Level: Intermediate Prerequisites: Chapter 1 Time to complete: ~30 minutes
Table of Contents
- What Makes Search "Uninformed"?
- Breadth-First Search (BFS)
- Depth-First Search (DFS)
- Uniform-Cost Search (UCS)
- Comparing BFS, DFS, and UCS
- Evaluating Search Algorithms — Four Criteria
- Summary & Next Steps
1. What Makes Search "Uninformed"?
Uninformed (or "blind") search algorithms have no additional information about how close a state is to the goal — they only know what Chapter 1's five-component problem definition provides: states, actions, and costs. Chapter 3's informed search adds a heuristic (an estimate of remaining distance to the goal) — the key thing that's absent here.
Uninformed vs Informed (Preview of Chapter 3)
─────────────────────────────────────────
Uninformed: "I don't know which direction is more promising —
I'll explore systematically until I find the goal."
Informed: "I have an ESTIMATE of how far each state is from
the goal — I'll explore the most PROMISING
states first." (Chapter 3)
─────────────────────────────────────────
2. Breadth-First Search (BFS)
BFS explores the state space level by level — all states 1 step away, then all states 2 steps away, and so on. The frontier is a queue (first-in-first-out), following the generic template from Chapter 1, Section 6.
from collections import deque
def breadth_first_search(problem):
frontier = deque([(problem.initial_state, [])]) # queue: FIFO
visited = {problem.initial_state}
while frontier:
state, path = frontier.popleft() # remove from the FRONT
if problem.goal_test(state):
return path
for action in problem.actions(state):
next_state = problem.result(state, action)
if next_state not in visited:
visited.add(next_state)
frontier.append((next_state, path + [action])) # add to the BACK
return None
# Using Chapter 1's romania_map and SearchProblem
from_arad_to_bucharest = SearchProblem("Arad", "Bucharest", romania_map)
solution = breadth_first_search(from_arad_to_bucharest)
print(solution) # e.g. ['Sibiu', 'Fagaras', 'Bucharest']BFS Exploration Order (Level by Level)
─────────────────────────────────────────
[Arad] ← Level 0
/ | \
[Zerind][Sibiu][Timisoara] ← Level 1 (explored FIRST, entirely)
/ | \
[Oradea] [Fagaras/Rimnicu] [Lugoj] ← Level 2 (explored SECOND, entirely)
─────────────────────────────────────────
Key property: BFS is guaranteed to find the shortest path in terms of number of steps — but NOT necessarily the lowest-cost path if steps have different costs (that's what UCS, Section 4, guarantees instead).
3. Depth-First Search (DFS)
DFS explores as deep as possible down one path before backtracking — the frontier is a stack (last-in-first-out).
def depth_first_search(problem):
frontier = [(problem.initial_state, [])] # stack: LIFO
visited = set()
while frontier:
state, path = frontier.pop() # remove from the END (top of stack)
if problem.goal_test(state):
return path
if state in visited:
continue
visited.add(state)
for action in problem.actions(state):
next_state = problem.result(state, action)
if next_state not in visited:
frontier.append((next_state, path + [action])) # add to the END
return None
solution = depth_first_search(from_arad_to_bucharest)
print(solution)DFS Exploration Order (Deep Before Wide)
─────────────────────────────────────────
[Arad] → [Zerind] → [Oradea] → [Sibiu] → ...
(follows ONE path as deep as it goes, only backtracking
when it hits a dead end or already-visited state)
─────────────────────────────────────────
Key trade-off: DFS uses far less memory than BFS (it only needs to remember one path at a time, not every state at the current level) — but it can find a much longer, worse solution than necessary, since it commits early to a direction without comparing alternatives.
The Recursive Formulation
DFS has a natural recursive implementation, directly connecting to Python Notes, Module 2, Chapter 4's recursion chapter:
def dfs_recursive(problem, state, path, visited):
if problem.goal_test(state):
return path
visited.add(state)
for action in problem.actions(state):
next_state = problem.result(state, action)
if next_state not in visited:
result = dfs_recursive(problem, next_state, path + [action], visited)
if result is not None:
return result # base case satisfied somewhere deeper in the recursion
return None # this branch is a dead end4. Uniform-Cost Search (UCS)
UCS always expands the state with the lowest cumulative path cost so far — the frontier is a priority queue, ordered by accumulated cost.
import heapq
def uniform_cost_search(problem):
frontier = [(0, problem.initial_state, [])] # (cumulative_cost, state, path)
visited = set()
while frontier:
cost, state, path = heapq.heappop(frontier) # lowest cost FIRST
if problem.goal_test(state):
return path, cost
if state in visited:
continue
visited.add(state)
for action in problem.actions(state):
next_state = problem.result(state, action)
step_cost = problem.step_cost(state, action)
if next_state not in visited:
heapq.heappush(frontier, (cost + step_cost, next_state, path + [action]))
return None, float("inf")
solution, total_cost = uniform_cost_search(from_arad_to_bucharest)
print(f"Path: {solution}, Total cost: {total_cost}") # the OPTIMAL (lowest-cost) routeWhy UCS Guarantees the OPTIMAL Path (Unlike BFS With Varying Costs)
─────────────────────────────────────────
BFS guarantees the FEWEST STEPS — irrelevant if step costs vary
(Arad→Sibiu costs 140, but that might be ONE step vs a longer,
cheaper multi-step alternative).
UCS always expands the CHEAPEST accumulated path so far — by
the time it reaches the goal, no cheaper path could possibly
still be waiting in the frontier.
─────────────────────────────────────────
UCS is what Chapter 1's heapq-based priority queue is really for — and it's the direct ancestor of A* (Chapter 3), which is UCS plus a heuristic.
5. Comparing BFS, DFS, and UCS
# Running all three on the same problem to see the difference directly
bfs_path = breadth_first_search(from_arad_to_bucharest)
dfs_path = depth_first_search(from_arad_to_bucharest)
ucs_path, ucs_cost = uniform_cost_search(from_arad_to_bucharest)
print(f"BFS path ({len(bfs_path)} steps): {bfs_path}")
print(f"DFS path ({len(dfs_path)} steps): {dfs_path}")
print(f"UCS path ({len(ucs_path)} steps, cost={ucs_cost}): {ucs_path}")| Algorithm | Frontier Structure | Guarantees Optimal? | Memory Use |
|---|---|---|---|
| BFS | Queue (FIFO) | Optimal for STEP COUNT only (equal-cost steps) | High — stores every state at the current level |
| DFS | Stack (LIFO) | No — can find a long, wasteful path | Low — only stores the current path |
| UCS | Priority queue (by cost) | Yes — optimal for PATH COST | High — similar to BFS |
6. Evaluating Search Algorithms — Four Criteria
Every search algorithm, uninformed or informed, is evaluated against these four standard criteria — worth memorizing as the lens for comparing every algorithm in this module:
The Four Evaluation Criteria
─────────────────────────────────────────
Completeness: Is the algorithm GUARANTEED to find a solution
if one exists?
Optimality: If it finds a solution, is it GUARANTEED to
be the best (lowest-cost) one?
Time Complexity: How does runtime grow as the problem gets bigger?
Space Complexity: How does memory usage grow as the problem
gets bigger?
─────────────────────────────────────────
| Algorithm | Complete? | Optimal? | Time | Space |
|---|---|---|---|---|
| BFS | Yes (finite branching) | Only for uniform step costs | O(b^d) | O(b^d) |
| DFS | Only in finite state spaces | No | O(b^m) | O(bm) |
| UCS | Yes | Yes | O(b^(C*/ε)) | O(b^(C*/ε)) |
(b = branching factor, d = depth of shallowest solution, m = maximum depth, C* = optimal cost, ε = minimum step cost — these formulas are standard AI textbook notation, worth recognizing rather than memorizing precisely.)
The practical takeaway: DFS's dramatically lower space complexity (O(bm) vs exponential for BFS/UCS) is the reason to ever choose it despite giving up optimality — for very large or deep state spaces where memory is the binding constraint, this trade-off can be decisive.
7. Summary & Next Steps
Key Takeaways
- Uninformed search has no estimate of distance-to-goal — it explores based purely on the problem's structure (states, actions, costs).
- BFS (queue) guarantees the fewest steps but not the lowest cost; DFS (stack) uses far less memory but gives up any optimality guarantee; UCS (priority queue by cost) guarantees the lowest-cost path.
- Every search algorithm is judged against four criteria: completeness, optimality, time complexity, and space complexity.
- DFS's linear-in-depth memory usage (vs BFS/UCS's exponential) is the key practical reason to accept its lack of optimality guarantees.
Concept Check
- Why does BFS guarantee the fewest steps but not necessarily the cheapest path?
- What's the core data-structure difference between BFS and DFS, and how does that difference explain their different memory usage?
- Under what circumstance would UCS and BFS return the identical path?
Next Chapter
→ Chapter 3: Informed Search & Heuristics
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index