Unit - 2 Problems, Problem Spaces, and Search
Unit - 2 Problems, Problem Spaces, and Search
Conducting a state space search with a graph involves exploring a set of nodes and edges with the goal
of finding a path from an initial node to a goal node. Below is a step-by-step procedure for conducting a
state space search using a graph:
• Step 1: Define the Problem: Clearly state the problem, specify the initial node, the goal node,
and the set of possible transitions (edges) between nodes.
• Step 2: Create the Graph: Represent the problem as a directed or undirected graph, where
nodes represent states and edges represent possible transitions or actions.
• Step 3: Select a Search Algorithm: Choose an appropriate search algorithm based on the
characteristics of the problem and the graph. Common choices include Breadth-First Search (BFS)
and Depth-First Search (DFS).
• Step 4: Initialize Data Structures: Create a data structure to keep track of visited nodes and
nodes to be explored. Add the initial node to the list of nodes to be explored.
• Step 5: Iterate through the Graph: While nodes remain to explore, extract the next node.
Verify if it’s the goal; if true, a solution is attained. If not, generate successor nodes by traversing
edges from the current node.
• Step 6: Check for Goal Node: Upon generating successor nodes, check if any of them are the
goal node. If so, the search is complete, and a solution has been found.
• Step 7: Manage Visited Nodes: Keep track of visited nodes to avoid revisiting them and
potentially entering loops.
• Step 8: Update Data Structures: Update the data structures with newly generated nodes. Add
them to the list of nodes to be explored.
• Step 9: Repeat Steps 5-8 until Goal Node is Found: Continue iterating through the graph,
generating successor nodes, and checking for the goal node until a solution is found.
• Step 10: Backtrack (If Necessary): In some cases, if a dead-end is reached, backtrack to a
previous node and explore alternative paths.
• Step 11: Retrieve Solution Path: Once the goal node is reached, trace back the path from the
goal node to the initial node to retrieve the sequence of actions or transitions that led to the
solution.
• Step 12: Evaluate Solution: Evaluate the solution based on relevant metrics, such as path cost,
optimality, and completeness.
• Step 13: Implement Post-Processing (if needed): Depending on the problem domain,
additional steps may be required to implement the solution.
Example of State Space Search in AI
The 8-puzzle is a popular sliding puzzle that involves a 3×3 grid where eight numbered tiles and one
blank space are arranged. The objective is to rearrange the tiles from an initial configuration to a
target configuration using a sequence of valid moves. Here’s a step-by-step explanation of the 8-
puzzle:
Step 1: Initial State
The 8-puzzle starts with an initial configuration where eight numbered tiles (usually from 1 to 8) and
one empty space are arranged randomly within a 3×3 grid. For example, an initial state could look like
this:
AI Production Systems exhibit several key features that make them versatile and powerful tools for
automated decision-making and problem-solving:
• Simplicity: Production Systems offer a straightforward way to encode and execute rules, making
them accessible for developers and domain experts.
• Modularity: These systems are composed of modular components, allowing for the addition,
removal, or modification of rules without disrupting the entire system. This modularity enhances
flexibility and ease of maintenance.
• Modifiability: AI Production Systems are highly adaptable. Rules can be updated or replaced
without extensive reengineering, ensuring the system remains up-to-date and aligned with
evolving requirements.
• Knowledge-intensive: They excel in handling knowledge-rich tasks, relying on a comprehensive
global database.
• Adaptability: AI Production Systems can dynamically adapt to new data and scenarios. This
adaptability allows them to continuously improve.
Classification of Production Systems in AI
AI production systems can be classified into four common classifications:
• Monotonic Production System: In a monotonic production system, the laws and truths remain
constant while being carried out. A rule remains constant throughout the procedure once a fact
is deduced. This stability ensures predictability but may limit adaptability in dynamic
environments.
• Partially Commutative Production System: In this type of system, rules can be applied
flexibly, allowing for some degree of adaptability while maintaining certain constraints. Partial
commutativity strikes a balance between stability and flexibility.
• Non-monotonic Production System: Non-monotonic production systems are more dynamic
and adaptive. Rules can be added, modified, or retracted during execution. They are excellent for
situations where the knowledge base needs to change in response to shifting circumstances
because of their flexibility.
• Commutative System: Commutative systems have rules that can be applied in any sequence
without changing the result. In circumstances where the sequence of rule application is not
essential, this high degree of flexibility may be beneficial.
Problem Characteristics
Artificial Intelligence (AI) problems refer to a wide range of tasks that involve the development of
algorithms or systems that can perform tasks that typically require human-level intelligence, such as
decision-making, language processing, image recognition, and problem-solving.
AI problems are characterized by several key features, including:
1. Complexity: AI problems are often highly complex, requiring the processing of large amounts of
data and the ability to handle uncertainty and ambiguity. The algorithms and systems developed
to solve these problems must be able to manage complexity and make decisions based on
incomplete or uncertain information.
2. Non-linearity: Many AI problems exhibit non-linear relationships, meaning that small changes
in the input can lead to significant changes in the output. This makes it difficult to develop
algorithms that can accurately predict outcomes or make decisions based on input data.
3. Context dependence: AI problems often require the ability to understand context and make
decisions based on the specific situation or environment. For example, a language processing
system must be able to understand the meaning of words in the context of a sentence or
paragraph.
4. Creativity: Some AI problems require the ability to generate novel solutions or ideas, often using
techniques such as generative adversarial networks (GANs) or evolutionary algorithms. These
techniques allow AI systems to learn from existing data and generate new and innovative
solutions to complex problems.
5. Learning and adaptation: AI problems often require the ability to learn and adapt over time
based on new data or feedback. This is achieved through techniques such as machine learning,
deep learning, and reinforcement learning, which enable AI systems to improve their performance
over time.
6. Multi-disciplinary: AI problems often require expertise from multiple fields, including computer
science, mathematics, statistics, and cognitive psychology. Successful AI systems must be
designed and developed by teams with a wide range of skills and knowledge.
7. Ethical considerations: AI problems raise ethical and social concerns related to privacy,
security, bias, and fairness. The development of AI systems requires careful consideration of
these issues to ensure that the technology is used ethically and responsibly.
Disadvantage of BFS
While Breadth-First Search (BFS) has several advantages, it also has some limitations and disadvantages
in certain contexts, especially in Artificial Intelligence (AI) applications. Here are some drawbacks of BFS:
1. Memory Consumption: BFS can consume a significant amount of memory, especially when
dealing with large state spaces or graphs. This is because it needs to store all the nodes at the
current level in the queue before moving to the next level. In situations with limited memory
resources, this can be a significant drawback.
2. Inefficiency for Large Graphs or State Spaces: In scenarios where the search space is
extensive, BFS may become inefficient due to its need to explore all nodes at each level before
moving to the next level. This can result in a large number of nodes being generated and stored.
3. Not Suitable for Graphs with Varying Edge Costs: BFS assumes that all edges have the same
cost. In cases where the edges have varying costs (weighted graphs), BFS may not provide the
optimal solution. In such scenarios, algorithms like Dijkstra's or A* may be more appropriate.
4. Inability to Handle Infinite Branching Factor: If the branching factor of the search tree is
very high or infinite, BFS may not be practical. The number of nodes generated and the memory
requirements can become unmanageable.
5. Limited Use in Finding Specific Paths: While BFS can find the shortest path in unweighted
graphs, it may not be suitable for scenarios where the goal is to find a specific type of path (e.g.,
paths with certain properties or constraints). Other algorithms like DFS or A* with appropriate
heuristics might be more suitable for such cases.
6. No Guidance Toward Goal Direction: BFS does not use any information about the potential
closeness of the goal during its search. In contrast, algorithms like A* use heuristics to guide the
search toward the goal more efficiently.
7. Doesn't Exploit Problem-Specific Knowledge: BFS is a generic algorithm and does not exploit
any problem-specific knowledge that might be available in certain AI applications. This can result
in a less efficient exploration of the search space.
8. Not Suitable for Online or Incremental Search: In situations where the search space is
dynamic and changes over time, BFS might not be well-suite
BFS Algorithm
The steps involved in the BFS algorithm to explore a graph are given as follows
Step 1: SET STATUS = 1 (ready state) for each node in G
Step 2: Enqueue the starting node A and set its STATUS = 2 (waiting state)
Step 3: Repeat Steps 4 and 5 until QUEUE is empty
Step 4: Dequeue a node N. Process it and set its STATUS = 3 (processed state).
Step 5: Enqueue all the neighbours of N that are in the ready state (whose STATUS = 1) and set
their STATUS = 2
(waiting state)
[END OF LOOP]
Step 6: EXIT
Example of BFS algorithm
Now, let's understand the working of BFS algorithm by using an example. In the example given below,
there is a directed graph having 7 vertices.
In the above graph, minimum path 'P' can be found by using the BFS that will start from Node A and end
at Node E. The algorithm uses two queues, namely QUEUE1 and QUEUE2. QUEUE1 holds all the nodes
that are to be processed, while QUEUE2 holds all the nodes that are processed and deleted from QUEUE1.
Now, let's start examining the graph starting from Node A.
Step 1 - First, add A to queue1 and NULL to queue2.
1. QUEUE1 = {A}
2. QUEUE2 = {NULL}
Step 2 - Now, delete node A from queue1 and add it into queue2. Insert all neighbors of node A to
queue1.
1. QUEUE1 = {B, D}
2. QUEUE2 = {A}
Step 3 - Now, delete node B from queue1 and add it into queue2. Insert all neighbors of node B to
queue1.
1. QUEUE1 = {D, C, F}
2. QUEUE2 = {A, B}
Step 4 - Now, delete node D from queue1 and add it into queue2. Insert all neighbors of node D to
queue1. The only neighbor of Node D is F since it is already inserted, so it will not be inserted again.
1. QUEUE1 = {C, F}
2. QUEUE2 = {A, B, D}
Step 5 - Delete node C from queue1 and add it into queue2. Insert all neighbors of node C to
queue1.
1. QUEUE1 = {F, E, G}
2. QUEUE2 = {A, B, D, C}
Step 6 - Delete node E from queue1. Since all of its neighbors have already been added, so we will
not insert them again. Now, all the nodes are visited, and the target node E is encountered into
queue2.
1. QUEUE1 = {G}
2. QUEUE2 = {A, B, D, C, F, E}
The space complexity of BFS can be expressed as O(V), where V is the number of vertices.
In this code, we are using the adjacency list to represent our graph. Implementing the Breadth-First
Search algorithm in Java makes it much easier to deal with the adjacency list since we only have to
travel through the list of nodes attached to each node once the node is dequeued from the head
(or start) of the queue.
In this example, the graph that we are using to demonstrate the code is given as follows -
# Create a graph given in the above diagram.
graph = {
'A': ['B', 'C', 'D'],
'B': ['A'],
'C': ['A', 'D'],
'D': ['A', 'C', 'E'],
'E': ['D'],
}
while queue:
# Remove the front vertex or the vertex at the 0th index from the queue and print that vertex.
v = queue.pop(0)
print(v, end=" ")
# Get all adjacent nodes of the removed node v from the graph hash table.
# If an adjacent node has not been visited yet,
# then mark it as visited and add it to the queue.
for neigh in graph[v]:
if neigh not in visited:
visited.append(neigh)
queue.append(neigh)
# Driver Code
if __name__ == "__main__":
bfs('A')
DFS (Depth First Search) algorithm
It is a recursive algorithm to search all the vertices of a tree data structure or a graph. The depth-first
search (DFS) algorithm starts with the initial node of graph G and goes deeper until we find the goal
node or the node with no children.
Because of the recursive nature, stack data structure can be used to implement the DFS algorithm. The
process of implementing the DFS is similar to the BFS algorithm.
The step-by-step process to implement the DFS traversal is given as follows -
1. First, create a stack with the total number of vertices in the graph.
2. Now, choose any vertex as the starting point of traversal, and push that vertex into the stack.
3. After that, push a non-visited vertex (adjacent to the vertex on the top of the stack) to the top
of the stack.
4. Now, repeat steps 3 and 4 until no vertices are left to visit from the vertex on the stack's top.
5. If no vertex is left, go back and pop a vertex from the stack.
6. Repeat steps 2, 3, and 4 until the stack is empty.
Applications of DFS algorithm
The applications of using the DFS algorithm are given as follows -
Backward Skip 10sPlay Video Forward Skip 10s
Algorithm
Step 2: Push the starting node A on the stack and set its STATUS = 2 (waiting state)
Step 4: Pop the top node N. Process it and set its STATUS = 3 (processed state)
Step 5: Push on the stack all the neighbors of N that are in the ready state (whose STATUS = 1) and
set their STATUS = 2 (waiting state)
[END OF LOOP]
Step 6: EXIT
Pseudocode
Now, let's understand the working of the DFS algorithm by using an example. In the example given
below, there is a directed graph having 7 vertices.
1. STACK: H
Step 2 - POP the top element from the stack, i.e., H, and print it. Now, PUSH all the neighbors of H
onto the stack that are in ready state.
1. Print: H]STACK: A
Step 3 - POP the top element from the stack, i.e., A, and print it. Now, PUSH all the neighbors of A
onto the stack that are in ready state.
1. Print: A
2. STACK: B, D
Step 4 - POP the top element from the stack, i.e., D, and print it. Now, PUSH all the neighbors of D onto
the stack that are in ready state.
1. Print: D
2. STACK: B, F
Step 5 - POP the top element from the stack, i.e., F, and print it. Now, PUSH all the neighbors of F onto
the stack that are in ready state.
1. Print: F
2. STACK: B
Step 6 - POP the top element from the stack, i.e., B, and print it. Now, PUSH all the neighbors of B onto
the stack that are in ready state.
1. Print: B
2. STACK: C
Step 7 - POP the top element from the stack, i.e., C, and print it. Now, PUSH all the neighbors of C onto
the stack that are in ready state.
1. Print: C
2. STACK: E, G
Step 8 - POP the top element from the stack, i.e., G and PUSH all the neighbors of G onto the stack that
are in ready state.
1. Print: G
2. STACK: E
Step 9 - POP the top element from the stack, i.e., E and PUSH all the neighbors of E onto the stack that
are in ready state.
1. Print: E
2. STACK:
Program
# DFS algorithm in Python
# DFS algorithm
def dfs(graph, start, visited=None):
if visited is None:
visited = set()
visited.add(start)
print(start)
for next in graph[start] - visited:
dfs(graph, next, visited)
return visited
graph = {'0': set(['1', '2']),
'1': set(['0', '3', '4']),
'2': set(['0']),
'3': set(['1']),
'4': set(['2', '3'])
dfs(graph, '0')
Difference Between BFS and DFS
S.no BFS DFS
BFS is an abbreviation of Breadth First DFS is an abbreviation of Depth First
Abbreviation for
Search. Search.
BFS (Breadth First Search) finds the
DFS (Depth First Search) makes use of
Data Organization shortest path using the Queue data
the Stack data structure.
structure.
Because BFS reaches a vertex with the
fewest edges from the source vertex, it In DFS, we may need to traverse more
Technique may be used to identify a single origin edges to go from a source vertex to a
shortest path inside an unweighted destination vertex.
graph.
The difference in DFS constructs the tree subtree by
BFS constructs a tree level per level.
Concepts subtree.
It is based on the FIFO principle (First in, It operates on the LIFO principle (Last
Utilised strategy
First Out). in First Out).
BFS is more suited for finding vertices If there are alternatives away from the
Appropriate for
that are near the provided source. source, DFS is more appropriate.
DFS is more suited to gaming or
Treestheirwinning BFS prioritises all
puzzle challenges. We make a choice
Suitable for neighbours and is hence unsuitable for
and then investigate all possible
Making a Decision decision-making trees in games or
outcomes. If this decision results in a
puzzles.
win-win situation, we cease.
The running time of BFS becomes O(V E) DFS has a time complexity of O(V E)
when using an Adjacency List and O(V2) when using an Adjacency List and
Time Complicacy when using an Adjacency Matrix, where V O(V2) when using an Adjacency
represents vertices and E represents Matrix, where V refers to vertices & E
edges. stands for edges.
The seen sites are added to a stack
Traversed Node Nodes that have been traversed multiple
whenever no more sites are visited
Removal times are removed from the queue.
and subsequently deleted.
BFS is utilised in various applications, DFS is utilised in various applications,
Applications including bipartite graphs, shortest including acyclic graphs & topological
routes, etc. order.
DFS requires less space since it only
The complexity of In BFS, case complexity is more
has to keep a single route from the
space important than time complexity.
root to a leaf node at a time.
When should you BFS works better whenever the target is DFS is advantageous whenever the
utilise it? near the source. target is remote from the source.
Speed When compared to DFS, BFS is slower. When compared to BFS, DFS is faster
Advantages of DFS:
1. Simplicity: DFS is a straightforward algorithm and is easy to implement.
2. Memory Efficiency: DFS can be more memory-efficient compared to Breadth-First Search (BFS)
as it requires less memory to store the visited nodes.
3. Space Complexity: In the worst case, the space complexity of DFS is O(h), where h is the
maximum depth of your tree. This can be advantageous when dealing with deep trees or graphs.
4. Versatility: DFS can be adapted for various applications, such as topological sorting, detecting
cycles in a graph, and solving puzzles like mazes.
5. Solution Optimality: In certain cases, DFS can find the solution with less memory consumption
compared to BFS.
Disadvantages of DFS:
1. Completeness: DFS may not find the shortest path in a weighted graph or the optimal solution
in certain cases. It is not guaranteed to find the most optimal solution because it may explore
paths that are longer before discovering a shorter one.
2. Non-Optimal for Shortest Paths: If the goal is to find the shortest path in a graph, DFS may
not be the best choice, as it can continue exploring a branch without considering the weight of
edges.
3. Vulnerability to Infinite Branching: In graphs with infinite branching (e.g., trees with infinite
nodes), DFS may not terminate unless there are mechanisms to handle this situation.
4. Not Ideal for Dense Graphs: DFS may not perform well on dense graphs because it tends to
go deep into a branch before backtracking. In dense graphs, this can lead to many unnecessary
explorations.
5. Pathological Cases: DFS might go deep into one branch before considering other options,
leading to inefficient solutions or longer paths.