Lab 03 Uninformed Search
Lab 03 Uninformed Search
Lab 03
Intelligence Uninformed Search
AI-2002 Algorithms
1. Objective 3
2. Uninformed Search 3
2. Uninformed Search
Uninformed search (also called blind search) is a type of search algorithm used in
artificial intelligence and computer science to explore a problem space without any
additional information about the problem other than its definition. These algorithms do
not use any domain-specific knowledge or heuristics to guide the search. Instead, they
systematically explore the search space until a solution is found.
Tree Structure: The tree is represented as a dictionary, where each key is a parent
node, and the value is a list of its children. For example:
# tree Representation
tree = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F', 'G'],
'D': ['H'],
'E': [],
'F': ['I'],
'G': [],
'H': [],
'I': []
}
● Parameters:
1. graph: The tree to be traversed.
2. start: The node where the search begins.
3. goal: The node that the algorithm is trying to find.
● Process:
1. Initialize an empty list visited to track the nodes that have already been
explored.
# BFS Function
def bfs(tree, start, goal):
visited = [] # List for visited nodes
queue = [] # Initialize a queue
visited.append(start)
queue.append(start)
while queue:
node = queue.pop(0) # Dequeue
print(node, end=" ")
if node == goal: # Stop if goal is found
print("\nGoal found!")
break
for neighbour in graph[node]:
if neighbour not in visited:
visited.append(neighbour)
queue.append(neighbour)
Execution: To run the BFS search, define the start and goal nodes and then call the bfs
function with the graph:
start_node = 'A'
goal_node = 'I'
# Run BFS
print("\nFollowing is the Breadth-First Search (BFS):")
bfs(tree, start_node, goal_node)
● The function starts at node 'A' and explores the tree in a breadth-first manner.
● Each node is visited in the order of their depth (level by level).
● Once the goal node ('I') is reached, the search stops and prints "Goal found!". If
the goal is not found, it continues until all reachable nodes are explored.
We are now integrating the BFS code within the Agent and Environment terminology. In
this setup, the agent is goal-oriented and uses the BFS algorithm to navigate through the
environment, whether it's a tree, graph, or maze. The agent perceives its environment,
determines if it has reached its goal, and if not, it uses the BFS strategy to explore the
environment level by level, moving closer to the goal. The environment provides the
necessary structure (tree, graph, maze) and supports the BFS search process, enabling
the agent to find its way to the goal efficiently.
In this section, we define a Goal-Based Agent that performs a BFS search to navigate
through its environment. The agent formulates its goal and decides whether it has
reached the goal based on the current percept. If the goal is not reached, the agent
continues searching by invoking the BFS algorithm.
if self.is_goal(current):
return current # Goal found
# Explore neighbors
for neighbour in env.tree[current]:
if neighbour not in self.visited:
self.visited.append(neighbour)
self.queue.append(neighbour)
return None # Goal not found
Environment Class
The Environment class represents the environment in which the agent operates. It can
be a tree, graph, maze, or other structures. This class provides the bfs_search method
that allows the agent to perform the breadth-first search to find its goal.
The run_agent function simulates the interaction between the agent and the
environment. The agent starts at a given node, perceives its current state, and acts
accordingly by performing the BFS search. The goal is to continue exploring until the
goal is reached.
In this example, the environment is a tree structure, and the agent is tasked with finding the
node 'I' starting from 'A':
visited.append(start)
queue.append(start)
while queue:
node = queue.pop(0) # FIFO: Dequeue from front
print(node, end=" ")
# Run BFS
print("\nFollowing is the Breadth-First Search (BFS):")
bfs(graph, start_node, goal_node)
Tree Structure: The tree is represented as a dictionary, where each key is a parent
node, and the value is a list of its children. For example:.
# tree Representation
tree = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F', 'G'],
'D': ['H'],
'E': [],
'F': ['I'],
'G': [],
'H': [],
'I': []}
The function dfs(graph, start, goal) takes in a graph (in this case, the tree), a start node,
and a goal node, and performs a depth-first search to find the goal. The function utilizes
a stack and a visited list to track the nodes to explore.
1. Parameter:
The algorithm begins at the start node and initializes two data structures:
○ Visited list: To keep track of the nodes that have already been visited and
prevent revisiting them.
○ Stack: A stack is used to store nodes that need to be explored. Since DFS is
a Last In, First Out (LIFO) traversal, nodes are explored in depth-first
order.
2. Process:
○ The algorithm first pushes the start node onto the stack and marks it as
visited.
○ It then enters a loop where it pops nodes from the stack and explores
them one by one:
■ If the popped node is the goal node, the search stops.
■ If the popped node has unvisited neighbors, they are pushed onto
the stack for further exploration.
3. Backtracking:
○ If a node has no unvisited neighbors, the algorithm backtracks by popping
the next node from the stack and continues the search from there.
○ This backtracking ensures that the algorithm explores deeper branches of
the tree first before moving on to other branches.
4. Termination:
○ The algorithm terminates when the goal node is found, or if all possible
nodes have been visited without finding the goal.
# DFS Function
def dfs(graph, start, goal):
visited = [] # List for visited nodes
stack = [] # Initialize stack
visited.append(start)
stack.append(start)
while stack:
node = stack.pop() # LIFO: Pop from top
Execution: To run the DFS search, define the start and goal nodes and then call the dfs
function with the graph:
# Run DFS
print("\nFollowing is the Depth-First Search (DFS):")
dfs(graph, start_node, goal_node)
class Agent:
def __init__(self, env, goal):
self.goal = goal
self.env = env
self.visited = list()
self.stack = list()
while self.stack:
current = self.stack.pop(0)
print(current, end=" ")
if self.is_goal(current):
return current # Goal found
# Explore neighbors
for neighbour in
reversed(self.env.tree.get(current, [])):
if neighbour not in self.visited:
self.visited.append(neighbour)
self.stack.append(neighbour)
return None # Goal not found
Environment Class
The Environment class represents the environment in which the agent operates. It can
be a tree, graph, maze, or other structures. This class provides the dfs_search method
that allows the agent to perform the depth-first search to find its goal.
class Environment:
def __init__(self, tree):
self.tree = tree
The run_agent function simulates the interaction between the agent and the
environment. The agent starts at a given node, perceives its current state, and acts
accordingly by performing the DFS search. The goal is to continue exploring until the
goal is reached.
# Perform DFS
goal = agent.dfs(start)
if goal:
print("\nGoal found!")
else:
print("\nGoal not reachable.")
This technique is particularly useful when the search space is large, and we want to
avoid exploring unnecessary deep branches of the graph.
The environment is represented as an unweighted graph, where each key is a node, and
its associated value is a list of neighboring nodes. The graph is structured as follows:
Parameters:
Process:
1. Initialize a visited list to track the nodes that have already been explored.
2. Define a recursive dfs function that takes a node and the current depth as
arguments.
3. If the current depth exceeds the depth limit, return None (stop searching
further).
4. If the node matches the goal, print the path to the goal.
5. Explore each unvisited neighbor of the current node by recursively calling dfs
for each neighbor.
6. If no path to the goal is found within the limit, backtrack and explore other
nodes.
# DLS Function
def dls(graph, start, goal, depth_limit):
visited = []
def dfs(node, depth):
if depth > depth_limit:
return None # Limit reached
visited.append(node)
if node == goal:
print(f"Goal found with DLS. Path: {visited}")
# return visited
for neighbor in graph.get(node, []):
if neighbor not in visited:
path = dfs(neighbor, depth + 1)
if path:
return dfs(start, 0)
Execution
To run the DLS search, we need to define the start node, goal node, and depth_limit.
Then, we can call the dls function with these parameters.
1. Starting Point: The algorithm starts at the start_node (A) and explores the graph
depth-first.
2. Depth Limitation: The search is restricted to a maximum depth of depth_limit. If
the depth exceeds this limit, the search will not continue.
3. Goal Check: If the goal_node (I) is found within the depth limit, the search stops,
and the path to the goal is printed.
4. Exploration Continuation: If the goal is not found, the algorithm will backtrack
and continue exploring other paths until the depth limit is reached.
How It Works:
tree = {
In this example, we will implement UCS to find the least-cost path from the start node to
the goal node in a weighted graph.
We represent the graph with weighted edges, where each edge has a specific cost. Each
node points to a dictionary of its neighbors and the corresponding costs.
Parameters:
graph: The weighted graph to be traversed.
start: The node where the search begins.
goal: The node that the algorithm is trying to find.
Process:
Initialize the frontier with the start node and cost 0.
Initialize a visited set to track nodes that have already been explored.
Track the cost to reach each node using a dictionary (cost_so_far).
Maintain a path reconstruction dictionary (came_from) to reconstruct the path once the
goal is reached.
The algorithm will:
● Sort the frontier by the accumulated cost to ensure the lowest cost node is
expanded first.
● Expand the node, visiting its neighbors and updating the cost to reach each
neighbor.
● If a neighbor offers a cheaper path, it is added to the frontier.
The algorithm continues until the goal is found or the frontier is empty.
while frontier:
# Sort frontier by cost, simulate priority queue
frontier.sort(key=lambda x: x[1])
# Explore neighbors
for neighbor, cost in graph[current_node].items():
new_cost = current_cost + cost
if neighbor not in cost_so_far or new_cost <
cost_so_far[neighbor]:
cost_so_far[neighbor] = new_cost
came_from[neighbor] = current_node
frontier.append((neighbor, new_cost)) # Add
to frontier
Execution
To run the UCS search, we need to define the start node, goal node Then, we can call the
UCS function with these parameters.
TASK #1
Convert the following searching algorithms into agent-based models:
● Uniform Cost Search (UCS): Implement as a Utility-Based Agent to find the goal
with the minimum cost path.
TASK # 2
Traveling Salesman Problem:
Given a set of cities and distances between every pair of cities, the problem is to find the
shortest possible route that visits every city exactly once and returns to the starting
point. Like any problem, which can be optimized, there must be a cost function. In the
context of TSP, total distance traveled must be reduced as much as possible.
Consider the below matrix representing the distances (Cost) between the cities. Find the
shortest possible route that visits every city exactly once and returns to the starting
point.
TASK # 3
TASK # 4
You are organizing a library and want to categorize books based on their genres and
subgenres. The library has a hierarchical structure where each genre can have multiple
subgenres, and each subgenre can further have its own subgenres. Your task is to design
a system that allows you to efficiently search for books within a specific genre or
subgenre, limiting the search to a certain depth (e.g., only within the main genre and its
immediate subgenres).
Library/
├── Fiction/
│ ├── Mystery/
│ │ ├── Crime/
│ │ └── Thriller/
│ └── Science Fiction/
│ ├── Space Opera/
│ └── Cyberpunk/
└── Non-Fiction/
├── History/
│ ├── Ancient History/
│ └── Modern History/
└── Science/
├── Biology/
└── Physics/
TASK # 5
You are managing a project with multiple tasks and dependencies. Each task has a
specific duration, and some tasks must be completed before others can start. Your goal
is to determine the earliest possible completion time for the entire project by identifying
the critical path—the sequence of tasks that determines the minimum project duration.
The project is represented as a directed acyclic graph (DAG), where nodes represent
tasks and edges represent dependencies between tasks.