240 likes | 363 Views
Search. Much of AI has traditionally revolved around search Search means “searching among all the possibilities for an answer” (or possibly the best answer) consider, as a programmer, you are facing a logical error in your program and want to fix it
E N D
Search • Much of AI has traditionally revolved around search • Search means “searching among all the possibilities for an answer” (or possibly the best answer) • consider, as a programmer, you are facing a logical error in your program and want to fix it • usually the program’s behavior gives you some indication of the problem • doesn’t print results? Stuck in an infinite loop • garbage output? Possible pointer dereferencing problem • wrong values output? Possible wrong computation • you draw conclusions based on your own experience with previous debugging sessions • A computer on the other hand has no experience to draw on, so it might have to consider all possibilities, evaluating each one using some kind of test • this is the basic idea of search
Consider the 8 puzzle One of those toys at carnivals, made of plastic move the numbers using the one opening so that the numbers eventually wind up in order 1-8 How do you solve it? at any given state (board configuration), you have 2-4 options based on where the space is slide tile up, down, left, right remember each state you have already visited so that you don’t get caught in an infinite loop 8 2 7 3 4 8 2 5 1 6 3 4 7 5 1 6 8 2 7 8 2 7 3 4 6 3 4 5 1 5 1 6 Example: 8 Puzzle This tree represents the “search space” of one move from the starting point how large is this tree?
8 Puzzle Search Space • We consider all possible “board” configurations of the 8 puzzle to be the search space • From any one board, you can manipulate the board into a few new positions • Each board configuration is called a state • We start in a starting state and our goal is to reach a goal state • All of the states can be illustrated in a tree or graph form Notice the cycle, we want to avoid this
8 Puzzle Algorithm • Let’s store the 8 puzzle as a list storing the number of each tile in the given position, for instance our starting point from the last slide would be (8 2 7 3 4 0 5 1 6) • 0 denotes the blank • Recursive function given the board: • Compare board to goal, are we done yet? If so, return that a solution has been found • Otherwise, add this board configuration to the list of tried states • Generate the possible moves from this position (this depends on where the blank is) • For each possible move, see if the board achieved by taking that move is already in our tried states list, if so, skip it • Otherwise, recursively call this function passing it the new board
Eight Puzzle Solution (defun eightpuzzle (board) (declare (special *tried* *goal*)) (when (equal board *goal*) (print board) (return-from eightpuzzle t)) (if (memberlis board *tried*) (return-from eightpuzzle nil)) (setf *tried* (append *tried* (list board))) (let* ((blank (position 0 board)) (moves (generate-boards blank board)) temp) (do ((i 0 (+ i 1))) ((or temp (= i (length moves)))) (setf temp (eightpuzzle (nth i moves)))) (if temp (print board)) temp)) (defun memberlis (a lis) (dolist (i lis) (if (equal i a) (return-from memberlis t))) nil)
Continued (defun generate-boards (blank board) (let (temp1 temp2) (setf temp1 (generate-moves blank)) (dolist (i temp1) (setf temp2 (append temp2 (list (swapem board i blank))))) temp2)) (defun generate-moves (blank) (cond ((= blank 0) '(1 3)) ((= blank 1) '(0 2 4)) ((= blank 2) '(1 5)) ((= blank 3) '(0 4 6)) ((= blank 4) '(1 3 5 7)) ((= blank 5) '(2 4 8)) ((= blank 6) '(3 7)) ((= blank 7) '(4 6 8)) ((= blank 8) '(5 7)))) (defun swapem (b i j) (let (temp (value (nth i b))) (dotimes (a 9) (if (= a i) (setf temp (append temp '(0))) (if (= a j) (setf temp (append temp (list value))) (setf temp (append temp (list (nth a b))))))) temp))
How Good is Our Algorithm? • We have two significant problems with our algorithm • First, because of recursion, we will push a lot of states on the stack before we might hit on a solution • how many? how deep is our tree? • for the 8 puzzle, it could be as deep as 8! does our stack space have enough room? • Second, our *tried* list grows with each new state visited • in the 8 puzzle, it can be as long as 8! near the end, which means that memberlis has a lot of work to do! • It is common to run out of stack space when trying to run such programs as the 8 puzzle
Improvement: Heuristic Search • Lets assume that we have another function, h, which can evaluate how likely a given move will lead us quickly to the solution • We slightly alter our previous algorithm as follows: • Let temp = F(S) • Remove all items in temp that are already in T • Sort temp in order using h • that is, (sort temp #’> :key #’h) • For each item i in temp, call try(i) • This allows us to try successor states in order of most likely to reach a solution • How much can this improve our search? • In the best case, the heuristic tells us the right move every time, so instead of an O(n!) complexity, it is reduced to perhaps as little as O(n) • In the worst case, it does nothing for us at all
Good Heuristics? • One of the things we study in AI is what makes up a good heuristic • A good heuristic is one that should reduce the complexity of the search by usually telling us the right move • Is there a good heuristic for the 8 puzzle? • Consider two: • Local heuristic: add up the number of tiles that are currently in the right position with respect to the goal state and subtract from it the number of tiles that are out of position, select the move that leads us to the state with the highest heuristic value • Global heuristic: add up the number of moves that each tile would have to make to get to its proper position in the goal state, select the move that leads us to the state with the lowest heuristic value • Some problems may not have heuristics (8 Queens? Knight’s Tour?)
Heuristic Version (defun compute-distance (a b) (cond ((= a b) 0) ((and (= a 0) (or (= b 1) (= b 3))) 1) ((and (= a 0) (or (= b 2) (= b 4) (= b 6))) 2) ((= a 0) 3) ((and (= a 1) (or (= b 0) (= b 2) (= b 5))) 1) ((and (= a 1) (or (= b 3) (= b 5) (= b 7))) 2) ((= a 1) 3) ;; … for a = 2, 3, 4, 5, 6, 7 ((and (= a 8) (or (= b 5) (= b 7))) 1) ((and (= a 8) (or (= b 2) (= b 4) (= b 6))) 2) (t 3))) (defun generate-boards (blank board) (let (temp1 temp2) (setf temp1 (generate-moves blank)) (dolist (i temp1) (setf temp2 (append temp2 (list (swapem board i blank))))) (sort temp2 #'< :key #'heuristic) temp2)) (defun heuristic (board) (declare (special *goal*)) (let ((sum 0) a b) (dotimes (i 9) (setf a (position i board)) (setf b (position i *goal*)) (setf sum (+ sum (compute-distance a b)))) sum))
General Purpose Search Algorithm • Let S = initial state, G be goal state, T be states already reached (initialized to nil) and F be a function that, given S, will generate successor states • try(S) • If S = G, then return success • Let temp = F(S) • Remove all items in temp that are already in T • For each item i in temp, call try(i) • Notice that this algorithm will not return T if it never reaches G, so it is implied to return nil • The only difficulty is in creating a proper function F • For the 8 puzzle, it was merely a list of new boards reachable from the given state and since there are no more than 8 possible locations for the blank, there are only 8 possible sets of moves, so it was easy to create F
Backtracking • What if we get stuck in a deadend when searching? • In doing a maze, if we reach a location where either we cannot continue, or we have tried all of the different branches, we cannot move forward, so instead we backtrack to the most recent intersection and try a different branch • To add backtracking to our algorithm, we have to know if our recursive function call ends with success or failure • if success, then return from this recursive call as well • if failure, backtrack and try a new branch from this point forward • Basic algorithm: • Backtrack(current) • If current is a goal node, return “success” • If current is nil (or a deadend state), return “failure” • For each child c of current, • value = Backtrack(C) • If value = “success” then return “success” else try the next possible choice from current
Example: 8 Queens • We want to place 8 queens on an 8x8 chessboard such that no queen can take any other queen • We will store our solution as a list where list element i will be the row of the queen in column i • We start with nil and extend it • Position a queen in column i at row 1 • If the queen cannot be captured, then extend else continue to try rows 2-8, if we exceed row 8, return failure • By returning failure, a previous recursive call to extend will move its current queen from the current row to the next, and continue Can’t place another queen at this point, so we must backtrack This queen is moved, we wind up continuing to backtrack until we have to move this queen
(defun queens (lis) (if (not (badboard lis)) (when (= (length lis) 8) (print lis)));; (return-from queens t))) (if (or (badboard lis) (> (length lis) 8)) (return-from queens nil) (progn (let (temp) (setf temp (queens (append lis '(1)))) (if (not temp) (setf temp (queens (append lis '(2))))) (if (not temp) (setf temp (queens (append lis '(3))))) (if (not temp) (setf temp (queens (append lis '(4))))) (if (not temp) (setf temp (queens (append lis '(5))))) (if (not temp) (setf temp (queens (append lis '(6))))) (if (not temp) (setf temp (queens (append lis '(7))))) (if (not temp) (setf temp (queens (append lis '(8))))) temp)))) 8 Queens Code (defun badboard (lis) (let ((board (butlast lis)) (a (car (last lis)))) (if (member a board) t (progn (let (x (return-value nil) (size (length board))) (dolist (temp board) (setf x (position temp board)) (if (= (/ (abs (- a temp)) (abs (- size x))) 1) (setf return-value t)))) return-value)))))
Knight’s Tour • A related problem is the knight’s tour • Can you move a knight on a chess board so that it eventually lands on every square of the chess board without repeating any board node twice? • Basic algorithm • Store the chessboard as an NxN array initialized to nil • move(x y num) • If <x, y> is a legal move (still on the board and this particular position has not been tried before) then set board[x][y] = num • Recursively try move (x’ y’ num+1) where x’ and y’ is the next available knight move from this position • this requires a loop that tries any of the 8 possible knight moves • If the recursive call does not permit the placement of the knight (no legal moves), then reset the position to nil and try the next position • Return nil if you were unable to place a knight at (x y num)
Example Early on, there are numerous choices, it doesn’t seem like any choice will lead us astray, but later on, we might find ourselves “boxed in” and we will have to backtrack to a previous decision point in order to continue
Knight’s Tour Code (defun knight (x y num) (declare (special *board* *size*)) (if (done) (return-from knight t)) (if (not (withinbounds x y)) (return-from knight nil)) (if (aref *board* x y) (return-from knight nil)) (setf (aref *board* x y) num) (let (tempx tempy legal) (do ((i 0 (+ i 1))) ((or legal (= i 8))) (setf tempx (+ x (nextx i))) (setf tempy (+ y (nexty i))) (setf legal (knight tempx tempy (+ num 1)))) (if (not legal) (setf (aref *board* x y) nil)) legal)) (defun nextx (i) (case i (0 2) (1 2) (2 1) (3 -1) (4 -2) (5 -2) (6 -1) (7 1))) (defun nexty (i) (case i (0 -1) (1 1) (2 2) (3 2) (4 1) (5 -1) (6 -2) (7 -2))) (defun withinbounds (x y) (declare (special *size*)) (and (< x *size*) (< y *size*) (>= x 0) (>= y 0)))
Continued (defun run (&optional size) (declare (special *board* *size*)) (if size (setf *size* size)) (setf *board* (make-array (list *size* *size*))) (if (knight 0 0 0) (print-board) (format t "~%Failed"))) (defun done () (declare (special *board* *size*)) (dotimes (i *size*) (dotimes (j *size*) (if (null (aref *board* i j)) (return-from done nil)))) t) (defun print-board () (declare (special *board* *size*)) (dotimes (i *size*) (format t "~%") (dotimes (j *size*) (format t "~4D" (aref *board* i j)))))
Water Jugs • You have 2 water jugs, one can hold exactly 4 gallons and one can hold exactly 3 gallons • Goal: Fill the 4 gallon jug with exactly 2 gallons of water • Assume: An infinite amount of water is available • Operations: • fill a jug to the top • dump the contents of a jug out (back into the well or onto the ground) • pour the contents of one jug into the other jug until the other jug is full or the current jug is empty • Search space will be represented as a list (x y) where x is the current contents in gallons of the 4 gallon jug and y is the current contents of the 3 gallon jug • Initial state: (0 0) • Goal state: (2 0) • We can search either recursively (depth-first) or using a queue (breadth-first) • Solution on the website
Water Jugs Code (defun waterjugs (x y) (declare (special *tried*)) (if (memberlis (list x y) *tried*) (return-from waterjugs nil) (setf *tried* (append *tried* (list (list x y))))) (when (and (= x 2) (= y 0)) (print (list x y)) (return-from waterjugs t)) (if (not (legal x y)) (return-from waterjugs nil)) (let (temp) (setf temp (waterjugs 4 y)) (if (not temp) (setf temp (waterjugs x 3))) (if (not temp) (setf temp (waterjugs 0 y))) (if (not temp) (setf temp (waterjugs x 0))) (if (not temp) (if (and (>= (+ x y) 4) (> y 0)) (setf temp (waterjugs 4 (- y (- 4 x))))))
Continued (if (not temp) (if (and (>= (+ x y) 3) (> x 0)) (setf temp (waterjugs (- x (- 3 y)) 3)))) (if (not temp) (if (and (<= (+ x y) 4) (> 0 y)) (setf temp (waterjugs (+ x y) 0)))) (if (not temp) (if (and (<= (+ x y) 3) (> x 0)) (setf temp (waterjugs 0 (+ x y))))) (if temp (print (list x y))) temp)) (defun legal (x y) (and (>= x 0) (<= x 4) (>= y 0) (<= y 3))) (defun memberlis (a lis) (dolist (i lis) (if (equal a i) (return-from memberlis t))) nil)
Consider Chess • In Chess, your goal state is a checkmate state • However, unlike the 8-puzzle, you can’t just search all states for a checkmate or use a heuristic • the opponent will make moves too • We need a different kind of search • Make this assumption: your opponent will always select a move that maximizes his board position’s heuristic worth • Therefore, as a player, you want to not only maximize your board position’s worth, but also minimize your opponent’s • this is called minimax • Compute the heuristic worth of all board configurations for x moves ahead • If x is even, take the minimum value from each subtree • If x is odd, take the maximum value from each subtree • Even or odd indicates whose turn that particular layer is (odd being your move and even being your opponent’s move)
Example Assume we have looked 1 level further and found the maximum heuristic values, we select only the best move Now, we know that our opponent will make an intelligent move such that he/she limits our options, so we assume that the opponent will look over the maximum heuristic values and select the move that is worst for us (so instead of offering us a move of a possible 10, our opponent will select a move that results in a move worth at most 3) Our best choice is to make the move on the right – if our opponent selects wisely, we can reach a state worth as much as 5
Other Search Strategies • Alpha-beta pruning • If we were to look ahead m turns, and at each turn, there are n possible moves to make, then we have to look ahead m^n • Consider chess, you want to look ahead 5 turns, and there are 20 possible moves per turn, you have to consider around 5^20 board configurations! • Alpha-beta pruning uses minimum and maximum threshold values to determine if a move should continue to be searched because it has already exceeded the maximum that we want the opponent to achieve or the minimum that we would desire to achieve • AND Search • In some cases, we are not searching for a single move, but a collection of moves • We may have a tree in which some nodes are OR nodes, you can pursue any one of the subtrees, but if we have AND nodes, then we must pursue all subtrees • This is more common in design/planning problems than in game playing