You are on page 1of 12

Vietnam National University Ho Chi Minh City

University of Science
Faculty of Information Technology

LAB 1:
N-Puzzle with A* and UCS

Course Introduction to AI
Class 22CLC02
Teacher Nguyễn Ngọc Thảo
Hồ Thị Thanh Tuyến

Author 22127357 − Phạm Trần Yến Quyên

HCMC, 2024
Mục lục

1 Check list 2

2 Source Code 2
2.1 Library Usages: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2.2 Methods Description: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2.2.1 Class PuzzleState . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2.2.2 A* with Inversion Distance Heuristic . . . . . . . . . . . . . . . . . . . . . . . 3
2.2.3 UCS - Uniform Cost Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.3 How to use: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

3 Time and Space Measurement 8


3.1 3x3 Puzzle (N = 8) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.2 4x4 Puzzle (N = 15) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.3 6x6 Puzzle (N = 35) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

4 References 11

VNUHCM-US-FIT 1 22127357
1 Check list
1. A* with Inversion Distance Heuristic Implementation.

2. UCS Implementation.

3. GUI showing the solution path.

4. Time and Space Measurement display.

2 Source Code
2.1 Library Usages:
− streamlit: Used to make the GUI for the program, allowing users to interact with the appli-
cation through buttons, input fields, and text displays.

− numpy: Used for representing the puzzle states as arrays, performing operations on these ar-
rays. It records the start time before solving the puzzle and calculates the elapsed time after
solving it.

− time: Used to measure the total time a test lasted.

− tracemalloc: Used to measure the total memory a test used. It starts tracing memory allo-
cations before solving the puzzle and retrieves memory statistics afterward.

− queue: from this library, "Priority Queue"is imported and is used in the UCS and A* al-
gorithms for managing the frontier (states to be explored). It ensures that states with lower
costs are explored first, facilitating efficient pathfinding.

2.2 Methods Description:


2.2.1 Class PuzzleState
For this program, a single Class was created to represent the state of an N-Puzzle board for easier
tracking as well as creating an object for handling/manipulating states within the N-Puzzle search
space.
The Class itself includes the following:

− Attributes:

• state: Represents the current configuration of the puzzle. It’s a 1D array that holds the
values of each space in the puzzle.
• parent: Represents the previous state from which the current state was derived. This
attribute helps trace back the solution path.
• operator: Indicates the action (move) that was applied to transition from the parent
state to the current state.
• depth: Represents the depth of the current state in the search tree. It indicates the
number of moves taken to reach this state from the initial state.

VNUHCM-US-FIT 2 22127357
• cost: Represents the cost associated with reaching the current state. In the context of
UCS, it represents the actual cost (number of moves). In A*, it represents the cumula-
tive cost of reaching the state.
• heuristic: Represents the heuristic value associated with the state. In A*, this at-
tribute stores the Inversion Distance heuristic value.

− Methods:

• __init__: Constructor method used for initializing the PuzzleState object with the pro-
vided attributes.
• __lt__: Comparison method used for comparing two PuzzleState objects based on their
combined cost and heuristic values. This method is crucial for prioritizing states in the
priority queue used by UCS and A* algorithms.
• is_goal: Method to check if the current state matches the goal state.
• create_PuzzleState: Static method used for creating a PuzzleState object. It provides
an alternative way to create PuzzleState objects without explicitly calling the construc-
tor.

− How It Works:

(a) When a PuzzleState is generated or expanded during the search process, it’s encapsu-
lated within a PuzzleState object.
(b) The attributes of the PuzzleState object store crucial information about the state, in-
cluding its configuration, parent state, action taken, depth, cost, and heuristic value (as
already mentioned above Attributes).
(c) The comparison method (__lt__) allows PuzzleState objects to be compared based
on their combined cost and heuristic values. This comparison is essential for prioritizing
states in the frontier during UCS and A* search.
(d) The PuzzleState objects are utilized by search algorithms (UCS and A*) to explore
the search space, trace the solution path, and estimate the cost of reaching the goal
state.
(e) By encapsulating states within PuzzleState objects, the codebase becomes modular
and easier to manage, facilitating the implementation of various search algorithms and
heuristic functions.

The most important implementations in this program are the 2 algorithms:

2.2.2 A* with Inversion Distance Heuristic


For this methods, 2 others methods are called within in order to calculate as well as update the
value of the heuristic (in this case it is Inversion Distance Heuristic). The heuristic is calculated as
the sum of: Start node (g-value) and the estimated cost of reaching the goal node from the current
node (h-value). The algorithm selects nodes with the lowest combined cost (f-value) for expansion,
prioritizing paths that are likely to lead to the goal.

− As the base, both algorithms used the same coding frame:

VNUHCM-US-FIT 3 22127357
def ALGORITHMS ( puzzle , goal ) :
path = []
frontier = PQ ()
explored = set ()
start = PuzzleState ( puzzle , None , None , 0 , 0 , ID ( puzzle ) ) # THIS IS
WHERE THE 2 ALGORITHMS DIFFER
frontier . put ( start ) # Add the initial state to the frontier
while not frontier . empty () :
state = frontier . get () # Get the state with the lowest cost
if state . is_goal ( goal ) :
while state . parent is not None :
path . append ( state . state )
state = state . parent
return path [:: -1] # Return the path in reverse order
explored . add ( tuple ( state . state ) ) # Add the state to the explored set
for neighbor in expand_state ( state ) : # Expand the state ( for A * , the
ID is updated in the expand_state function according to the action taken
)
if tuple ( neighbor . state ) not in explored :
frontier . put ( neighbor ) # Add the neighbor to the frontier ,
if it is not already in the explored set
return None

− How it works:
• It takes two parameters: puzzle (the initial state of the puzzle) and goal (the goal
state of the puzzle).
• It initializes an empty list path to store the path from the initial state to the goal state.
• It initializes a PriorityQueue frontier to store the states to be explored, where states
with lower costs are given higher priority.
• The empty set explored() is used to keep track of explored states.
• It creates a start state object representing the initial state of the puzzle.
• It puts the start state into the frontier.
• It then enters a loop that continues until the frontier is empty:
+ It gets the state with the lowest cost from the frontier.
+ If the current state is the goal state, it reconstructs the path from the goal state to
the initial state and returns it.
Otherwise, it adds the current state to the explored set and expands it to generate
its neighboring states.
+ It puts the unexplored neighboring states into the frontier.
+ If the loop terminates without finding a solution, it returns None.
− With the major divergence in format at the creation of the object start = PuzzleState(puzzle,
None, None, 0, 0, ID(puzzle)) as A* has the heuristic value attribute (calculated at first
using the ID() method) instead of just being 0 for UCS.

(a) ID(state): This function calculates the inversion distance heuristic for a given state of
the puzzle.

VNUHCM-US-FIT 4 22127357
• It takes a single parameter state, which represents the current state of the puzzle.
• It initializes a variable idx to keep track of the index of the current tile.
• It iterates over the elements of the state array to calculate the number of inversions
for horizontal moves and vertical moves:
+ For horizontal moves, it counts the number of inversions by comparing each tile
with the tiles to its left.
+ For vertical moves, it calculates the number of inversions based on the relative
positions of the tiles in the puzzle grid.
• It then returns the sum of the number of vertical moves and horizontal moves, which
represents the Inversion Distance Heuristic.
def ID ( state ) :
idx = 0 # Index of the current tile
# Calculate the number of inversions
for i in range ( len ( state ) ) :
if state [ i ] != 0:
for j in range ( i ) :
if state [ i ] < state [ j ]:
idx += 1

inv = 0 # Number of inversions


# Calculate the number of inversions for the horizontal
moves
for i in range ( len ( state ) ) :
if state [ i ] != 0:
for j in range ( i ) :
if state [ i ] < state [ j ]:
inv += 1
vertical = inv // 3 + 1 # Number of vertical moves

idx = 0 # Reset the index


# Calculate the number of inversions for the vertical
moves
for i in range ( int ( np . sqrt ( len ( state ) ) ) ) : # For each
column
for j in range ( int ( np . sqrt ( len ( state ) ) ) ) : # For each
row
idx = j * int ( np . sqrt ( len ( state ) ) ) + i # Get the
index of the current tile by using the row
and column indices
inv += abs ( idx - ( j * int ( np . sqrt ( len ( state ) ) ) +
i ) ) # Calculate the number of inversions for
the vertical moves
horizontal = inv // 3 + 1 # Number of horizontal moves
return vertical + horizontal
(b) IDfromMove(state, action): The function calculates and updates the heuristic value
based on the action taken from a given state in the sliding puzzle. It adjusts the heuris-

VNUHCM-US-FIT 5 22127357
tic value according to the change in the number of inversions caused by the action, pro-
viding a more accurate estimate of the remaining cost to reach the goal state.
• It takes two parameters: state (the current state of the puzzle) and action (the ac-
tion to be taken: Up, Down, Left, or Right).
• It calculates the initial heuristic value using the ID() function.
• It determines the position of the blank tile after the action is taken and compares it
with the positions of other tiles affected by the action to adjust the heuristic value
accordingly:
+ A variable count is used to keep track of how many inversions are affected by
the action.
+ Updates the count variable based on the comparisons made during action han-
dling.
+ If count is negative, it means there are more inversions after the action, so it
adjusts the heuristic value accordingly.
+ If count is positive or zero, it means there are fewer or the same number of in-
versions after the action, so it adjusts the heuristic value accordingly.
def IDfromMove ( state , action ) :
heuristic = ID ( state )
size = int ( np . sqrt ( len ( state ) ) ) - 1 # -1 because the index starts at
0
count = 0
if action == " Up " :
idx = size + 1
num = state [ size ] # The element that is moved
if num > state [ idx ]:
count += 1
else :
count -= 1
idx += size
if num > state [ idx ]:
count += 1
else :
count -= 1
idx += size
if num > state [ idx ]:
count += 1
else :
count -= 1
elif action == " Down " :
idx = size + 1
num = state [ size ]
if num < state [ idx ]:
count += 1
else :
count -= 1
idx -= size
if num < state [ idx ]:
count += 1
else :
count -= 1
idx -= size
if num < state [ idx ]:

VNUHCM-US-FIT 6 22127357
count += 1
else :
count -= 1
. . . . . . . . . . . . . . . . . . . . . . # For full , please refers to the source code
if count < 0:
heuristic += abs ( count ) /3 + 1
else :
heuristic -= abs ( count ) / 3
return heuristic
# How the heuristic is updated :
# Upon moving a tile , the number of inversions that are affected by
the move is calculated
# Using the lookup table , the number of inversions that are affected
by the move is calculated
# If the number of inversions that are affected by the move is
negative , the heuristic is increased by the number of inversions that
are affected by the move divided by 3 and 1
# If the number of inversions that are affected by the move is
positive , the heuristic is decreased by the number of inversions that
are affected by the move divided by 3

− Other than the above, there are also numerous methods (Ex: the actions on the blank space,
applying the actions by checking if it’s possible, etc) with expand_state(state) being the
methods that separate how the cost of states are chosen for UCS and A*.

2.2.3 UCS - Uniform Cost Search


Similar to A*, UCS is simply a A* without heuristic value being calculated when comparing be-
tween the 2 states. Thus leading to UCS expands nodes in increasing order of path cost, guarantee-
ing an optimal solution when all edge costs are non-negative.
As for how this works, refers to the mentioned above coding frame.

2.3 How to use:


Inside of a IDE (Ex: Visual Code)
To open the streamlit web app, simply "cd"into the directory that has the source code and type in
the command: "streamlit run 22127357.py"From there you will be taken to a the web app and
can proceed to run tests. The provided functions are:

• Select the size of the puzzle: Ranging from 3x3->6x6 - N=15->N=35 using the incremental
box +/-.

• Algorithm selector: A drop down box inlcuding the 2 algorithms.

• Shuffle Puzzle: Shuffle the puzzle randomly using np.random.permutation() for test cases.
(NOTICE: there are no "unsolvable checker")

• Solve Puzzle: Solve the desired randomized puzzle. The resulting path is printed on screen
with a Step Counter (starting with the initial state as Step 0).

VNUHCM-US-FIT 7 22127357
3 Time and Space Measurement
Before we get into the full information board, let go over the test cases and thhe performance of
each algorithms

3.1 3x3 Puzzle (N = 8)


− Test 1:

Test 1

Steps:

• A*: 21 Steps
• UCS: 21 Steps

− Test 2:

Test 2

Steps:

• A*: 11 Steps
• UCS: 11 Steps

− Test 3:

Test 3

Steps:

VNUHCM-US-FIT 8 22127357
• A*: 21 Steps
• UCS: 21 Steps

− Results:

Results recorded

The total number of states for a 3x3 N-Puzzle board is:

9! = 362, 880

As can be seen with the results recorded, in three of the cases (or just the Average), A* over-
all performed better than UCS, this is due to the added benefit of Heuristic value.

3.2 4x4 Puzzle (N = 15)


Now the state Search Space has increase into the trillions:
16!/2 = 1.0461395e + 13 (roughly 10 trillion)
For 15-puzzle, hard initial states can get up to 80 levels deep solution and requires exploring 280 ≈
1024 states. The N=15 Puzzle posed a problem going forward which is too large to store in RAM
(>= 100 TB).
From this point onwards, the UCS algorithms is insufficient in providing a solution as the
A* however, in most cases can still output a solution:

− Test 1:

Test 1

VNUHCM-US-FIT 9 22127357
Steps:

• A*: 14 Steps
• UCS: INTRACTABLE

− Test 2:

Test 2

Steps:

• A*: 19 Steps
• UCS: INTRACTABLE

− Test 3:

Test3

Steps:

• A*: 26 Steps
• UCS: INTRACTABLE

− Results

VNUHCM-US-FIT 10 22127357
Results recorded

UCS having no information about the goal state or how close the current state is to the goal. It
will have to explore numerous states, effectively performing a brute-force search. Given the large
number of possible states and the lack of heuristic guidance, UCS might take an impractical amount
of time to find the solution.

3.3 6x6 Puzzle (N = 35)


Similar to 4x4, the state space to find the solution for a 6x6 N-Puzzle board is simply too big for
Thus, the results recorded for this test in Intractable

4 References
− N-Puzzle:

+ Bart Selman - CS 4700: Problem Solving by Search

− UCS:

+ Hossam-Elbahrawy - Search.py

− A*:

+ Michael Kim - Blog: Solving the 15 Puzzle


+ Inversion Distance
+ RedDocMD - FifteenPuzzle

VNUHCM-US-FIT 11 22127357

You might also like