Home Module 1 - Introduction to AI Module 2 - Problem-Solving & Search Module 3 - Uncertainty in AI Module 4 - Games & CSP Module 5 - AI in Practice & Ethics Important Questions
Module 2 of 5

Problem-Solving and Search Agents

Module 1 gave us uninformed search - algorithms blind to how close they are to the goal. Module 2 introduces heuristic (informed) search, where a problem-specific estimate guides exploration toward the goal far more efficiently. We then move to local search methods that don't track paths at all - only current states - for large optimization problems, and finally extend search to handle uncertainty: non-deterministic actions, partial observability, and unknown/online environments.

13 Hours
11 Core Topics
CO2 - Apply classical techniques (L3)
2.1

Greedy Best-First Search

This is the first informed (heuristic) search algorithm in the course. Unlike Module 1's blind strategies, informed search uses problem-specific knowledge - a heuristic function h(n) - to estimate how close a state is to the goal, allowing the search to prioritize promising directions.

📘 Definition - Heuristic Function h(n)

h(n) is an estimate of the cost of the cheapest path from node n to a goal state. It encodes domain knowledge in a single number - the smaller h(n), the closer n is believed to be to the goal.

📘 Definition - Greedy Best-First Search

Greedy Best-First Search expands the node that appears closest to the goal, by always selecting the node in the frontier with the lowest h(n) value. It is "greedy" because it ignores the cost already spent reaching n (g(n)) and looks only at the estimated remaining cost.

f(n) = h(n)   (evaluation function used to order the frontier - Greedy ignores g(n) entirely)

Worked Example - Romania Map Problem

Using straight-line distance to Bucharest as h(n): from Arad, the agent greedily picks the neighbor with smallest h (e.g., Sibiu, h=253), then from Sibiu picks Fagaras (h=176) over Rimnicu Vilcea (h=193), then Fagaras→Bucharest. This finds a solution fast (Arad-Sibiu-Fagaras-Bucharest, 450 km) but it is not the optimal 418 km route via Rimnicu Vilcea & Pitesti - illustrating Greedy's key weakness.

Arad
Sibiu h=253
Timisoara h=329
Zerind h=374
Fagaras h=176
Rimnicu h=193

Fig 2.1 - Greedy always expands the lowest-h node; picks Fagaras (176) over the actually-cheaper route through Rimnicu Vilcea (193)

Python
import heapq

def greedy_best_first(graph, start, goal, h):
    frontier = [(h[start], start, [start])]
    visited = set()
    while frontier:
        _, node, path = heapq.heappop(frontier)
        if node == goal:
            return path
        if node in visited:
            continue
        visited.add(node)
        for neighbor, _cost in graph.get(node, []):
            if neighbor not in visited:
                heapq.heappush(frontier, (h[neighbor], neighbor, path + [neighbor]))
    return None

Properties

PropertyResultExplanation
CompletenessNo (in general); Yes in finite spaces with repeated-state checkingCan follow a misleading h(n) into an infinite or dead-end path
OptimalityNoIgnores accumulated cost g(n) entirely - can settle for an expensive path that merely "looked close"
Time ComplexityO(bm) worst caseGood heuristic can make this close to O(bm) in practice
Space ComplexityO(bm)Stores frontier, same concern as BFS/UCS
✓ Advantages
  • Often very fast in practice with a good heuristic - explores far fewer nodes than uninformed search
  • Simple to implement once h(n) is defined
  • Useful when a quick, "good enough" solution is preferred over the optimal one
✗ Disadvantages
  • Not optimal - can be misled by an inaccurate heuristic
  • Not complete in infinite spaces without cycle checking
  • Behaves like poorly-guided DFS in the worst case

Applications

  • Pathfinding in video games where near-optimal (not perfect) routes are acceptable
  • Web crawling prioritized by relevance score
  • Quick approximate solutions in robotics navigation
🔑 Key Points
  • f(n) = h(n) only - ignores path cost so far.
  • Fast but not optimal and not complete in general - A* (next topic) fixes this.
Q. Why is Greedy Best-First Search not optimal?

Because it selects nodes based solely on the estimated remaining cost h(n), completely disregarding the cost g(n) already incurred to reach that node. This can lead it to commit early to a path that "looks" close to the goal but turns out to have a higher total cost than an alternative the algorithm overlooked.

2.2

A* Search Algorithm

A* (pronounced "A-star") is the single most important search algorithm in classical AI - it combines UCS's guarantee of optimality with Greedy Search's speed, by considering both the cost already paid and the estimated cost remaining.

📘 Definition

A* Search expands the node with the lowest value of f(n) = g(n) + h(n), where g(n) is the exact cost from the start to n, and h(n) is the estimated cost from n to the goal. It is a "best-first" search using this combined evaluation function.

f(n) = g(n) + h(n)
g(n) = actual cost so far  |  h(n) = estimated cost to goal  |  f(n) = estimated total cost of cheapest solution through n

Step-by-Step Working

  1. Insert the start node into a priority queue ordered by f(n) = g(n)+h(n); g(start)=0.
  2. Pop the node with lowest f(n).
  3. If it's the goal, return the solution (this is guaranteed optimal - see proof below).
  4. Otherwise expand it: for each successor, compute g(child) = g(parent) + step cost, h(child) from the heuristic, and f(child) = g+h.
  5. If the child is new, or this path is cheaper than a previously found path to it, insert/update it in the frontier.
  6. Repeat from step 2.

Worked Example - Romania Map (same problem, A* this time)

City (path)g(n)h(n)f(n) = g+h
Arad0366366
Arad→Sibiu140253393
Arad→Zerind75374449
Arad→Sibiu→Rimnicu Vilcea220193413
Arad→Sibiu→Fagaras239176415
Arad→Sibiu→RV→Pitesti31798415
Arad→Sibiu→RV→Pitesti→Bucharest4180418 ✓ optimal

Notice that A* correctly finds the 418 km optimal path via Rimnicu Vilcea and Pitesti - the same route Greedy Search missed - because it never lets a low h(n) override a high accumulated g(n).

Python
import heapq

def a_star(graph, start, goal, h):
    frontier = [(h[start], 0, start, [start])]   # (f, g, node, path)
    best_g = {start: 0}
    while frontier:
        f, g, node, path = heapq.heappop(frontier)
        if node == goal:
            return path, g
        if g > best_g.get(node, float('inf')):
            continue
        for neighbor, step_cost in graph.get(node, []):
            new_g = g + step_cost
            if new_g < best_g.get(neighbor, float('inf')):
                best_g[neighbor] = new_g
                new_f = new_g + h[neighbor]
                heapq.heappush(frontier, (new_f, new_g, neighbor, path + [neighbor]))
    return None, float('inf')

Properties of A*

PropertyResultCondition
CompletenessYesIf branching factor finite and step costs bounded above zero
Optimality (Tree-Search)YesIf h(n) is admissible (never overestimates)
Optimality (Graph-Search)YesIf h(n) is consistent (monotonic) - stronger requirement
Time ComplexityExponential in generalBut polynomial if |h(n) − h*(n)| ≤ O(log h*(n)) - a tight heuristic helps enormously
Space ComplexityO(bd)Keeps all generated nodes in memory - A*'s main practical limitation
⚠ Why Memory Is A*'s Achilles Heel

Despite being optimal and efficient in node-expansions, A* still must retain every generated node in memory to backtrack and update costs. This is why memory-bounded variants exist: IDA* (Iterative Deepening A*, using f-cost as the cutoff) and SMA* (Simplified Memory-bounded A*), which trade some completeness/optimality for fixed memory budgets.

A* vs Greedy vs UCS - Master Comparison Table

Algorithmf(n)Complete?Optimal?
UCSg(n)YesYes
Greedy Best-Firsth(n)No (generally)No
A*g(n) + h(n)YesYes (admissible h)
✓ Advantages
  • Guaranteed optimal solution with an admissible heuristic
  • Much more efficient than uninformed search - explores far fewer nodes
  • Industry-standard for pathfinding (games, robotics, GPS)
✗ Disadvantages
  • High memory consumption - stores entire frontier and explored set
  • Performance entirely depends on quality of h(n) - a poor heuristic degrades toward uninformed search
  • Designing a good admissible/consistent h(n) can be non-trivial for complex domains

Applications

  • GPS and map navigation (turn-by-turn routing)
  • Video game pathfinding (NPC movement)
  • Robotics motion planning
  • Puzzle solvers (8-puzzle, 15-puzzle - Lab Experiment 5)
  • Network routing optimization
🔑 Key Points
  • A* = UCS + Greedy combined: f(n) = g(n) + h(n).
  • Optimal if h(n) is admissible (tree search) or consistent (graph search).
  • Space complexity is exponential - the main real-world bottleneck, addressed by IDA*/SMA*.

Interview & Exam Questions

Q. Derive why A* with an admissible heuristic guarantees optimality.

Suppose A* returns a goal G via a suboptimal path with cost g(G). Since h(G)=0, f(G)=g(G). There must exist some node n on the true optimal path still in the frontier, with f(n) = g(n) + h(n) ≤ g(n) + h*(n) = C* (true optimal cost), using admissibility (h(n) ≤ h*(n)). Since C* < g(G) = f(G) by assumption, f(n) ≤ C* < f(G), so A* would have expanded n before G - contradiction. Hence A* cannot terminate on a suboptimal goal.

Q. What happens to A* if h(n) = 0 for all n?

A* reduces exactly to Uniform Cost Search, since f(n) = g(n) + 0 = g(n). This shows UCS is a special case of A* with a trivial (always admissible) heuristic.

Q. What is the difference between A*'s Tree-Search and Graph-Search optimality conditions?

Tree-Search A* only needs an admissible heuristic for optimality, because it never merges duplicate states. Graph-Search A* (which prunes already-visited states for efficiency) requires the stronger consistent/monotonic heuristic property, otherwise it might discard a cheaper path to an already-expanded node and return a suboptimal solution.

2.3

Heuristic Functions: Designing Heuristic Functions

A* is only as good as its heuristic. This topic focuses on the standard engineering technique for designing a heuristic: relaxing the problem.

📘 Definition - Relaxed Problem

A relaxed problem is obtained by removing or loosening one or more constraints of the original problem. The exact solution cost of the relaxed problem is a good - and provably admissible - heuristic for the original problem, since removing constraints can only make the problem easier (cheaper), never harder.

Worked Example - 8-Puzzle Heuristics

The 8-puzzle's rule is: "a tile can move from square A to square B if A is horizontally/vertically adjacent to B AND B is blank." Two classic relaxations give two classic heuristics:

HeuristicRelaxation appliedDefinitionQuality
h1 - Misplaced TilesDrop the adjacency requirement: a tile can move anywhereNumber of tiles not in their goal positionAdmissible but weak (loose bound)
h2 - Manhattan DistanceDrop only the "B must be blank" requirement; tile still must move legally square-by-squareSum, over all tiles, of the number of grid steps from current to goal positionAdmissible and noticeably tighter than h1 - dominates h1
1
2
3
8
·
4
7
6
5

Fig 2.2 - Example state. If tile "8" belongs 2 squares away in the goal, it alone contributes 2 to Manhattan Distance, but only 1 to Misplaced Tiles

Dominance - Comparing Heuristic Quality

📘 Definition - Dominance

If h2(n) ≥ h1(n) for all n (both admissible), then h2 dominates h1. A dominant heuristic is always at least as good, since it is provably more efficient - A* using h2 never expands more nodes than A* using h1.

Other Common Heuristic-Construction Techniques

  • Pattern Databases - precompute exact solution costs for a sub-problem (e.g., a subset of 8-puzzle tiles) and store them in a lookup table for fast heuristic estimates.
  • Combining heuristics via max() - if h1, h2, …, hk are all admissible, h(n) = max(h1(n),…,hk(n)) is also admissible and dominates each individually - never average, since averaging can break admissibility if any one heuristic is too optimistic relative to the others' combination logic; max is always safe.
  • Learning heuristics from experience - using machine learning (e.g., regression) on solved instances to predict h(n) for unseen states.
💡 Real-world example

For GPS route planning, "straight-line (Euclidean) distance to destination" is a relaxed-problem heuristic - it solves a version of the problem where roads can go anywhere, ignoring the constraint that you must follow actual road segments - yet it's provably admissible since straight-line distance never exceeds actual road distance.

🔑 Key Points
  • Heuristics are systematically designed by relaxing constraints of the original problem.
  • A dominant (larger, still admissible) heuristic always expands fewer or equal nodes.
  • Combine multiple admissible heuristics using max(), never averaging.
Q. Why does removing a constraint from a problem always yield an admissible heuristic?

Removing a constraint can only add more possible solution paths (or keep the same set), never fewer. So the optimal cost of the relaxed problem can never exceed the optimal cost of the original problem - exactly the definition of admissibility: h(n) ≤ h*(n).

Q. Between h1 (misplaced tiles) and h2 (Manhattan distance) for the 8-puzzle, which is preferred and why?

h2 (Manhattan distance) is preferred because it dominates h1 - h2(n) ≥ h1(n) for every state, while both remain admissible. A* using h2 explores strictly fewer (or equal) nodes than using h1, making search more efficient without sacrificing optimality.

2.4

Admissibility and Consistency of Heuristics

📘 Definition - Admissible Heuristic

A heuristic h(n) is admissible if it never overestimates the true cost to reach the goal: h(n) ≤ h*(n) for every node n, where h*(n) is the actual optimal cost from n to the goal. An admissible heuristic is always "optimistic."

📘 Definition - Consistent (Monotonic) Heuristic

A heuristic h(n) is consistent if, for every node n and every successor n′ generated by action a with step cost c(n,a,n′): h(n) ≤ c(n,a,n′) + h(n′) - the "triangle inequality." This means the estimated cost never drops by more than the actual step cost taken.

Admissibility:   h(n) ≤ h*(n)    |    Consistency:   h(n) ≤ c(n,a,n′) + h(n′)

Key Theorem: Consistency ⟹ Admissibility

Every consistent heuristic is also admissible (the reverse is not always true). This can be shown by induction along any path to the goal: since h(goal)=0, repeatedly applying the consistency inequality back to n shows h(n) is bounded by the actual path cost, hence h(n) ≤ h*(n).

PropertyGuaranteesRelationship
AdmissibleA* optimal under Tree-SearchNecessary condition
ConsistentA* optimal under Graph-Search; also guarantees f(n) is non-decreasing along any pathConsistent ⟹ Admissible (strictly stronger)

Why Consistency Matters for Graph-Search

Graph-Search A* discards a node once it has been expanded, to avoid redundant work. If h(n) is merely admissible but not consistent, it's possible to later discover a cheaper path to an already-expanded node - but since that node was discarded, A* can never use the cheaper path, breaking optimality. Consistency guarantees that the first time a node is popped from the frontier, it has already been reached via its cheapest possible path.

💡 Worked check - is Manhattan Distance consistent?

For the 8-puzzle, moving one tile one step changes Manhattan distance by exactly ±1, while the step cost is always 1. So h(n) − h(n′) ≤ 1 = c(n,a,n′), satisfying consistency. This is why Manhattan Distance is the standard heuristic taught for 8-puzzle A* search.

⚠ Exam Trap

"Admissible" and "Consistent" are not synonyms. All consistent heuristics are admissible, but an admissible heuristic can fail to be consistent (it can locally "jump" optimistically in a way that violates the triangle inequality, even while never globally overestimating).

Key Points

  • Admissible = never overestimates total remaining cost (global property).
  • Consistent = obeys the triangle inequality step-by-step (local property); strictly stronger.
  • Consistency ⟹ Admissibility, but not vice versa.
  • Graph-Search A* needs consistency; Tree-Search A* only needs admissibility.

Interview & Exam Questions

Q. Prove that every consistent heuristic is admissible.

Take any path n₀(=n), n₁, …, nₖ(=goal). By consistency, h(nᵢ) ≤ c(nᵢ,a,nᵢ₊₁) + h(nᵢ₊₁) for each step. Chaining this from n to the goal and using h(goal)=0 gives h(n) ≤ Σ c(nᵢ,a,nᵢ₊₁) = total path cost. Since this holds for the optimal path in particular, h(n) ≤ h*(n) - admissibility follows.

Q. Why is straight-line distance an admissible heuristic for road-network pathfinding, but not always a consistent one in unusual metrics?

It's admissible because no road route can be shorter than the straight-line (Euclidean) distance - the triangle inequality of ordinary geometry guarantees this globally. It is also typically consistent in standard 2D space because Euclidean distance itself obeys the triangle inequality locally between any three points, matching the consistency definition step-by-step.

2.5

Local Search and Optimization: Hill Climbing

So far, every algorithm tracked entire paths from start to goal. Local search algorithms instead operate using a single current state and move to neighboring states - useful when the path itself doesn't matter, only the final configuration (e.g., optimization, scheduling, circuit design).

📘 Definition - Hill Climbing

Hill Climbing is a local search algorithm that continually moves toward increasing value (or decreasing cost) - it evaluates neighboring states and moves to the neighbor with the best value, stopping when no neighbor is better than the current state. It is essentially "greedy local search," sometimes called steepest-ascent hill climbing.

Step-by-Step Working

  1. Start at a randomly or heuristically chosen initial state.
  2. Evaluate all neighboring states using the objective function.
  3. If the best neighbor is better than the current state, move to it.
  4. If no neighbor is better (a local optimum), stop and return the current state.
  5. Repeat from step 2.

The Landscape Metaphor

local max global max plateau/shoulder

Fig 2.3 - Hill Climbing on a 1D landscape: it can get permanently stuck at the local max (amber), never reaching the true global max (green), because it never moves "downhill"

Variants of Hill Climbing

VariantDescription
Steepest-AscentExamines all neighbors, picks the single best one each step
First-ChoiceGenerates neighbors randomly until one is found that's better, then moves - efficient for states with many neighbors
StochasticChooses randomly among uphill moves, weighted by how much each improves the objective
Random-RestartRuns hill climbing repeatedly from random initial states, keeping the best result found - trades computation for a (probabilistic) escape from local optima

The Four Classic Problems of Hill Climbing

  • Local Maxima - a peak lower than the global maximum, where every neighbor is worse, trapping the search.
  • Plateaus / Shoulders - a flat region where neighbors have equal value, giving no directional signal.
  • Ridges - a sequence of local maxima very difficult to navigate using only single-step axis-aligned moves.
  • Diagonal/complex landscapes - Hill Climbing has no memory or lookahead, so it can't "see" a better path that requires temporarily moving downhill.
Python
def hill_climbing(initial_state, get_neighbors, objective):
    current = initial_state
    while True:
        neighbors = get_neighbors(current)
        best_neighbor = max(neighbors, key=objective, default=None)
        if best_neighbor is None or objective(best_neighbor) <= objective(current):
            return current          # local optimum reached
        current = best_neighbor

Properties

PropertyResult
CompletenessNo - can get permanently stuck at a local optimum/plateau
OptimalityNo - only guarantees a local optimum, not global
Space ComplexityO(1) - only stores the current state, not a frontier
Time ComplexityDepends on landscape; can be very fast but quality varies
✓ Advantages
  • Extremely memory-efficient - O(1) space, no frontier or explored set
  • Simple to implement and understand
  • Fast convergence when the landscape is well-behaved (unimodal)
✗ Disadvantages
  • Gets stuck in local maxima, plateaus, and ridges
  • No guarantee of finding the global optimum
  • Performance highly sensitive to the shape of the objective landscape

Applications

  • Circuit/VLSI design layout optimization
  • Job-shop and timetable scheduling
  • N-Queens problem (minimizing conflicts)
  • Neural network weight tuning (early/simple optimizers)
🔑 Key Points
  • Hill Climbing = greedy local search; tracks only the current state, O(1) memory.
  • Suffers from local maxima, plateaus, and ridges - never moves downhill.
  • Random-restart hill climbing is the standard fix for local optima.
Q. Why does Hill Climbing fail on a plateau, and how does random-restart help?

On a plateau, all neighboring states have the same objective value, so the algorithm has no directional signal telling it which way to move - it may wander randomly or stop entirely. Random-restart hill climbing addresses this (and local maxima generally) by running many independent searches from different random starting points and keeping the best overall result, increasing the probability that at least one run avoids the problematic region.

2.6

Local Search: Simulated Annealing

Simulated Annealing (SA) fixes Hill Climbing's local-optimum trap by occasionally allowing downhill (worse) moves, with decreasing probability over time - directly inspired by the metallurgical process of annealing, where metal is heated then slowly cooled to reach a low-energy, stable crystalline structure.

📘 Definition

Simulated Annealing is a probabilistic local search technique that, at each step, picks a random neighboring state. If it improves the objective, the move is always accepted. If it worsens the objective by ΔE, the move is accepted with probability e−ΔE/T, where T is a "temperature" parameter that gradually decreases according to a cooling schedule.

P(accept worse move) = e−ΔE / T

At high temperature T, this probability is close to 1 - the algorithm explores almost freely, like molten metal with highly mobile atoms. As T → 0, the probability of accepting a worse move shrinks toward 0, and the algorithm increasingly behaves like ordinary Hill Climbing - "freezing" into a final solution.

Step-by-Step Working

  1. Initialize current state randomly; set initial temperature T to a high value.
  2. For each time step, decrease T according to the cooling schedule (e.g., T ← T × 0.95).
  3. Pick a random neighbor of the current state.
  4. Compute ΔE = value(neighbor) − value(current).
  5. If ΔE > 0 (improvement), always move to the neighbor.
  6. If ΔE ≤ 0 (worse), move anyway with probability eΔE/T; otherwise stay.
  7. Repeat until T reaches (near) zero or a stopping criterion is met.
Python
import random, math

def simulated_annealing(initial, get_random_neighbor, objective,
                         T0=100.0, cooling_rate=0.97, min_T=1e-3):
    current, T = initial, T0
    best = current
    while T > min_T:
        neighbor = get_random_neighbor(current)
        delta = objective(neighbor) - objective(current)
        if delta > 0 or random.random() < math.exp(delta / T):
            current = neighbor
            if objective(current) > objective(best):
                best = current
        T *= cooling_rate
    return best

Cooling Schedules

ScheduleFormulaBehavior
LinearT = T₀ − k·tSimple but cools too fast for complex landscapes
Geometric (most common)T = T₀ × αt, 0 < α < 1Smooth exponential decay; α close to 1 (e.g., 0.95–0.99) gives slow, thorough cooling
LogarithmicT = T₀ / log(1+t)Theoretically guarantees convergence to global optimum, but impractically slow
💡 Real-world example

Simulated Annealing is used in VLSI chip placement and routing, where it can escape locally-good-but-globally-poor layouts by occasionally accepting a temporarily worse arrangement, eventually converging on a near-optimal chip layout as "temperature" cools.

Properties

PropertyResult
CompletenessNot guaranteed in finite time, but with a sufficiently slow (logarithmic) cooling schedule, converges to the global optimum with probability → 1
OptimalityProbabilistically approaches global optimum as T → 0 slowly enough; not guaranteed for fast cooling
Space ComplexityO(1) - same as Hill Climbing
✓ Advantages
  • Can escape local optima, unlike plain Hill Climbing
  • O(1) memory - scales to very large search spaces
  • Theoretically guaranteed to find global optimum with ideal (slow) cooling
✗ Disadvantages
  • Performance highly sensitive to the cooling schedule - poorly tuned schedules perform badly
  • Slower than plain Hill Climbing in well-behaved landscapes
  • No early guarantee of solution quality - must wait for convergence

Applications

  • VLSI circuit design and chip layout
  • Traveling Salesman Problem (TSP) approximations
  • Image processing / denoising
  • Scheduling and resource allocation problems
🔑 Key Points
  • Accepts worse moves with probability e−ΔE/T - this is the defining formula.
  • Temperature T decreases over time via a cooling schedule, gradually reducing randomness.
  • At T→0, behaves like Hill Climbing; at high T, behaves like random search.

Interview Questions

Q. How does Simulated Annealing avoid the local-optimum trap that plagues Hill Climbing?

It occasionally accepts moves that worsen the objective function, with probability e^(−ΔE/T). This lets the search "jump out" of a local optimum's basin of attraction by temporarily moving downhill, something pure Hill Climbing structurally cannot do since it only ever accepts improving moves.

Q. What happens if the temperature is decreased too quickly?

The algorithm "freezes" prematurely into Hill-Climbing-like behavior before it has adequately explored the landscape, making it likely to get stuck in a local optimum - same failure mode as plain Hill Climbing, since a fast cooling schedule never gives the algorithm enough opportunities to escape early bad regions.

2.7

Local Search in Continuous Space: Genetic Algorithms

Hill Climbing and Simulated Annealing track a single current state. Genetic Algorithms (GAs) instead maintain an entire population of candidate solutions simultaneously, evolving them generation by generation using principles borrowed from biological evolution: selection, crossover, and mutation.

📘 Definition

A Genetic Algorithm is a population-based metaheuristic search technique inspired by natural selection. Each candidate solution is encoded as a chromosome (typically a string/array); a fitness function scores each chromosome; high-fitness chromosomes are more likely to be selected as "parents" to produce the next generation via crossover and mutation.

Core GA Vocabulary

TermMeaning
Chromosome / IndividualA single candidate solution, encoded (often as a bit-string or array)
GeneA single component/position within a chromosome
PopulationThe full set of chromosomes maintained at each generation
Fitness FunctionA function scoring how "good" a chromosome is - analogous to the objective function in Hill Climbing
SelectionThe process of choosing fitter individuals to become parents (e.g., roulette-wheel, tournament selection)
Crossover (Recombination)Combining genetic material from two parents to create offspring
MutationRandomly altering a gene to maintain genetic diversity and explore new regions

Step-by-Step GA Cycle

  1. Initialize - generate a random initial population of N chromosomes.
  2. Evaluate - compute the fitness of every chromosome in the population.
  3. Select - probabilistically choose parent pairs, favoring higher-fitness individuals.
  4. Crossover - combine each parent pair at a random crossover point to produce two children.
  5. Mutate - with small probability, randomly flip/alter genes in the offspring.
  6. Replace - form the new generation from offspring (and optionally elite parents).
  7. Repeat from step 2 until a stopping criterion (max generations, fitness threshold) is met.

Visual Walkthrough - 8-Bit Chromosome Crossover

Parent 1: 1 0 1 10 1 0 0
Parent 2: 0 0 1 01 1 1 1
↓ crossover at bit 4 ↓
Child 1:  1 0 1 11 1 1 1
Child 2:  0 0 1 00 1 0 0

Fig 2.4 - Single-point crossover at position 4: each child inherits the first half from one parent and second half from the other

Python
import random

def genetic_algorithm(pop_size, gene_len, fitness_fn, generations=200, mutation_rate=0.01):
    population = [[random.randint(0,1) for _ in range(gene_len)] for _ in range(pop_size)]

    for gen in range(generations):
        fitnesses = [fitness_fn(ind) for ind in population]
        new_population = []
        for _ in range(pop_size // 2):
            p1, p2 = random.choices(population, weights=fitnesses, k=2)   # selection
            point = random.randint(1, gene_len - 1)
            c1 = p1[:point] + p2[point:]                                  # crossover
            c2 = p2[:point] + p1[point:]
            for child in (c1, c2):
                for i in range(gene_len):                                 # mutation
                    if random.random() < mutation_rate:
                        child[i] = 1 - child[i]
                new_population.append(child)
        population = new_population
    return max(population, key=fitness_fn)

Properties

PropertyResult
CompletenessNo formal guarantee; in practice often finds good solutions given enough generations
OptimalityNo guarantee of global optimum, but population diversity reduces (not eliminates) the local-optimum risk seen in single-state search
ParallelismNaturally parallelizable - fitness evaluation of each individual is independent
✓ Advantages
  • Explores many regions of the search space simultaneously (population diversity)
  • Naturally parallelizable across individuals
  • Works well on complex, multi-modal, poorly-understood landscapes where gradient information is unavailable
✗ Disadvantages
  • Many hyperparameters to tune: population size, mutation rate, crossover method
  • Can suffer "premature convergence" if diversity collapses too early
  • Computationally expensive - many fitness evaluations per generation

Applications

  • Feature selection and hyperparameter tuning in machine learning
  • Scheduling, timetabling, and resource allocation
  • Evolving neural network architectures (neuroevolution)
  • Engineering design optimization (antenna shapes, aerodynamics)
🔑 Key Points
  • GA = population + fitness + selection + crossover + mutation, repeated over generations.
  • Mutation maintains diversity and prevents premature convergence.
  • No optimality guarantee, but practically effective on hard, non-differentiable landscapes.
Q. Why is mutation necessary in a Genetic Algorithm if crossover already produces new combinations?

Crossover can only recombine genetic material already present in the population - it cannot introduce a value that no individual currently has at a given gene position. Mutation injects genuinely new genetic material, preventing the population from converging prematurely around a limited gene pool and helping the search escape local optima.

Q. How does a GA's search strategy differ fundamentally from Hill Climbing's?

Hill Climbing maintains and improves a single candidate state at a time. A GA maintains an entire population of candidates in parallel, using crossover to combine information from multiple good solutions simultaneously - this population-level exploration gives GAs a structurally different (and often more robust) way of escaping local optima compared to single-state local search.

2.8

Local Search in Continuous Space: Particle Swarm Optimization

Particle Swarm Optimization (PSO) is another population-based metaheuristic, inspired this time by the collective, decentralized behavior of bird flocks and fish schools - rather than evolution, it models social information sharing.

📘 Definition

PSO maintains a swarm of particles, each representing a candidate solution with a position and a velocity in the search space. At every iteration, each particle adjusts its velocity based on (1) its own best-known position (pbest) and (2) the swarm's best-known position (gbest), then moves accordingly - balancing individual exploration with social/collective convergence.

vi(t+1) = w·vi(t) + c1r1(pbesti − xi(t)) + c2r2(gbest − xi(t))
xi(t+1) = xi(t) + vi(t+1)

w = inertia weight  |  c₁,c₂ = cognitive & social coefficients  |  r₁,r₂ = random values in [0,1]

Interpreting the Velocity Update

TermRole
w·vi(t) - InertiaKeeps the particle moving in its current direction (exploration momentum)
c₁r₁(pbest − x) - CognitivePulls the particle back toward its own personal best position (individual memory)
c₂r₂(gbest − x) - SocialPulls the particle toward the swarm's global best position (collective knowledge)

Step-by-Step Working

  1. Initialize a swarm of particles with random positions and velocities.
  2. Evaluate the fitness of each particle's position.
  3. Update each particle's pbest if its current position is better than its recorded pbest.
  4. Update the swarm's gbest if any particle's pbest beats the current gbest.
  5. Update each particle's velocity and position using the formula above.
  6. Repeat from step 2 until convergence or max iterations.
Python
import random

def pso(fitness_fn, dim, n_particles=30, iterations=100,
        w=0.7, c1=1.5, c2=1.5, bounds=(-10, 10)):
    positions = [[random.uniform(*bounds) for _ in range(dim)] for _ in range(n_particles)]
    velocities = [[0.0]*dim for _ in range(n_particles)]
    pbest = [p[:] for p in positions]
    pbest_val = [fitness_fn(p) for p in positions]
    gbest = max(pbest, key=fitness_fn)

    for _ in range(iterations):
        for i in range(n_particles):
            for d in range(dim):
                r1, r2 = random.random(), random.random()
                velocities[i][d] = (w*velocities[i][d]
                    + c1*r1*(pbest[i][d] - positions[i][d])
                    + c2*r2*(gbest[d] - positions[i][d]))
                positions[i][d] += velocities[i][d]
            if fitness_fn(positions[i]) > pbest_val[i]:
                pbest[i] = positions[i][:]
                pbest_val[i] = fitness_fn(positions[i])
        gbest = pbest[pbest_val.index(max(pbest_val))]
    return gbest

GA vs PSO - Comparison

AspectGenetic AlgorithmParticle Swarm Optimization
InspirationBiological evolution (selection, crossover, mutation)Social/collective behavior (flocking, schooling)
RepresentationChromosomes (often discrete/binary)Particles with continuous position & velocity
Information sharingVia crossover between selected parentsVia direct pull toward pbest and gbest
Best suited forCombinatorial / discrete optimizationContinuous, real-valued optimization
ConvergenceCan be slower, more exploratoryOften faster convergence, risk of premature convergence to gbest
✓ Advantages
  • Few parameters to tune compared to GA (mainly w, c1, c2)
  • Fast convergence for continuous optimization problems
  • Simple to implement and computationally efficient per iteration
✗ Disadvantages
  • Can converge prematurely to a local optimum if gbest dominates too early
  • Performance sensitive to parameter choices (w, c1, c2)
  • Less naturally suited to purely discrete/combinatorial problems than GA

Applications

  • Neural network training (weight optimization)
  • Engineering design optimization (antenna design, structural optimization)
  • Power system/economic load dispatch optimization
  • Robot path planning in continuous space
🔑 Key Points
  • PSO update = inertia + cognitive pull (pbest) + social pull (gbest).
  • Best suited to continuous optimization; GA better suited to discrete/combinatorial problems.
  • Risk: premature convergence if the swarm collapses toward gbest too early.
Q. Explain the role of the inertia weight w in PSO. What happens if w is too large or too small?

w controls how much of a particle's previous velocity carries over into the next step. If w is too large, particles keep moving in their existing direction and explore broadly but converge slowly (or never). If w is too small, particles lose momentum quickly and converge fast, but risk getting stuck in a local optimum near their starting region - w is typically decreased over iterations to balance exploration early and exploitation later.

Q. Compare and contrast GA and PSO for solving a continuous optimization problem.

Both are population-based metaheuristics without gradient requirements, suited to non-differentiable, multi-modal landscapes. However, PSO's continuous position/velocity model is naturally suited to real-valued problems and tends to converge faster, while GA's discrete chromosome and crossover model is more naturally suited to combinatorial problems, though it can be adapted to continuous domains (real-coded GA) at extra complexity.

2.9

Search with Non-Deterministic Actions

So far, every algorithm assumed the environment is deterministic: taking action a in state s always leads to exactly the same resulting state. Real environments are rarely this clean - a robot's wheel might slip, a vacuum's "suck" might fail to pick up all dirt. This topic extends search to handle non-deterministic (uncertain) action outcomes.

📘 Definition

In a non-deterministic environment, the RESULTS(s,a) function returns a set of possible outcome states rather than a single state - the agent cannot predict in advance which outcome will actually occur, only the set of possibilities (and, in stochastic settings, their probabilities).

Key Shift: From a Solution Path to a Contingency Plan

Because the agent cannot know in advance which outcome will result from an action, a single fixed sequence of actions ("plan") is no longer sufficient. Instead the agent must compute a conditional plan (also called a contingency plan) - a tree-shaped plan with branches for each possible percept/outcome, e.g.: "Suck; if dirt remains, Suck again; else move Right."

AND-OR Search Trees

📘 Definition - AND-OR Tree

Search trees for non-deterministic problems alternate between two node types: OR nodes (the agent chooses which action to take - like ordinary search) and AND nodes (the environment chooses which outcome occurs - the agent's plan must handle every possible branch).

OR node: agent picks action a₁ or a₂
↓ choose a₁
outcome 1 (env. choice)
Resulting state s₁
outcome 2 (env. choice)
Resulting state s₂

Fig 2.5 - An AND node: the agent's plan must succeed along BOTH outcome branches, since it cannot control which one the environment produces

AND-OR-GRAPH-SEARCH (Conceptual Algorithm)

Pseudocode
function AND-OR-SEARCH(problem):
    return OR-SEARCH(problem.INITIAL_STATE, problem, [])

function OR-SEARCH(state, problem, path):
    if problem.GOAL-TEST(state): return empty plan
    if state in path: return failure          # cycle check
    for each action in problem.ACTIONS(state):
        plan ← AND-SEARCH(RESULTS(state, action), problem, [state] + path)
        if plan ≠ failure:
            return [action] + plan
    return failure

function AND-SEARCH(states, problem, path):
    for each si in states:
        plan_i ← OR-SEARCH(si, problem, path)
        if plan_i == failure: return failure   # must succeed on EVERY outcome
    return {if outcome is si then follow plan_i, for each si}
💡 Real-world example - Erratic Vacuum World

Suppose the vacuum's Suck action is non-deterministic: in a dirty square, it sometimes cleans only that square, but sometimes (unpredictably) also cleans the adjacent square. A correct conditional plan must account for both outcomes - e.g., "Suck; if adjacent square is still dirty, move there and Suck again."

✓ Why Conditional Plans Matter
  • Guarantee goal achievement despite uncertain action outcomes (if a solution exists)
  • Model real robotic/physical systems far more realistically than deterministic search
✗ Challenges
  • Search trees can grow much larger - every AND node multiplies the branching
  • A solution may not exist (no plan can handle all possible outcomes) - must be detected and reported as failure

Key Points

  • Non-deterministic actions return a set of possible result states, not a single state.
  • Solutions become conditional plans, represented as AND-OR trees.
  • OR nodes = agent's choice; AND nodes = environment's choice - plan must succeed on every AND branch.
Q. Why can't a simple linear action sequence solve a non-deterministic search problem?

A linear sequence assumes each action leads to one known next state, allowing the next action to be pre-selected. But in a non-deterministic environment, the actual outcome of an action is uncertain until it happens, so the agent must be prepared with different follow-up actions for each possible outcome - requiring a branching conditional plan rather than a flat sequence.

Q. Explain the difference between OR nodes and AND nodes in an AND-OR search tree.

OR nodes represent points where the agent selects one action among several alternatives - the agent only needs ONE successful branch (like ordinary search). AND nodes represent points where the environment determines the outcome of a non-deterministic action - the agent's plan must work for EVERY possible outcome branch, since it cannot control which one occurs.

2.10

Search in Partially Observable Environments

A further complication: what if the agent cannot even fully perceive the current state? This is the partially-observable case, requiring the agent to reason over its uncertainty about which state it's actually in.

📘 Definition - Belief State

A belief state (or "belief") is the set of all world states the agent considers possible at a given time, given its percept history. Search in partially observable environments operates over belief states rather than individual physical states - this is sometimes called searching in "belief-state space."

Two Extreme Cases

CaseDescriptionBelief state behavior
Sensorless (Conformant) SearchThe agent has no sensors at allBelief state can only shrink via actions whose effects are predictable, never via observation
Partially Observable SearchAgent has imperfect/partial sensorsBelief state updates via both actions and partial percepts, narrowing (or sometimes splitting) over time
Fully Observable (for contrast)Sensors reveal the complete stateBelief state always collapses to a single known state - this is what Module 1's algorithms assumed

Updating a Belief State

A belief state update after taking action a, then receiving percept e, has two component steps:

  1. Prediction - compute the set of possible states reachable by applying a to every state currently in the belief state: b′ = ⋃s ∈ b RESULTS(s,a).
  2. Update (Observation) - filter b′ down to only the states consistent with the just-received percept e: b″ = {s ∈ b′ : PERCEPT(s) = e}.
💡 Worked example - Sensorless Vacuum World

With no sensors, the agent doesn't know if it starts in {A-Dirty} or {B-Dirty}, so its belief state is {A-Dirty, B-Dirty} (could be either). The fixed action sequence [Suck, Right, Suck] is guaranteed to clean both rooms regardless of the true starting state - this is a conformant solution, one that works no matter which actual state the agent began in.

Belief: {A-Dirty, B-Dirty}
action: Suck
Belief: {A-Clean, B-Dirty}
action: Right, Suck → both clean

Fig 2.6 - Belief-state search: the agent reasons over sets of possible states, narrowing them via actions

Properties & Challenges

✓ Why Belief-State Search Works
  • Reduces to ordinary search over a new (larger) state space - the space of belief states
  • Conformant/contingency plans found this way are guaranteed correct regardless of the true hidden state
✗ Computational Cost
  • Belief-state space can be exponentially larger than the original state space (a belief state is a subset of physical states - up to 2|states| possible belief states)
  • Sensorless problems may have no solution if no fixed plan works for all possible starting states

Applications

  • Robot localization with noisy/limited sensors
  • Medical diagnosis under incomplete patient information
  • Card games with hidden information (opponent's hand)
  • Autonomous exploration of unknown/unmapped terrain
🔑 Key Points
  • Search shifts from physical states to belief states (sets of possible states).
  • Belief-state update = prediction (apply action) + observation (filter by percept).
  • Sensorless search needs a single plan that works for every possible true state.
Q. What is a "conformant plan" and when is it needed?

A conformant plan is a fixed action sequence guaranteed to achieve the goal regardless of which actual state the agent started in or which non-deterministic outcomes occurred - needed in sensorless (no-observation) environments, where the agent cannot use percepts to adapt its plan mid-execution.

Q. Why can the belief-state space be much larger than the original state space?

A belief state is a subset of the original state space, and there can be up to 2^N distinct subsets for N physical states. So even a modest-sized original problem can produce an exponentially larger belief-state space, making the search significantly more computationally expensive.

📋 Module 2 - Complete Summary

Module 2 moved from uninformed to informed search: Greedy Best-First (fast, not optimal) and A* (f=g+h, optimal with an admissible/consistent heuristic) - with heuristics systematically designed via problem relaxation. We then left path-tracking search behind for local search: Hill Climbing (fast but trapped by local optima), Simulated Annealing (escapes local optima via probabilistic downhill moves), and the population-based metaheuristics Genetic Algorithms (evolution-inspired, suited to discrete problems) and Particle Swarm Optimization (flocking-inspired, suited to continuous problems). Finally we relaxed search's core assumptions: non-deterministic actions require conditional (AND-OR) plans, partial observability requires reasoning over belief states, and unknown/dynamic environments require online agents like LRTA* that interleave acting and learning.