🤖 AI Search Algorithms in Python
1. Breadth-First Search (BFS)
from collections import deque
def bfs(graph, start, goal):
"""
Breadth-First Search implementation
Returns: path from start to goal
"""
visited = set()
queue = deque([(start, [start])])
while queue:
node, path = queue.popleft()
if node == goal:
return path
if node not in visited:
visited.add(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
queue.append((neighbor, path + [neighbor]))
return None # No path found
# Example usage
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
path = bfs(graph, 'A', 'F')
print(f"BFS Path: {path}") # Output: ['A', 'C', 'F']
2. Depth-First Search (DFS)
def dfs(graph, start, goal, visited=None, path=None):
"""
Depth-First Search implementation (recursive)
Returns: path from start to goal
"""
if visited is None:
visited = set()
if path is None:
path = []
visited.add(start)
path = path + [start]
if start == goal:
return path
for neighbor in graph.get(start, []):
if neighbor not in visited:
result = dfs(graph, neighbor, goal, visited, path)
if result:
return result
return None
# Iterative version
def dfs_iterative(graph, start, goal):
stack = [(start, [start])]
visited = set()
while stack:
node, path = stack.pop()
if node == goal:
return path
if node not in visited:
visited.add(node)
for neighbor in reversed(graph.get(node, [])):
if neighbor not in visited:
stack.append((neighbor, path + [neighbor]))
return None
# Example usage
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
path = dfs(graph, 'A', 'F')
print(f"DFS Path: {path}") # Output: ['A', 'B', 'E', 'F']
3. Uniform Cost Search (Dijkstra's Algorithm)
import heapq
def uniform_cost_search(graph, start, goal):
"""
Uniform Cost Search (Dijkstra's Algorithm)
Returns: (cost, path) tuple
"""
frontier = [(0, start, [start])]
explored = set()
while frontier:
cost, node, path = heapq.heappop(frontier)
if node == goal:
return cost, path
if node not in explored:
explored.add(node)
for neighbor, edge_cost in graph.get(node, []):
if neighbor not in explored:
new_cost = cost + edge_cost
heapq.heappush(frontier, (new_cost, neighbor, path + [neighbor]))
return float('inf'), None
# Example usage with weighted graph
weighted_graph = {
'A': [('B', 4), ('C', 2)],
'B': [('D', 5), ('C', 1)],
'C': [('D', 8), ('E', 10)],
'D': [('E', 2), ('F', 6)],
'E': [('F', 3)],
'F': []
}
cost, path = uniform_cost_search(weighted_graph, 'A', 'F')
print(f"UCS - Cost: {cost}, Path: {path}") # Output: Cost: 13, Path: ['A', 'B', 'D', 'E', 'F']
4. A* Search Algorithm
import heapq
import math
def astar(graph, start, goal, heuristic):
"""
A* Search Algorithm
Returns: (cost, path) tuple
"""
frontier = [(heuristic(start, goal), 0, start, [start])]
explored = set()
while frontier:
f, g, node, path = heapq.heappop(frontier)
if node == goal:
return g, path
if node not in explored:
explored.add(node)
for neighbor, edge_cost in graph.get(node, []):
if neighbor not in explored:
new_g = g + edge_cost
new_f = new_g + heuristic(neighbor, goal)
heapq.heappush(frontier, (new_f, new_g, neighbor, path + [neighbor]))
return float('inf'), None
# Example with 2D grid and Manhattan distance heuristic
def manhattan_distance(node, goal):
return abs(node[0] - goal[0]) + abs(node[1] - goal[1])
# Grid-based graph example
def create_grid_graph(width, height, obstacles=[]):
graph = {}
for x in range(width):
for y in range(height):
if (x, y) not in obstacles:
neighbors = []
for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
nx, ny = x + dx, y + dy
if 0 <= nx < width and 0 <= ny < height and (nx, ny) not in obstacles:
neighbors.append(((nx, ny), 1))
graph[(x, y)] = neighbors
return graph
# Example usage
grid = create_grid_graph(5, 5, obstacles=[(2, 2), (2, 3)])
cost, path = astar(grid, (0, 0), (4, 4), manhattan_distance)
print(f"A* - Cost: {cost}, Path: {path}")
5. Greedy Best-First Search
import heapq
def greedy_best_first_search(graph, start, goal, heuristic):
"""
Greedy Best-First Search
Returns: path from start to goal
"""
frontier = [(heuristic(start, goal), start, [start])]
explored = set()
while frontier:
h, node, path = heapq.heappop(frontier)
if node == goal:
return path
if node not in explored:
explored.add(node)
for neighbor in graph.get(node, []):
if neighbor not in explored:
h_value = heuristic(neighbor, goal)
heapq.heappush(frontier, (h_value, neighbor, path + [neighbor]))
return None
# Example with straight-line distance heuristic
def euclidean_distance(node, goal):
return math.sqrt((node[0] - goal[0])**2 + (node[1] - goal[1])**2)
# Graph with coordinates
coord_graph = {
(0, 0): [(1, 0), (0, 1)],
(1, 0): [(0, 0), (2, 0), (1, 1)],
(2, 0): [(1, 0), (2, 1)],
(0, 1): [(0, 0), (0, 2), (1, 1)],
(1, 1): [(1, 0), (0, 1), (2, 1), (1, 2)],
(2, 1): [(2, 0), (1, 1), (2, 2)],
(0, 2): [(0, 1), (1, 2)],
(1, 2): [(0, 2), (1, 1), (2, 2)],
(2, 2): [(2, 1), (1, 2)]
}
path = greedy_best_first_search(coord_graph, (0, 0), (2, 2), euclidean_distance)
print(f"Greedy Path: {path}")
6. Constraint Satisfaction Problem (CSP) Solver
class CSP:
"""Constraint Satisfaction Problem solver"""
def __init__(self, variables, domains, constraints):
self.variables = variables
self.domains = domains
self.constraints = constraints
def is_consistent(self, assignment, var, value):
"""Check if assigning value to var is consistent"""
assignment[var] = value
for constraint in self.constraints:
if not constraint(assignment):
del assignment[var]
return False
del assignment[var]
return True
def backtrack(self, assignment={}):
"""Backtracking search for CSP"""
if len(assignment) == len(self.variables):
return assignment
# Select unassigned variable
var = None
for v in self.variables:
if v not in assignment:
var = v
break
# Try each value in domain
for value in self.domains[var]:
if self.is_consistent(assignment, var, value):
assignment[var] = value
result = self.backtrack(assignment)
if result:
return result
del assignment[var]
return None
def solve(self):
"""Solve the CSP"""
return self.backtrack()
# Example: Map Coloring Problem
def map_coloring_example():
# Australian states
variables = ['WA', 'NT', 'SA', 'Q', 'NSW', 'V', 'T']
domains = {var: ['red', 'green', 'blue'] for var in variables}
# Adjacency constraints
def constraint(assignment):
edges = [('WA', 'NT'), ('WA', 'SA'), ('NT', 'SA'), ('NT', 'Q'),
('SA', 'Q'), ('SA', 'NSW'), ('SA', 'V'), ('Q', 'NSW'),
('NSW', 'V')]
for v1, v2 in edges:
if v1 in assignment and v2 in assignment:
if assignment[v1] == assignment[v2]:
return False
return True
csp = CSP(variables, domains, [constraint])
solution = csp.solve()
return solution
# N-Queens Problem
def n_queens_csp(n=8):
variables = list(range(n)) # Columns
domains = {col: list(range(n)) for col in variables} # Possible rows
def constraint(assignment):
for c1 in assignment:
for c2 in assignment:
if c1 < c2:
r1, r2 = assignment[c1], assignment[c2]
# Same row or diagonal
if r1 == r2 or abs(r1 - r2) == abs(c1 - c2):
return False
return True
csp = CSP(variables, domains, [constraint])
return csp.solve()
# Example usage
print("Map Coloring Solution:")
print(map_coloring_example())
print("\n8-Queens Solution (row positions for each column):")
print(n_queens_csp(8))
7. Min-Conflicts Algorithm for CSP
import random
def min_conflicts(csp, max_steps=1000):
"""
Min-conflicts algorithm for CSP
Local search approach
"""
# Generate random complete assignment
assignment = {}
for var in csp.variables:
assignment[var] = random.choice(csp.domains[var])
for _ in range(max_steps):
# Check if current assignment is solution
if all(constraint(assignment) for constraint in csp.constraints):
return assignment
# Find conflicted variable
conflicted = []
for var in csp.variables:
# Check if var is in conflict
temp = assignment[var]
for constraint in csp.constraints:
if not constraint(assignment):
conflicted.append(var)
break
if not conflicted:
return assignment
# Pick random conflicted variable
var = random.choice(conflicted)
# Find value that minimizes conflicts
min_conflicts_count = float('inf')
best_values = []
for value in csp.domains[var]:
assignment[var] = value
conflicts = sum(1 for c in csp.constraints if not c(assignment))
if conflicts < min_conflicts_count:
min_conflicts_count = conflicts
best_values = [value]
elif conflicts == min_conflicts_count:
best_values.append(value)
# Choose random best value
assignment[var] = random.choice(best_values)
return None # No solution found
# Example: Sudoku solver
def sudoku_csp():
"""Create a simple 4x4 Sudoku CSP"""
variables = [(i, j) for i in range(4) for j in range(4)]
domains = {var: [1, 2, 3, 4] for var in variables}
def constraint(assignment):
# Check rows
for i in range(4):
row = [assignment.get((i, j)) for j in range(4) if (i, j) in assignment]
if len(row) != len(set(row)): # Duplicates
return False
# Check columns
for j in range(4):
col = [assignment.get((i, j)) for i in range(4) if (i, j) in assignment]
if len(col) != len(set(col)):
return False
# Check 2x2 boxes
for box_r in range(0, 4, 2):
for box_c in range(0, 4, 2):
box = []
for i in range(box_r, box_r + 2):
for j in range(box_c, box_c + 2):
if (i, j) in assignment:
box.append(assignment[(i, j)])
if len(box) != len(set(box)):
return False
return True
class SudokuCSP:
def __init__(self):
self.variables = variables
self.domains = domains
self.constraints = [constraint]
return min_conflicts(SudokuCSP())
print("4x4 Sudoku solution using Min-Conflicts:")
solution = sudoku_csp()
if solution:
for i in range(4):
print([solution[(i, j)] for j in range(4)])
else:
print("No solution found")
8. N-Queens CSP
def solve_n_queens(n):
"""Solve N-Queens problem using backtracking"""
def is_safe(board, row, col):
# Check column
for i in range(row):
if board[i] == col:
return False
# Check diagonals
for i in range(row):
if abs(board[i] - col) == abs(i - row):
return False
return True
def backtrack(board, row):
if row == n:
return board[:]
for col in range(n):
if is_safe(board, row, col):
board[row] = col
result = backtrack(board, row + 1)
if result:
return result
board[row] = -1
return None
board = [-1] * n
solution = backtrack(board, 0)
if solution:
# Print the board
for row in range(n):
line = ['Q' if solution[row] == col else '.' for col in range(n)]
print(' '.join(line))
return solution
return None
# Solve 8-Queens
print("8-Queens Solution:")
solve_n_queens(8)
9. Hill Climbing
import random
def hill_climbing(problem, max_iterations=1000):
"""Hill climbing local search algorithm"""
current = problem.initial_state()
for _ in range(max_iterations):
neighbors = problem.get_neighbors(current)
if not neighbors:
break
# Find best neighbor
best_neighbor = max(neighbors, key=problem.evaluate)
if problem.evaluate(best_neighbor) <= problem.evaluate(current):
break # Local maximum reached
current = best_neighbor
return current
# Example: Maximize function f(x) = -x^2 + 10x
class FunctionOptimization:
def initial_state(self):
return random.uniform(-10, 10)
def get_neighbors(self, x, step=0.1):
return [x - step, x + step]
def evaluate(self, x):
return -x**2 + 10*x
problem = FunctionOptimization()
result = hill_climbing(problem)
print(f"Hill Climbing result: x={result:.2f}, f(x)={problem.evaluate(result):.2f}")
10. Greedy
import heapq
def gbfs_path(adj, names, tone, start_idx, exit_idx):
# Priority queue sorted by (tone, name, idx)
pq = [(tone[start_idx], names[start_idx], start_idx)]
seen = {start_idx}
parent = [-1] * len(names)
while pq:
_, _, u = heapq.heappop(pq)
if u == exit_idx:
path = []
while u != -1:
path.append(names[u])
u = parent[u]
return path[::-1]
for v in adj[u]:
if v not in seen:
seen.add(v)
parent[v] = u
heapq.heappush(pq, (tone[v], names[v], v))
return None
def main():
# --- Hardcoded sample ---
N, M = 7, 8
names = ["A", "B", "C", "D", "E", "F", "G"]
tone = [12, 7, 6, 5, 3, 9, 1] # must be before function call
edges = [
("A", "B"),
("A", "C"),
("B", "D"),
("C", "D"),
("C", "E"),
("D", "F"),
("E", "G"),
("F", "G"),
("A", "G"), # extra edge
]
# Build index map
idx = {name: i for i, name in enumerate(names)}
# Build adjacency list
adj = [[] for _ in range(N)]
for u, v in edges[:M]: # only take first M edges
ui, vi = idx[u], idx[v]
adj[ui].append(vi)
adj[vi].append(ui)
start_name, exit_name = "A", "G"
s, t = idx[start_name], idx[exit_name]
if s == t:
print(start_name)
return
path = gbfs_path(adj, names, tone, s, t)
print(-1 if path is None else " ".join(path))
if name == "main":
main()
11. Timetable Scheduling Problem
# Timetable Scheduling Problem
def build_graph(N, edges):
"""Build adjacency list for N classes given conflict edges"""
adj = [[] for _ in range(N)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
return adj
def conflicts_with(u, slot_u, adj, assign):
"""Check if assigning class u -> slot_u conflicts with neighbors"""
for v in adj[u]:
if assign[v] != -1 and assign[v] == slot_u:
return True
return False
def pick_slot_for_class(u, S, adj, assign, fixed):
"""Pick a valid slot for class u"""
if fixed[u] != -1: # if fixed slot exists
return fixed[u]
for slot in range(S):
if not conflicts_with(u, slot, adj, assign):
return slot
return -1 # should never happen if input is valid
def solve_case(N, S, edges, fixed_pairs):
"""Solve one test case"""
adj = build_graph(N, edges)
assign = [-1] * N
fixed = [-1] * N
# Apply fixed slots
for x, k in fixed_pairs:
fixed[x] = k
assign[x] = k
# Assign classes in order
for u in range(N):
if assign[u] == -1:
assign[u] = pick_slot_for_class(u, S, adj, assign, fixed)
return assign
def main():
T = int(input("Enter number of test cases: "))
for _ in range(T):
N, S, M, F = map(int, input("Enter N S M F: ").split())
edges = []
print(f"Enter {M} conflict pairs (u v):")
for _ in range(M):
u, v = map(int, input().split())
edges.append((u, v))
fixed_pairs = []
print(f"Enter {F} fixed pairs (x k):")
for _ in range(F):
x, k = map(int, input().split())
fixed_pairs.append((x, k))
ans = solve_case(N, S, edges, fixed_pairs)
print("Output:", " ".join(map(str, ans)))
if name == "main":
main()
12. Maze
import heapq
def solve(N, M, grid, start_x, start_y, crystal_x, crystal_y):
if (start_x, start_y) == (crystal_x, crystal_y):
print(0)
return
INF = 10**18
dist = [[INF] * M for _ in range(N)]
dist[start_x][start_y] = 0
# Min-heap: (time_so_far, x, y)
pq = [(0, start_x, start_y)]
directions = [(1,0), (-1,0), (0,1), (0,-1)]
while pq:
g, x, y = heapq.heappop(pq)
if g != dist[x][y]:
continue
if (x, y) == (crystal_x, crystal_y):
print(g)
return
for dx, dy in directions:
nx = x + dx,
ny = y + dy
if 0 <= nx < N and 0 <= ny < M and grid[nx][ny] != '#':
if grid[nx][ny] == '.':
step = 1
else:
step= 3 # entering cost
ng = g + step
if ng < dist[nx][ny]:
dist[nx][ny] = ng
heapq.heappush(pq, (ng, nx, ny))
print(-1) # unreachable
if name == "main":
# Read inputs using input()
N, M = map(int, input().split())
grid = [input().strip() for _ in range(N)]
start_x, start_y = map(int, input().split())
crystal_x, crystal_y = map(int, input().split())
solve(N, M, grid, start_x, start_y, crystal_x, crystal_y)