0% found this document useful (0 votes)
56 views17 pages

Lab Problem 01 AI

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
56 views17 pages

Lab Problem 01 AI

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 17

Breadth First Search

Here's a Python implementation of Breadth First Search (BFS) for graphs


represented using an adjacency list:
Python
from collections import deque

def bfs(graph, start):


"""
Performs BFS traversal on a graph represented by an adjacency list.

Args:
graph: A dictionary where keys are nodes and values are lists of
neighbors.
start: The starting node for the traversal.

Returns:
A list containing the nodes visited in BFS order.
"""
visited = set() # Keep track of visited nodes
queue = deque([start]) # Queue for BFS traversal

while queue:
current_node = queue.popleft()
visited.add(current_node)

for neighbor in graph[current_node]:


if neighbor not in visited:
queue.append(neighbor)
visited.add(neighbor)

return visited

# Example usage
graph = {
0: [1, 2],
1: [2, 3],
2: [0, 1, 4],
3: [1],
4: [2]
}

starting_node = 0
bfs_result = bfs(graph, starting_node)
print(f"BFS traversal starting from {starting_node}: {bfs_result}")
This code defines a function bfs that takes a graph (represented as an
adjacency list) and a starting node as input. It uses two data structures:
 visited: A set to keep track of nodes that have already been visited
during the traversal.
 queue: A deque (double-ended queue) to implement the BFS behavior.
Nodes are added to the back of the queue and removed from the front.
The algorithm works as follows:
1. Initialize visited and queue with the starting node added to the queue.
2. While the queue is not empty:
o Dequeue a node from the queue and mark it as visited.
o For each unvisited neighbor of the dequeued node:
 Add the neighbor to the back of the queue.
 Mark the neighbor as visited.
3. Return the set of visited nodes.
The example usage demonstrates how to use the bfs function with a sample
graph and starting node. It then prints the BFS traversal order.

Uniform-Cost Search

Here's a Python implementation of Uniform Cost Search (UCS):


Python
from heapq import heappush, heappop

class Node:
def __init__(self, state, parent=None, cost=0):
self.state = state
self.parent = parent
self.cost = cost

def __lt__(self, other):


return self.cost < other.cost

def uniform_cost_search(graph, start, goal):


"""
Performs Uniform Cost Search on a graph.

Args:
graph: A dictionary representing the graph. Keys are nodes, values are
dictionaries where keys are neighbors and values are edge costs.
start: The starting node.
goal: The goal node.

Returns:
A list of nodes representing the path from start to goal, or None if no
path is found.
"""
frontier = []
explored = set()
heappush(frontier, Node(start))

while frontier:
current = heappop(frontier)
explored.add(current.state)

if current.state == goal:
# Found the goal! Reconstruct the path
path = []
while current:
path.append(current.state)
current = current.parent
return path[::-1]

for neighbor, cost in graph[current.state].items():


if neighbor not in explored:
new_cost = current.cost + cost
heappush(frontier, Node(neighbor, current, new_cost))

return None # No path found

# Example usage
graph = {
'A': {'B': 1, 'C': 3},
'B': {'D': 2, 'E': 4},
'C': {'D': 5},
'D': {'E': 1},
'E': {}
}

start = 'A'
goal = 'E'

path = uniform_cost_search(graph, start, goal)

if path:
print("Path found:", path)
else:
print("No path found")
This code defines a Node class to represent nodes in the graph. The
uniform_cost_search function takes a graph, starting node, and goal node as
input. It uses a priority queue (implemented with heapq) to explore nodes
with the lowest cost first. The function returns a list of nodes representing
the path from start to goal, or None if no path is found.
This is a basic implementation and can be extended to include features like:
 Handling different data structures for representing graphs (e.g.,
adjacency lists)
 Heuristic functions to guide the search towards the goal
 Error handling for invalid inputs

Depth First Search

Here's a Python implementation of Depth-First Search (DFS):


Python
def depth_first_search(graph, start, visited=None):
"""
Performs Depth-First Search on a graph.

Args:
graph: A dictionary representing the graph. Keys are nodes, values are
lists of neighbors.
start: The starting node.
visited: An optional set to track visited nodes (default None).

Returns:
A list of nodes visited in the DFS traversal.
"""
if visited is None:
visited = set() # Initialize visited set if not provided

visited.add(start)
print(start, end=" ") # You can modify this line to perform actions on
visited nodes

for neighbor in graph[start]:


if neighbor not in visited:
depth_first_search(graph, neighbor, visited)

# Example usage
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}

start = 'A'

depth_first_search(graph.copy(), start) # Use a copy to avoid modifying


original graph
This code uses recursion to traverse the graph. It keeps track of visited
nodes in a set to avoid revisiting them. The print(start, end=" ") line
demonstrates visiting a node, you can replace it with your desired action on
each visited node.
Note:
 This implementation uses recursion, an iterative version using a stack
can also be achieved.
 The code uses a copy of the graph during DFS to avoid modifying the
original graph in the example.

Depth Limited Search


Here's a Python implementation of Depth-Limited Search (DLS):
Python
def depth_limited_search(graph, start, goal, depth):
"""
Performs Depth-Limited Search on a graph with a maximum depth limit.

Args:
graph: A dictionary representing the graph. Keys are nodes, values are
lists of neighbors.
start: The starting node.
goal: The goal node.
depth: The maximum depth limit for the search.
Returns:
"cutoff" if the depth limit is reached before finding the goal.
"failure" if the goal is not found within the depth limit.
A list of nodes representing the path to the goal if found, otherwise None.
"""
def recursive_dls(node, current_depth):
if current_depth == depth:
return "cutoff" # Reached depth limit
if node == goal:
return [node] # Found the goal!

for neighbor in graph[node]:


result = recursive_dls(neighbor, current_depth + 1)
if result is not None:
return [node] + result # Prepend current node to path

return "failure" # No goal found in this branch

result = recursive_dls(start, 0)
if result == "cutoff":
return "cutoff"
elif result == "failure":
return "failure"
else:
return result # Return the path

# Example usage
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': ['G']
}

start = 'A'
goal = 'G'
depth = 3

result = depth_limited_search(graph.copy(), start, goal, depth)

print(result)
This code defines two functions:
 depth_limited_search: This function takes the graph, starting node,
goal node, and depth limit as input. It calls the helper function
recursive_dls to perform the actual search.
 recursive_dls: This recursive function explores the graph up to the
specified depth. It returns "cutoff" if the depth limit is reached, "failure"
if the goal is not found within the limit, or a list of nodes representing
the path to the goal if found.
The code uses a copy of the graph during DLS to avoid modifying the original
graph in the example.
This implementation allows you to iteratively increase the depth limit until
the goal is found or all possibilities are exhausted.

Iterative Deeping Depth-First Search


Here's a Python implementation of Iterative Depth-First Search (IDFS):
Python
def iterative_depth_first_search(graph, start, goal):
"""
Performs Iterative Depth-First Search on a graph.

Args:
graph: A dictionary representing the graph. Keys are nodes, values are
lists of neighbors.
start: The starting node.
goal: The goal node.

Returns:
A list of nodes representing the path to the goal if found, otherwise None.
"""
for depth in range(1, float('inf')):
if depth_first_search_limited(graph, start, goal, depth):
return depth_first_search_limited(graph, start, goal, depth)

return None # Goal not found

def depth_first_search_limited(graph, start, goal, depth):


"""
Performs Depth-First Search on a graph with a maximum depth limit.

Args:
graph: A dictionary representing the graph. Keys are nodes, values are
lists of neighbors.
start: The starting node.
goal: The goal node.
depth: The maximum depth limit for the search.

Returns:
A list of nodes representing the path to the goal if found, otherwise None.
"""
stack = [(start, 0)]
visited = set()

while stack:
current, current_depth = stack.pop()
if current_depth > depth:
continue

if current == goal:
path = []
while stack:
node, _ = stack.pop()
path.append(node)
return path[::-1] # Reverse path to get start to goal order

if current not in visited:


visited.add(current)
for neighbor in graph[current]:
stack.append((neighbor, current_depth + 1))

return None # Goal not found within any depth limit

# Example usage
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': ['G']
}

start = 'A'
goal = 'G'

result = iterative_depth_first_search(graph.copy(), start, goal)


if result:
print("Path found:", result)
else:
print("Goal not found")
This code defines two functions:
 iterative_depth_first_search: This function iteratively increases the
depth limit until the goal is found or all possibilities are exhausted. It
calls the helper function depth_first_search_limited for each depth.
 depth_first_search_limited: This function performs a DFS with a
specified depth limit similar to the previous implementation. It uses a
stack to keep track of nodes to explore and their current depth.
The code uses a copy of the graph during both functions to avoid modifying
the original graph in the example.

Bidirectional Search
Here's a Python implementation of Bidirectional Search:
Python
from collections import deque

def bidirectional_search(graph, start, goal):


"""
Performs Bidirectional Search on a graph.

Args:
graph: A dictionary representing the graph. Keys are nodes, values are
dictionaries where keys are neighbors and values are edge costs (assumed
to be 1 in this implementation).
start: The starting node.
goal: The goal node.

Returns:
A list of nodes representing the path from start to goal if found,
otherwise None.
"""
forward_queue = deque([start])
forward_visited = set([start])
backward_queue = deque([goal])
backward_visited = set([goal])

while forward_queue and backward_queue:


# Explore forward frontier
current_forward = forward_queue.popleft()
for neighbor in graph[current_forward]:
if neighbor in backward_visited:
return reconstruct_path(current_forward, neighbor, forward_visited,
backward_visited)
if neighbor not in forward_visited:
forward_queue.append(neighbor)
forward_visited.add(neighbor)

# Explore backward frontier


current_backward = backward_queue.popleft()
for neighbor in graph[current_backward]:
if neighbor in forward_visited:
return reconstruct_path(current_backward, neighbor, backward_visited,
forward_visited)
if neighbor not in backward_visited:
backward_queue.append(neighbor)
backward_visited.add(neighbor)

return None # Path not found

def reconstruct_path(node1, meeting_point, visited1, visited2):


"""
Reconstructs the path from two search frontiers by backtracking from the
meeting point.

Args:
node1: Node from the forward search.
meeting_point: The node where the two searches met.
visited1: Set of visited nodes from the forward search.
visited2: Set of visited nodes from the backward search.

Returns:
A list of nodes representing the path from start to goal.
"""
path1 = []
while node1:
path1.append(node1)
node1 = get_parent(node1, visited1)
path2 = list(reversed([node for node in get_parents(meeting_point,
visited2)]))
return path1 + path2

def get_parent(node, visited):


"""
Helper function to retrieve the parent node from the visited set based on
search direction.

Args:
node: The current node.
visited: Set of visited nodes with parent information (modified for this
function).

Returns:
The parent node of the current node, or None if not found.
"""
for parent, child in visited:
if child == node:
return parent
return None

def get_parents(node, visited):


"""
Helper function to retrieve all parent nodes of the current node based on
search direction.

Args:
node: The current node.
visited: Set of visited nodes with parent information (modified for this
function).

Returns:
A list of parent nodes of the current node, or an empty list if not found.
"""
parents = []
for parent, child in visited:
if child == node:
parents.append(parent)
return parents

# Example usage
graph = {
'A': {'B': 1, 'C': 1},
'B': {'D': 1, 'E': 1},
'C': {'D': 1},
'D': {'E': 1},
'E': {}
}

start = 'A'
goal = 'E'
path = bidirectional_search(graph, start, goal)

if path:
print("Path found:", path)
else:
print("No path found")
This implementation uses two frontiers, one for the forward search starting
from the beginning and another for the backward search starting from the
goal. It explores both frontiers simultaneously until they meet at a common
node. The reconstruct_path function then backtracks from the meeting point
to build the complete path from start to goal.
Note:
 This implementation assumes edge costs are all 1. You can modify it to
handle different costs.
 The get_parent and get_parents functions are helper functions to
retrieve parent information from the visited sets, which are

Greedy Best First Search


Here's a Python implementation of Greedy Best-First Search (GBFS):
Python
from heapq import heappush, heappop

class Node:
def __init__(self, state, parent=None, heuristic_value=float('inf')):
self.state = state
self.parent = parent
self.heuristic_value = heuristic_value

def __lt__(self, other):


return self.heuristic_value < other.heuristic_value

def greedy_best_first_search(graph, start, goal, heuristic_fn):


"""
Performs Greedy Best-First Search on a graph.

Args:
graph: A dictionary representing the graph. Keys are nodes, values are
dictionaries where keys are neighbors and values are edge costs.
start: The starting node.
goal: The goal node.
heuristic_fn: A function that takes a state and returns its heuristic value
(estimated distance to goal).

Returns:
A list of nodes representing the path from start to goal if found,
otherwise None.
"""
frontier = []
explored = set()
heappush(frontier, Node(start, heuristic_value=heuristic_fn(start, goal)))

while frontier:
current = heappop(frontier)
explored.add(current.state)

if current.state == goal:
# Found the goal! Reconstruct the path
path = []
while current:
path.append(current.state)
current = current.parent
return path[::-1]

for neighbor, cost in graph[current.state].items():


if neighbor not in explored:
new_heuristic = heuristic_fn(neighbor, goal)
heappush(frontier, Node(neighbor, current, new_heuristic))

return None # No path found

# Example usage (assuming a simple distance heuristic)


def distance_heuristic(node, goal):
# Replace this with your specific heuristic function
# This example calculates the manhattan distance between node and goal
coordinates
# (assuming coordinates are available as node attributes)
return abs(node.x - goal.x) + abs(node.y - goal.y)

graph = {
'A': {'B': 1, 'C': 3},
'B': {'D': 2, 'E': 4},
'C': {'D': 5},
'D': {'E': 1},
'E': {}
}

start = 'A'
goal = 'E'

path = greedy_best_first_search(graph.copy(), start, goal, distance_heuristic)

if path:
print("Path found:", path)
else:
print("No path found")
This code defines a Node class to represent nodes in the graph with an
additional attribute for heuristic value. The greedy_best_first_search function
works similarly to Uniform Cost Search (UCS) but uses a priority queue to
prioritize nodes with lower heuristic values. The heuristic_fn argument is a
user-defined function that estimates the distance from a state to the goal.
Note:
 This example uses a simple distance heuristic. You'll need to replace it
with a more specific heuristic function depending on your problem
domain.
 The code uses a copy of the graph to avoid modifying the original
graph during the search.

A* Search
Here's a Python implementation of A* Search:
Python
from heapq import heappush, heappop

class Node:
def __init__(self, state, parent=None, g_score=0, h_score=0):
self.state = state
self.parent = parent
self.g_score = g_score # Cost from start to current node
self.h_score = h_score # Heuristic estimate of cost to goal
self.f_score = g_score + h_score # Total score (f = g + h)

def __lt__(self, other):


return self.f_score < other.f_score
def a_star_search(graph, start, goal, heuristic_fn):
"""
Performs A* Search on a graph.

Args:
graph: A dictionary representing the graph. Keys are nodes, values are
dictionaries where keys are neighbors and values are edge costs.
start: The starting node.
goal: The goal node.
heuristic_fn: A function that takes a state and returns its heuristic value
(estimated distance to goal).

Returns:
A list of nodes representing the path from start to goal if found,
otherwise None.
"""
frontier = []
explored = set()
start_node = Node(start)
start_node.h_score = heuristic_fn(start, goal)
start_node.f_score = start_node.g_score + start_node.h_score
heappush(frontier, start_node)

while frontier:
current = heappop(frontier)
explored.add(current.state)

if current.state == goal:
# Found the goal! Reconstruct the path
path = []
while current:
path.append(current.state)
current = current.parent
return path[::-1]

for neighbor, cost in graph[current.state].items():


if neighbor not in explored:
tentative_g_score = current.g_score + cost
if neighbor not in (node.state for node in frontier):
new_node = Node(neighbor, current, tentative_g_score)
new_node.h_score = heuristic_fn(neighbor, goal)
new_node.f_score = new_node.g_score + new_node.h_score
heappush(frontier, new_node)
else:
# Update node if new path is better (lower g score)
for node in frontier:
if node.state == neighbor and tentative_g_score < node.g_score:
node.parent = current
node.g_score = tentative_g_score
node.f_score = node.g_score + node.h_score
break

return None # No path found

# Example usage (assuming a simple distance heuristic)


def distance_heuristic(node, goal):
# Replace this with your specific heuristic function
# This example calculates the manhattan distance between node and goal
coordinates
# (assuming coordinates are available as node attributes)
return abs(node.x - goal.x) + abs(node.y - goal.y)

graph = {
'A': {'B': 1, 'C': 3},
'B': {'D': 2, 'E': 4},
'C': {'D': 5},
'D': {'E': 1},
'E': {}
}

start = 'A'
goal = 'E'

path = a_star_search(graph.copy(), start, goal, distance_heuristic)

if path:
print("Path found:", path)
else:
print("No path found")
This implementation builds upon Uniform Cost Search (UCS) and Greedy
Best-First Search (GBFS). It uses a priority queue to prioritize nodes based on
their f-score (f = g + h), where g_score is the cost from the start to the
current node and h_score is a heuristic estimate of the cost from the current
node to the goal. The code maintains a set of explored nodes to avoid
revisiting them.
Note:
 This example uses a simple distance heuristic. You'll need to replace it
with a more specific heuristic function depending on your problem
domain.
 The code uses a copy of the graph to avoid modifying the original
graph during the search.

You might also like