UNIT 03 β€’ CORE SEARCH & PLANNING

Problem Solving & State Space Search

An exhaustive engineering treatment of goal-based rational problem solving: state-space graph formulations, uninformed tree and graph search strategies, heuristic evaluation with A* admissibility and consistency, local optimization landscapes, and constraint satisfaction problems (CSPs).

πŸ“– Standard: Russell & Norvig (AIMA 4th Edition)
⏱️ Study Depth: 6.5 Hours β€’ In-Depth Theory & Code
πŸ’» Implementation: Python 3.12 (BFS, DFS, UCS, A*, CSP)
🎯 Verification: 100% Pure Natural English Language
01

Problem-Solving Agents & The Goal Formulation Cycle

In modern Artificial Intelligence, reflex agents act purely on immediate perceptual inputs using condition-action mappings. When environments require multi-step planning over long horizons, rational action requires a Problem-Solving Agent: an agent architecture that adopts a goal, explores plausible future action sequences within an internal model, and executes the discovered optimal trajectory.

πŸ”„ The 4-Phase Problem-Solving Cycle
  1. Goal Formulation: Based on current percepts and performance metrics, the agent establishes an explicit objective (e.g., reaching Bucharest from Arad, or completing an 8-Puzzle grid). Goals narrow down the space of choices by identifying desirable terminal world configurations.
  2. Problem Formulation: The agent translates the real-world scenario into a formal computational abstraction by defining what states of the world and transitions it must consider. An abstraction systematically removes irrelevant environmental details (such as scenery or fuel gauge needle positions) while preserving path-critical state variables.
  3. Search (Offline Deliberation): The agent simulates alternative action sequences mentally without taking real physical actions in the world. The search algorithm explores state-to-state paths within its internal model until a sequence reaching the goal is discovered.
  4. Execution (Online Acting): Once a valid solution path is verified, the agent executes the sequence of actions one by one in the real physical environment. In static, fully observable, deterministic environments, this execution phase is open-loop (no sensory feedback is needed during execution because the predicted trajectory is exact).
System Architecture: Offline Search vs. Online Execution
   +------------------+         Percepts         +---------------------+
   |   Environment    | -----------------------> |  Goal Formulation   |
   +------------------+                          +---------------------+
            ^                                               |
            |                                       Formulated Goal
            |                                               v
            | Action                             +---------------------+
            | Sequence                           | Problem Formulation |
            |                                    +---------------------+
            |                                               |
            |                                       States, Actions, Cost
            |                                               v
   +------------------+                          +---------------------+
   | Execution Phase  | <----------------------- |    Search Phase     |
   | (Online Action)  |       Solution Path      | (Offline Simulation)|
   +------------------+                          +---------------------+
πŸ’‘ Open-Loop vs. Closed-Loop Execution

If an environment is deterministic and fully observable, an agent plans offline once and blindly executes the resulting action sequence (open-loop). If the environment is stochastic, dynamic, or partially observable, unexpected events can invalidate the plan. Such agents must interleave search and execution via closed-loop control (continually re-perceiving and re-planning at each intermediate step).

02

State Space Search & Formal Problem Formulation

To allow algorithmic search, every classical AI problem must be formally specified by five well-defined mathematical components. These five components define the State Space: the set of all possible configurations reachable from the initial state by any sequence of legal actions.

1. Initial State

The exact world configuration where the agent begins its operation. For example, in the Romania route finding problem, the initial state is represented as In(Arad). In the 8-puzzle, it is the initial 3 by 3 arrangement of numbered tiles.

2. Actions Function: Actions(s)

Given a particular state s, the function Actions(s) returns the set of legal actions the agent can execute. For example, when in Arad, Actions(In(Arad)) = {Go(Zerind), Go(Sibiu), Go(Timisoara)}.

3. Transition Model: Result(s, a)

A formal description of what each action does. The function Result(s, a) returns the resulting state when action a is executed in state s. Together, the initial state, actions, and transition model implicitly define the directed graph of the state space.

4. Goal Test

A boolean test that evaluates whether a given state s is a goal state. This test can be explicit (e.g., checking if state == In(Bucharest)) or implicit (e.g., checking if no two queens attack each other on an 8-Queens board).

5. Path Cost Function: g(n)

A numeric function that assigns a cost to a path. The step cost of taking action a in state s to reach state s' is denoted as StepCost(s, a, s'). The total path cost g(n) is the sum of the step costs along the path from the root to node n.

🧩 Classic Benchmark Formulations
Problem Domain States Representation Actions Available Goal Test Condition Path Cost Function
8-Puzzle (Sliding Tiles) Location of each of the 8 numbered tiles and the blank square in a 3 by 3 grid. Total reachable states: 181,440 (9! / 2). Move the blank space: Up, Down, Left, or Right (subject to grid boundaries). Grid matches the exact goal configuration: tiles 1 to 8 in clockwise order with center blank. Each step costs 1 unit (path cost equals number of moves taken).
Romania Road Map Current city: e.g., In(Arad), In(Oradea), In(Sibiu), etc. Total: 20 Romanian cities. Drive along an existing highway connecting current city to an adjacent neighbor. Current city equals In(Bucharest). Sum of road segment distances in kilometers (e.g., Arad to Zerind is 75 km).
8-Queens Problem Arrangement of 0 to 8 queens on an 8 by 8 chessboard with no two queens sharing a row, column, or diagonal. Place a new queen in an empty column, or slide an existing queen within its column. All 8 queens are placed on the board with zero mutual attacks. 0 for finding any valid state; or number of moves taken in iterative repair.
Traveling Salesperson (TSP) Sequence of visited cities and current location. Visit an unvisited city from current city. All cities visited exactly once and agent has returned to the origin city. Total distance or travel time summed over the entire tour.
03

Search Tree vs. Search Graph & Evaluation Metrics

A critical conceptual distinction in AI problem solving is the difference between a State (a physical configuration of the world) and a Search Node (a book-keeping data structure instantiated during the search process).

πŸ—‚οΈ Anatomy of a Search Node

A search node n contains five essential bookkeeping fields:

  • n.State: The world state corresponding to this node.
  • n.Parent: Pointer to the node that generated this node.
  • n.Action: The specific action applied to the parent to yield this node.
  • n.PathCost g(n): The total cost of the path from the root to this node.
  • n.Depth: The number of steps along the path from the root (root depth is 0).
⚠️ Loopy Paths and Redundant States

In many problem graphs, actions are reversible (e.g., Arad → Sibiu → Arad). If an algorithm does not remember where it has been, a Tree Search will traverse the same loop endlessly, building an infinite tree over a small finite state space.

To prevent infinite cycling and catastrophic exponential duplication, Graph Search maintains an explicit Explored Set (often called the Closed List) of all states whose successors have already been expanded.

Graph Search vs. Tree Search Topology
   Physical State Graph (Loopy)               Expanded Search Tree (Without Closed List)
         +---------+                                          [Arad]
         |  Arad   |                                         /      \
         +---------+                                    [Sibiu]    [Zerind]
          /       \                                     /     \        \
         v         v                                 [Arad] [Fagaras] [Arad]
    +-------+   +--------+                            /  \       \       /  \
    | Sibiu |<->| Zerind |                          ...  ...     ...   ...  ...
    +-------+   +--------+                         (Trapped in Infinite Redundancy)

   --------------------------------------------------------------------------------------
   Graph Search Rule: Before expanding a node, check if node.State is in Explored Set
   or Frontier. If already present with lower or equal cost, discard immediately!
πŸ“ The Four Formal Performance Metrics

Every search algorithm is evaluated rigorously against four mathematical dimensions:

  • Completeness: Is the algorithm guaranteed to find a solution whenever a valid path exists in the state space?
  • Optimality: Does the strategy guarantee finding the highest-quality solutionβ€”meaning the path with the lowest overall path cost g(n) among all possible solution paths?
  • Time Complexity: How many operations or generated nodes are required to locate the goal? Usually expressed in terms of:
    • b: Branching factor (maximum number of successor nodes generated by any state).
    • d: Depth of the shallowest goal node in the search tree.
    • m: Maximum depth of any path in the state space (can be infinite).
  • Space Complexity: How much maximum memory (number of nodes retained simultaneously in the frontier and explored set) is consumed during execution? In AI search, memory exhaustion is almost always the fatal barrier long before time runs out.
07

Constraint Satisfaction Problems (CSPs) & Backtracking

In standard search, states are atomic "black boxes" with no internal structure accessible to the algorithm. A Constraint Satisfaction Problem (CSP) uses a factored representation where states are composed of variables, each with a domain of values, and a set of explicit constraints that values must satisfy.

πŸ“ Formal Triple: CSP = (Variables X, Domains D, Constraints C)
  • Variables X: {X1, X2, ..., Xn} β€” The variables to be assigned values (e.g., Australian territories: WA, NT, SA, Q, NSW, V, T).
  • Domains D: {D1, D2, ..., Dn} β€” The set of allowable values for each variable (e.g., colors: {Red, Green, Blue}).
  • Constraints C: {C1, C2, ..., Cm} β€” Pairwise or group restrictions specifying legal combinations of values (e.g., SA != WA, SA != NT).

A state is a partial assignment of values to variables. An assignment is consistent if it violates no constraints. A complete assignment assigns a value to every variable. The goal of a CSP is to find a complete, consistent assignment.

πŸ”„ Backtracking Search for CSPs

Because variable assignments are commutative (assigning WA = Red then NT = Green is identical to assigning NT = Green then WA = Red), a basic CSP search only needs to consider assignments for a single variable at each step.

Backtracking Search is a specialized depth-first search that chooses values for one variable at a time, checks consistency, and backtracks immediately when a variable has no legal values left.

⚑ Heuristics to Accelerate Backtracking
  • MRV (Minimum Remaining Values): Choose the unassigned variable with the fewest remaining legal values in its domain ("Fail-First" heuristic).
  • Degree Heuristic: Tie-breaker for MRV. Choose the variable involved in the largest number of constraints with other unassigned variables.
  • LCV (Least Constraining Value): Given a chosen variable, choose the value that rules out the fewest choices for neighboring unassigned variables ("Fail-Last" heuristic).
πŸ” Constraint Propagation: Forward Checking vs. Arc Consistency (AC-3)

Rather than merely searching, CSP solvers use Constraint Propagation: reasoning forward to prune impossible values from variable domains before search explores them.

Forward Checking

Whenever variable X is assigned value v, examine each unassigned neighbor Y connected to X by a constraint and delete from Y's domain any value inconsistent with v. If any domain becomes empty, backtrack immediately.

Limitation: It only checks forward from the newly assigned variable to its immediate neighbors. It does not detect indirect inconsistencies between unassigned neighbors.

Arc Consistency (AC-3 Algorithm)

A directed arc (Xi, Xj) is arc-consistent if for every value x in domain Di, there exists at least one value y in Dj that satisfies the constraint.

AC-3 Workflow: Maintains a queue of all directed arcs. When an arc (Xi, Xj) has values pruned from Di, all incoming arcs (Xk, Xi) must be re-added to the queue because pruning Di might make Xk arc-inconsistent! AC-3 propagates constraints transitively across the entire network.

08

Complete Python Implementations: Search & CSP

Below is a complete, production-grade Python script demonstrating: Graph Search with Breadth-First Search (BFS), Uniform-Cost Search (UCS), and A* Search on Romania's road map, followed by a Constraint Satisfaction Backtracking Solver for Map Coloring with the MRV heuristic and Forward Checking.

ai_search_and_csp.py β€” Classical Search & CSP Engine
"""
AI Track Unit 3: Classical Search Algorithms and CSP Solver
Russell & Norvig AIMA 4th Edition Compliant
Author: DevAscent Engineering Team
"""

import heapq
from collections import deque
from typing import Dict, List, Tuple, Optional, Set

# =====================================================================
# 1. ROMANIA ROAD MAP GRAPH DEFINITION
# =====================================================================
ROMANIA_MAP: Dict[str, List[Tuple[str, int]]] = {
    'Arad': [('Zerind', 75), ('Sibiu', 140), ('Timisoara', 118)],
    'Zerind': [('Arad', 75), ('Oradea', 71)],
    'Oradea': [('Zerind', 71), ('Sibiu', 151)],
    'Sibiu': [('Arad', 140), ('Oradea', 151), ('Fagaras', 99), ('Rimnicu Vilcea', 80)],
    'Timisoara': [('Arad', 118), ('Lugoj', 111)],
    'Lugoj': [('Timisoara', 111), ('Mehadia', 70)],
    'Mehadia': [('Lugoj', 70), ('Drobeta', 75)],
    'Drobeta': [('Mehadia', 75), ('Craiova', 120)],
    'Craiova': [('Drobeta', 120), ('Rimnicu Vilcea', 146), ('Pitesti', 138)],
    'Rimnicu Vilcea': [('Sibiu', 80), ('Craiova', 146), ('Pitesti', 97)],
    'Fagaras': [('Sibiu', 99), ('Bucharest', 211)],
    'Pitesti': [('Rimnicu Vilcea', 97), ('Craiova', 138), ('Bucharest', 101)],
    'Bucharest': [('Fagaras', 211), ('Pitesti', 101), ('Giurgiu', 90), ('Urziceni', 85)],
    'Giurgiu': [('Bucharest', 90)],
    'Urziceni': [('Bucharest', 85), ('Vaslui', 142), ('Hirsova', 98)],
    'Hirsova': [('Urziceni', 98), ('Eforie', 86)],
    'Eforie': [('Hirsova', 86)],
    'Vaslui': [('Urziceni', 142), ('Iasi', 92)],
    'Iasi': [('Vaslui', 92), ('Neamt', 87)],
    'Neamt': [('Iasi', 87)]
}

# Straight-Line Distance Heuristic h(n) to Bucharest (Admissible and Consistent)
SLD_TO_BUCHAREST: Dict[str, int] = {
    'Arad': 366, 'Bucharest': 0, 'Craiova': 160, 'Drobeta': 242,
    'Eforie': 161, 'Fagaras': 176, 'Giurgiu': 77, 'Hirsova': 151,
    'Iasi': 226, 'Lugoj': 244, 'Mehadia': 241, 'Neamt': 234,
    'Oradea': 380, 'Pitesti': 100, 'Rimnicu Vilcea': 193, 'Sibiu': 253,
    'Timisoara': 329, 'Urziceni': 80, 'Vaslui': 199, 'Zerind': 374
}

# =====================================================================
# 2. BREADTH-FIRST SEARCH (BFS)
# =====================================================================
def breadth_first_search(graph: Dict[str, List[Tuple[str, int]]], start: str, goal: str):
    """
    Standard BFS using a FIFO queue.
    Early goal test is applied on child generation.
    """
    if start == goal:
        return [start], 0

    frontier = deque([start])
    explored: Set[str] = {start}
    parent_map: Dict[str, str] = {}
    path_cost_map: Dict[str, int] = {start: 0}

    while frontier:
        current = frontier.popleft()

        for neighbor, weight in graph.get(current, []):
            if neighbor not in explored and neighbor not in frontier:
                parent_map[neighbor] = current
                path_cost_map[neighbor] = path_cost_map[current] + weight

                # Early Goal Test for BFS
                if neighbor == goal:
                    path = []
                    curr = goal
                    while curr in parent_map:
                        path.append(curr)
                        curr = parent_map[curr]
                    path.append(start)
                    path.reverse()
                    return path, path_cost_map[goal]

                frontier.append(neighbor)
                explored.add(neighbor)

    return None, float('inf')

# =====================================================================
# 3. UNIFORM-COST SEARCH (UCS) / DIJKSTRA
# =====================================================================
def uniform_cost_search(graph: Dict[str, List[Tuple[str, int]]], start: str, goal: str):
    """
    Uniform-Cost Search using priority queue ordered by path cost g(n).
    Goal test applied on POPPING node for expansion.
    """
    # Priority queue stores tuples: (cumulative_g, node_state, path_list)
    pq = [(0, start, [start])]
    explored: Dict[str, int] = {}

    while pq:
        cost, current, path = heapq.heappop(pq)

        # Goal Test on Removal (Guarantees Optimality)
        if current == goal:
            return path, cost

        if current in explored and explored[current] <= cost:
            continue
        explored[current] = cost

        for neighbor, weight in graph.get(current, []):
            new_cost = cost + weight
            if neighbor not in explored or new_cost < explored[neighbor]:
                heapq.heappush(pq, (new_cost, neighbor, path + [neighbor]))

    return None, float('inf')

# =====================================================================
# 4. A* SEARCH (A-STAR)
# =====================================================================
def a_star_search(graph: Dict[str, List[Tuple[str, int]]], 
                  heuristic: Dict[str, int], 
                  start: str, 
                  goal: str):
    """
    A* Graph Search using f(n) = g(n) + h(n).
    Optimal with consistent heuristic (SLD).
    """
    # Priority queue stores: (f_score, g_cost, state, path)
    initial_f = 0 + heuristic[start]
    pq = [(initial_f, 0, start, [start])]
    best_g: Dict[str, int] = {start: 0}

    nodes_expanded = 0

    while pq:
        f, g, current, path = heapq.heappop(pq)
        nodes_expanded += 1

        # Goal test when popped
        if current == goal:
            return path, g, nodes_expanded

        # If we already found a cheaper route to this state, skip
        if g > best_g.get(current, float('inf')):
            continue

        for neighbor, weight in graph.get(current, []):
            new_g = g + weight
            if new_g < best_g.get(neighbor, float('inf')):
                best_g[neighbor] = new_g
                new_f = new_g + heuristic[neighbor]
                heapq.heappush(pq, (new_f, new_g, neighbor, path + [neighbor]))

    return None, float('inf'), nodes_expanded

# =====================================================================
# 5. CONSTRAINT SATISFACTION PROBLEM (CSP) SOLVER
# Map Coloring with Minimum Remaining Values (MRV) & Forward Checking
# =====================================================================
class MapColoringCSP:
    def __init__(self, variables: List[str], domains: Dict[str, List[str]], neighbors: Dict[str, List[str]]):
        self.variables = variables
        self.domains = {var: list(domains[var]) for var in variables}
        self.neighbors = neighbors

    def is_consistent(self, var: str, value: str, assignment: Dict[str, str]) -> bool:
        """Check if assigning value to var violates any constraint with already assigned neighbors."""
        for neighbor in self.neighbors.get(var, []):
            if neighbor in assignment and assignment[neighbor] == value:
                return False
        return True

    def select_unassigned_variable_mrv(self, assignment: Dict[str, str]) -> str:
        """Minimum Remaining Values (MRV): Pick variable with smallest legal domain."""
        unassigned = [v for v in self.variables if v not in assignment]
        return min(unassigned, key=lambda var: len(self.domains[var]))

    def forward_check(self, var: str, value: str, assignment: Dict[str, str]) -> Optional[Dict[str, List[str]]]:
        """
        Prune 'value' from unassigned neighbors.
        Returns pruned dictionary snapshot or None if domain wipeout occurs.
        """
        pruned = {}
        for neighbor in self.neighbors.get(var, []):
            if neighbor not in assignment:
                if value in self.domains[neighbor]:
                    if len(self.domains[neighbor]) == 1:
                        # Domain wipeout!
                        return None
                    self.domains[neighbor].remove(value)
                    pruned.setdefault(neighbor, []).append(value)
        return pruned

    def restore_domains(self, pruned: Dict[str, List[str]]):
        """Restore pruned values upon backtracking."""
        for var, values in pruned.items():
            self.domains[var].extend(values)

    def backtrack(self, assignment: Dict[str, str]) -> Optional[Dict[str, str]]:
        """Backtracking search with MRV and Forward Checking."""
        if len(assignment) == len(self.variables):
            return assignment

        var = self.select_unassigned_variable_mrv(assignment)

        for value in list(self.domains[var]):
            if self.is_consistent(var, value, assignment):
                assignment[var] = value
                pruned = self.forward_check(var, value, assignment)

                if pruned is not None:
                    result = self.backtrack(assignment)
                    if result is not None:
                        return result
                    self.restore_domains(pruned)

                del assignment[var]

        return None

# =====================================================================
# DEMONSTRATION HARNESS
# =====================================================================
if __name__ == "__main__":
    print("=" * 68)
    print("DEMO 1: CLASSICAL PATH SEARCH ON ROMANIA ROAD NETWORK")
    print("Origin: Arad  -->  Destination: Bucharest")
    print("=" * 68)

    # 1. Breadth-First Search
    bfs_path, bfs_cost = breadth_first_search(ROMANIA_MAP, 'Arad', 'Bucharest')
    print(f"[BFS] Path: {' -> '.join(bfs_path)}")
    print(f"[BFS] Cumulative Cost: {bfs_cost} km (Note: Sub-optimal! BFS minimizes hops, not cost)\n")

    # 2. Uniform-Cost Search
    ucs_path, ucs_cost = uniform_cost_search(ROMANIA_MAP, 'Arad', 'Bucharest')
    print(f"[UCS] Path: {' -> '.join(ucs_path)}")
    print(f"[UCS] Cumulative Cost: {ucs_cost} km (Guaranteed Optimal!)\n")

    # 3. A* Search
    astar_path, astar_cost, expanded = a_star_search(ROMANIA_MAP, SLD_TO_BUCHAREST, 'Arad', 'Bucharest')
    print(f"[A* ] Path: {' -> '.join(astar_path)}")
    print(f"[A* ] Cumulative Cost: {astar_cost} km (Optimal & Efficient)")
    print(f"[A* ] Total Nodes Expanded: {expanded}\n")

    print("=" * 68)
    print("DEMO 2: MAP COLORING CSP (AUSTRALIA 7 REGIONS, 3 COLORS)")
    print("=" * 68)

    regions = ['WA', 'NT', 'SA', 'Q', 'NSW', 'V', 'T']
    colors = ['Red', 'Green', 'Blue']
    initial_domains = {r: colors.copy() for r in regions}
    adjacencies = {
        'WA': ['NT', 'SA'],
        'NT': ['WA', 'SA', 'Q'],
        'SA': ['WA', 'NT', 'Q', 'NSW', 'V'],
        'Q':  ['NT', 'SA', 'NSW'],
        'NSW':['SA', 'Q', 'V'],
        'V':  ['SA', 'NSW'],
        'T':  []  # Tasmania is an island (isolated)
    }

    csp = MapColoringCSP(regions, initial_domains, adjacencies)
    solution = csp.backtrack({})
    print("CSP Solution Found via MRV + Forward Checking:")
    for region, color in sorted(solution.items()):
        print(f"  {region:5} : {color}")
09

Viva Voce & Engineering Interview Questions

Q1: Why does Depth-First Search (DFS) require only linear memory while Breadth-First Search (BFS) explodes exponentially?

Answer: At any point during exploration, BFS must retain the entire boundary of the search tree at depth d in its FIFO queue, requiring O(b^d) storage. In contrast, DFS only maintains the current path from the root down to the active leaf node, along with the unexpanded sibling nodes at each depth level along that single branch. The maximum memory consumed by DFS is therefore b * m nodes (where b is the branching factor and m is the maximum tree depth), which is purely linear.

Q2: Why is Iterative Deepening Search (IDS) practically preferred over BFS despite repeatedly re-generating nodes from earlier levels?

Answer: In an exponential search tree, the number of nodes at bottom levels completely dominates the sum of all higher levels. Specifically, the bottom level contains approximately b^d nodes, while all higher levels combined contain b^d / (b - 1). When b = 10, re-generating earlier levels in IDS introduces an overhead of only ~11%. In return for this tiny 11% time overhead, IDS provides the linear space complexity of DFS (O(b * d)) while retaining the completeness and optimality of BFS. BFS typically crashes modern systems by running out of memory at depth 10, whereas IDS executes smoothly within megabytes of RAM.

Q3: Why must Uniform-Cost Search (UCS) and A* apply the goal test only when a node is removed from the priority queue, rather than when it is generated?

Answer: In graphs with varying edge costs, the first path that reaches a goal node is not necessarily the cheapest. A node might be generated via a high-cost edge while an alternate, optimal route is still queued with lower accumulated cost. By postponing the goal test until a node is popped for expansion, the priority queue mathematically guarantees that all nodes with smaller path costs have already been evaluated and no cheaper path to that goal can exist.

Q4: What is the formal difference between an Admissible heuristic and a Consistent (Monotonic) heuristic? Can an admissible heuristic be inconsistent?

Answer: An admissible heuristic never overestimates the true cost to the goal (0 <= h(n) <= h*(n)). A consistent heuristic enforces the triangle inequality: for every node n and successor n', h(n) <= StepCost(n, a, n') + h(n'). Every consistent heuristic is admissible, but an admissible heuristic is not necessarily consistent. Inconsistency occurs when a parent has a high admissible estimate, but moving to an adjacent node causes the heuristic estimate to drop precipitously by more than the edge cost. In tree search, admissibility alone suffices for optimality. In graph search, consistency is required to guarantee that when a state is first expanded, its path cost is strictly minimal.

Q5: How does Simulated Annealing escape local maxima, and what role does Temperature (T) play?

Answer: When simulated annealing evaluates a neighbor state that degrades fitness (Delta E < 0), instead of immediately rejecting it like hill climbing, it accepts it with probability P = e^(Delta E / T). The parameter Temperature T controls the degree of exploration: at high T, Delta E / T is close to zero, making P close to 1 (accepting almost all moves and wandering freely to escape deep local basins). As T gradually cools according to the annealing schedule, P approaches 0 for negative moves, causing the algorithm to smoothly converge into pure greedy hill climbing to lock onto the global optimum.

Q6: In Constraint Satisfaction Problems, how does Forward Checking differ from Arc Consistency (AC-3)?

Answer: Forward Checking only looks one step ahead: whenever a variable X is assigned, it removes inconsistent values from the domains of immediate unassigned neighbors of X. However, it does not propagate constraints between unassigned neighbors. Arc Consistency (AC-3) performs comprehensive multi-hop constraint propagation. If pruning a value from X causes neighbor Y's domain to shrink, AC-3 adds all arcs leading into Y back into its priority queue to verify whether Y's shrunk domain now renders other variables inconsistent. AC-3 catches domain wipeouts and contradictions far earlier in the search tree than forward checking.

Q7: Explain the MRV and LCV heuristics in Backtracking CSPs. Why is MRV called "fail-first" while LCV is "fail-last"?

Answer: MRV (Minimum Remaining Values) selects the variable with the smallest legal domain. It is called "fail-first" because if a variable has few options (or is destined to fail), testing it immediately causes an early failure and prunes large subtrees near the top of the search tree. Once a variable is chosen, LCV (Least Constraining Value) selects the value that eliminates the fewest choices for neighboring unassigned variables. It is called "fail-last" because we want to uncover a valid solution on the very first try without backtracking.