├── README.md ├── Main.py ├── Search_Algorithms.py └── State.py /README.md: -------------------------------------------------------------------------------- 1 | # N-Puzzle-solver-with-Search-Algorithms 2 | 3 | N-puzzle solver with Search Algorithms including: BFS, DFS (with limited depth), Greedy and A* 4 | 5 | 6 | There are 2 heuristics for Greedy and A* algorithms. The first one evaluates states with manhattan distance and the second one evaluates states with number of misplaced tiles. 7 | 8 | ## Input 9 | 10 | First n should be entered and then the initial state of puzzle would be a list of numbers from 0 to (n*n)-1. An example of 8-puzzle with n = 3 is: 11 | 12 | ``` 13 | initial_state = [1, 8, 2, 0, 4, 3, 7, 6, 5] 14 | ``` 15 | 16 | Note that if you want to work with n > 3, goal should be changed in State class. 17 | 18 | ## Output 19 | 20 | Output of each search algorithm will be the puzzle solution, number of explored nodes and spent time for search. 21 | 22 | ``` 23 | BFS Solution is ['Right', 'Up', 'Right', 'Down', 'Down', 'Left', 'Up', 'Right', 'Down'] 24 | Number of explored nodes is 224 25 | BFS Time: 0.005887031555175781 26 | ``` 27 | -------------------------------------------------------------------------------- /Main.py: -------------------------------------------------------------------------------- 1 | from Search_Algorithms import BFS, DFS, Greedy, AStar_search 2 | 3 | #initial state 4 | n = int(input("Enter n\n")) 5 | print("Enter your" ,n,"*",n, "puzzle") 6 | root = [] 7 | for i in range(0,n*n): 8 | p = int(input()) 9 | root.append(p) 10 | 11 | print("The given state is:", root) 12 | 13 | 14 | #count the number of inversions 15 | def inv_num(puzzle): 16 | inv = 0 17 | for i in range(len(puzzle)-1): 18 | for j in range(i+1 , len(puzzle)): 19 | if (( puzzle[i] > puzzle[j]) and puzzle[i] and puzzle[j]): 20 | inv += 1 21 | return inv 22 | 23 | def solvable(puzzle): #check if initial state puzzle is solvable: number of inversions should be even. 24 | inv_counter = inv_num(puzzle) 25 | if (inv_counter %2 ==0): 26 | return True 27 | return False 28 | 29 | 30 | #1,8,2,0,4,3,7,6,5 is solvable 31 | #2,1,3,4,5,6,7,8,0 is not solvable 32 | 33 | from time import time 34 | 35 | if solvable(root): 36 | print("Solvable, please wait. \n") 37 | 38 | time1 = time() 39 | BFS_solution = BFS(root, n) 40 | BFS_time = time() - time1 41 | print('BFS Solution is ', BFS_solution[0]) 42 | print('Number of explored nodes is ', BFS_solution[1]) 43 | print('BFS Time:', BFS_time , "\n") 44 | 45 | time2 = time() 46 | DFS_solution = DFS(root, n) 47 | DFS_time = time() - time2 48 | print('DFS Solution is ', DFS_solution[0]) 49 | print('Number of explored nodes is ', DFS_solution[1]) 50 | print('DFS Time:', DFS_time, "\n") 51 | 52 | time3 = time() 53 | Greedy_solution = Greedy(root, n) 54 | Greedy_time = time() - time3 55 | print('Greedy Solution is ', Greedy_solution[0]) 56 | print('Number of explored nodes is ', Greedy_solution[1]) 57 | print('Greedy Time:', Greedy_time , "\n") 58 | 59 | time4 = time() 60 | AStar_solution = AStar_search(root, n) 61 | AStar_time = time() - time4 62 | print('A* Solution is ', AStar_solution[0]) 63 | print('Number of explored nodes is ', AStar_solution[1]) 64 | print('A* Time:', AStar_time) 65 | 66 | 67 | else: 68 | print("Not solvable") 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /Search_Algorithms.py: -------------------------------------------------------------------------------- 1 | from State import State 2 | from queue import PriorityQueue 3 | from queue import Queue 4 | from queue import LifoQueue 5 | 6 | 7 | #Breadth-first Search 8 | def BFS(given_state , n): 9 | root = State(given_state, None, None, 0, 0) 10 | if root.test(): 11 | return root.solution() 12 | frontier = Queue() 13 | frontier.put(root) 14 | explored = [] 15 | 16 | while not(frontier.empty()): 17 | current_node = frontier.get() 18 | explored.append(current_node.state) 19 | 20 | children = current_node.expand(n) 21 | for child in children: 22 | if child.state not in explored: 23 | if child.test(): 24 | return child.solution(), len(explored) 25 | frontier.put(child) 26 | return 27 | 28 | #Depth-first Search with limited depth 29 | def DFS(given_state , n): 30 | root = State(given_state, None, None, 0, 0) 31 | if root.test(): 32 | return root.solution() 33 | frontier = LifoQueue() 34 | frontier.put(root) 35 | explored = [] 36 | 37 | while not(frontier.empty()): 38 | current_node = frontier.get() 39 | max_depth = current_node.depth #current depth 40 | explored.append(current_node.state) 41 | 42 | if max_depth == 30: 43 | continue #go to the next branch 44 | 45 | children = current_node.expand(n) 46 | for child in children: 47 | if child.state not in explored: 48 | if child.test(): 49 | return child.solution(), len(explored) 50 | frontier.put(child) 51 | return (("Couldn't find solution in the limited depth."), len(explored)) 52 | 53 | 54 | 55 | def Greedy(given_state , n): 56 | frontier = PriorityQueue() 57 | explored = [] 58 | counter = 0 59 | root = State(given_state, None, None, 0, 0) 60 | #root.evaluation() 61 | evaluation = root.Manhattan_Distance(n) #we can use Misplaced_Tiles() instead. 62 | frontier.put((evaluation[0], counter, root)) #based on greedy evaluation 63 | 64 | while not frontier.empty(): 65 | current_node = frontier.get() 66 | current_node = current_node[2] 67 | explored.append(current_node.state) 68 | 69 | if current_node.test(): 70 | return current_node.solution(), len(explored) 71 | 72 | children = current_node.expand(n) 73 | for child in children: 74 | if child.state not in explored: 75 | counter += 1 76 | evaluation = child.Manhattan_Distance(n) #we can use Misplaced_Tiles() instead. 77 | frontier.put((evaluation[0], counter, child)) #based on greedy evaluation 78 | return 79 | 80 | 81 | def AStar_search(given_state , n): 82 | frontier = PriorityQueue() 83 | explored = [] 84 | counter = 0 85 | root = State(given_state, None, None, 0, 0) 86 | evaluation = root.Manhattan_Distance(n) #we can use Misplaced_Tiles() instead. 87 | frontier.put((evaluation[1], counter, root)) #based on A* evaluation 88 | 89 | while not frontier.empty(): 90 | current_node = frontier.get() 91 | current_node = current_node[2] 92 | explored.append(current_node.state) 93 | 94 | if current_node.test(): 95 | return current_node.solution(), len(explored) 96 | 97 | children = current_node.expand(n) 98 | for child in children: 99 | if child.state not in explored: 100 | counter += 1 101 | evaluation = child.Manhattan_Distance(n) #we can use Misplaced_Tiles() instead. 102 | frontier.put((evaluation[1], counter, child)) #based on A* evaluation 103 | return 104 | -------------------------------------------------------------------------------- /State.py: -------------------------------------------------------------------------------- 1 | class State: 2 | goal = [1, 2, 3, 4, 5, 6, 7, 8, 0] 3 | #this should be changed manually based on n 4 | #e.g. it should be [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0] if n is 4. 5 | 6 | greedy_evaluation = None 7 | AStar_evaluation = None 8 | heuristic = None 9 | def __init__(self, state, parent, direction, depth, cost): 10 | self.state = state 11 | self.parent = parent 12 | self.direction = direction 13 | self.depth = depth 14 | 15 | if parent: 16 | self.cost = parent.cost + cost 17 | 18 | else: 19 | self.cost = cost 20 | 21 | 22 | 23 | def test(self): #check if the given state is goal 24 | if self.state == self.goal: 25 | return True 26 | return False 27 | 28 | #heuristic function based on Manhattan distance 29 | def Manhattan_Distance(self ,n): 30 | self.heuristic = 0 31 | for i in range(1 , n*n): 32 | distance = abs(self.state.index(i) - self.goal.index(i)) 33 | 34 | #manhattan distance between the current state and goal state 35 | self.heuristic = self.heuristic + distance/n + distance%n 36 | 37 | self.greedy_evaluation = self.heuristic 38 | self.AStar_evaluation = self.heuristic + self.cost 39 | 40 | return( self.greedy_evaluation, self.AStar_evaluation) 41 | 42 | 43 | #heuristic function based on number of misplaced tiles 44 | def Misplaced_Tiles(self,n): 45 | counter = 0; 46 | self.heuristic = 0 47 | for i in range(n*n): 48 | for j in range(n*n): 49 | if (self.state[i] != self.goal[j]): 50 | counter += 1 51 | self.heuristic = self.heuristic + counter 52 | 53 | self.greedy_evaluation = self.heuristic 54 | self.AStar_evaluation = self.heuristic + self.cost 55 | 56 | return( self.greedy_evaluation, self.AStar_evaluation) 57 | 58 | 59 | 60 | @staticmethod 61 | 62 | #this would remove illegal moves for a given state 63 | def available_moves(x,n): 64 | moves = ['Left', 'Right', 'Up', 'Down'] 65 | if x % n == 0: 66 | moves.remove('Left') 67 | if x % n == n-1: 68 | moves.remove('Right') 69 | if x - n < 0: 70 | moves.remove('Up') 71 | if x + n > n*n - 1: 72 | moves.remove('Down') 73 | 74 | return moves 75 | 76 | #produces children of a given state 77 | def expand(self , n): 78 | x = self.state.index(0) 79 | moves = self.available_moves(x,n) 80 | 81 | children = [] 82 | for direction in moves: 83 | temp = self.state.copy() 84 | if direction == 'Left': 85 | temp[x], temp[x - 1] = temp[x - 1], temp[x] 86 | elif direction == 'Right': 87 | temp[x], temp[x + 1] = temp[x + 1], temp[x] 88 | elif direction == 'Up': 89 | temp[x], temp[x - n] = temp[x - n], temp[x] 90 | elif direction == 'Down': 91 | temp[x], temp[x + n] = temp[x + n], temp[x] 92 | 93 | 94 | children.append(State(temp, self, direction, self.depth + 1, 1)) #depth should be changed as children are produced 95 | return children 96 | 97 | 98 | #gets the given state and returns it's direction + it's parent's direction till there is no parent 99 | def solution(self): 100 | solution = [] 101 | solution.append(self.direction) 102 | path = self 103 | while path.parent != None: 104 | path = path.parent 105 | solution.append(path.direction) 106 | solution = solution[:-1] 107 | solution.reverse() 108 | return solution 109 | --------------------------------------------------------------------------------