580 likes | 748 Views
Chapter 5. Recursion as a problem solving technique. Recalling recursion. This chapter considers organized ways to make successive guesses at a solution If a particular guess is a dead-end solution, you backtrack to the beginning and then make another guess
E N D
Chapter 5 Recursion as a problem solving technique
Recalling recursion • This chapter considers organized ways to make successive guesses at a solution • If a particular guess is a dead-end solution, you backtrack to the beginning and then make another guess • Recursion and backtracking can be used to solve…
The 8 Queens problem • A chessboard contains 64 squares • There are 8 horizontal rows • There are 8 vertical columns • The most powerful piece in the game of chess is the queen because she can move with almost impunity • She can go horizontal or vertical as many spaces as she likes • She can go diagonally in any direction as many spaces as she likes
More on the 8 Queens problem Our task is to place 8 queens on the chessboard in such a way that no queen can attack or overtake another queen on one move
Solving the problem • One strategy would be to guess, i.e. try a solution at random • There are 4,426,165,368 possible ways to arrange 8 pieces on a chessboard of 64 squares • Trying this approach would takes days for a computer to solve the problem • We’re smart because we’ve taken 2315, so we know that there must be a better way to solve the problem at hand
A few observations • Since a queen can move along any horizontal or vertical path, a valid solution to the 8 queens problem has only one queen per column and per row • In other words, each row and column of the chessboard can contain only one queen if it is to be a valid solution to our problem
A prototype solution • By enforcing the row/column rule, we have eliminated the possibility of a queen being on the same row or column • The reduces our number of possibly solutions from > 4 billion to 8!=40,320 solutions • A trial and error approach to 40k solutions is quite a bit more feasible than that of 4 billion • We can now provide a bit more structure for guessing the remaining placements
A first pass solution • Let’s say that we place the first queen on the upper left hand corner of the chessboard (the (1,1) position) • We can now place our second queen • We know she cannot go in either the first row or the first column • We know that she cannot go on the second row of the second column because she will be vulnerable to a diagonal attack from Queen 1 • We therefore choose to place her in the second column on the third row
Placing successive Queens • For Queen 3, we take an approach similar to that of Queen 2 and place her in the third column, fifth row • For Queen 4, we decide to place her in column 4, row 2 • For Queen 5, we decide to place her in column 5, row 4
So far, so good, but… • We’re stuck! • We cannot place a queen in column 6 because the way that we have laid out the board with the previous 5 queens means that a Queen can attack any piece placed in column 6. • Since we have to place a Queen in column 6, we know we do not have a valid solution • We now backtrack to see if we can salvage our solution
Repositioning Queen 5 • We now choose to put Queen 5 in column 5, row 8 • We still have the same problem of a completely vulnerable column 6 • This is still not an acceptable solution • There are no more alternatives to placing a Queen in column 5 with Queens 1-4 being in their current positions, so we backtrack again to the placement of Queen 4
Moving Queen 4 • We now opt to move Queen 4 to row 7 and try our hand at Queens 5-8 again • Queen 5 can be placed in row 2 • Queen 6 can be placed in row 4 • …and so forth • We find that this solution will not work either and backtrack to reposition Queen 5
Moving Queen 5 • We move Queen 5 to row 8 • We place Queen 6 in row 2 • We place Queen 7 in row 4 • We place Queen 8 in row 6 • We now have a valid solution!
Recursively solving this problem • If you can successfully place a Queen in the current column, do so • If you cannot, backtrack one column and reposition that Queen • If you have tried all those positions, backtrack one more column • Repeat until you find a column with a Queen’s position that you haven’t tried
An implementation of the 8 Queens solution • This can be found in your textbook on pp. 240-242. • Typically you would want to place the first Queen randomly in the first column to discover other potential solutions • You could exhaustively search all 40k possibilities to find how many yield valid solutions to the 8 Queens problem (roughly 1k solutions)
Defining languages • English and C++ are two languages with which you should already be familiar • A language is defined as a set of strings and/or symbols that communicate an idea • All programs are strings of some sort, but not all strings are programs
Extending the definition of language • We can expand the definition of the word “language” to include non-programming and non-communicative variants; namely, the set of algebraic expressions forms a language. AlgebraicExpressions=[w:w is an algebraic expression] • The language AlgebraicExpressions is a set of strings that meet certain criteria (rules of syntax), but this definition does not parlay those rules
Grammar (not Kelsey) • The rules for forming a string that conforms to the language are referred to as a grammar. • The grammars that will be covered in this chapter are recursive in their nature, but it should not be inferred that all grammars are recursive!
Benefits to a recursive grammar • Oftentimes a recursive grammar provides an elegant solution to determining if a string is part of a language • Such a recursive algorithm is commonly called a recognition algorithm for the language
Basics of Grammar • A grammar uses several special symbols • x|y means x or y • xy means x immediately followed by y • <word> means any instance of word that the language definition defines • This is probably all very foreign and new to you, so let us begin with an example to elucidate our constructs
C++ Identifiers • Let’s say that we want to construct a grammar for C++ Identifiers • In other words, we want to define which identifiers are valid in the C++ language • More importantly, we want to be able to distinguish them from one another • A grammar for C++Identifiers = [w:w is a legal C++ Identifier] is simple
Valid C++ Identifiers • A valid identifier must begin with a letter and is then followed by 0 or more letters and digits • The underscore character (_) is treated as a letter in this grammar • We can represent this with a syntax diagram like such:
Syntax diagram vs. Grammar • A syntax diagram is a handy visual tool for people to use, but a grammar is generally a better starting point for writing a function that will test the validity of an identifier • The definition is: An identifier is a letter or an identifier followed by a letter or an identifier followed by a digit. • The grammar looks like this: • <identifier>=<letter>|<identifier><letter>|<identifier><digit> • <letter>=a|b|c|…|y|z|A|B|C|…|Y|Z|_ • <digit>=0|1|2|3|4|5|6|7|8|9
A recursive definition of our grammar • The more astute of you will have noticed that – contrary to what your English teacher taught you – the word identifier appears in the definition of the word identifier • This should implicitly tell us that the definition lends itself to recursion (as do a great many (though not all) grammars)
More on the recursive definition • Given a string w you can determine if it is in the language C++Identifiers by using the grammar to construct a recognition algorithm • The algorithm proceeds thusly: • If w.length=1, it is in the language if the character is a letter (this is a base case) • If w.length>1, it is in the language if the last character is a letter or digit and the first length-1 characters form an identifier
Another simple language • Let us move on to a slightly more interesting example; namely, palindromes • A palindrome, as you may know, is a string that reads the same both forward and backward • Radar is a palindrome • Race car is also a palindrome
Defining the language of palindromes • The language of a palindrome can be simply defined in our language as Palindromes=[w:w reads the same from right-to-left as it does left-to-right] • Your visceral reaction might be to say that if w-1 is a palindrome, then w is a palindrome • This is the case with “deeds” • However, this is not the case with “radar”
A little thinking… • If you consider the problem holistically, you will probably notice that you must consider the letters in pairs. • In this manner, w is a palindrome if w-first-last is a palindrome, i.e. if we strip the first and last letters of w • Moreover, w can be a palindrome iff • the first and last characters of w are the same • w-first-last is also a palindrome
Finding a base case • There are two possibilities for our test string • It may have an even number of characters • It may have an odd number of characters • This should signal to you that there will be two base cases • If w.length is even, then we can strip away pairs of letters checking that first=last until we reach the empty string (which is automatically a palindrome) • If w.length is odd, then we can strip away pairs of letters checking that first=last until we reach a single character (which is automatically a palindrome)
A grammar for palindromes • This gives us the following grammar: <palindrome>=empty string | <character> | a<palindrome>a | b<palindrome>b |… <character>=a|b|c|… • Based on this grammar, we can construct a recursive-valued function for recognizing palindromes
Some pseudo-code isPalidrome(in w:string):boolean //Returns true if w is palindrome, //else returns false if (w is empty string or w.length = 1) return true else if (w.first = w.last) return isPalindrome(w-first-last) else return false
Another example • Now let us consider a slightly more complicated example: strings of the form A^nB^n • This is a string of n consecutive As followed by n consecutive Bs • L=[w:w is of the form A^nB^n for some n>0] • The grammar is very similar to that for palindromes • You must strip away the first character to see if it is an A and strip away the last character to see if it is a B • <legal>=empty string|A<legal>B
More pseudo-code isAnBn(in w:string):boolean //Returns true if w is of the form A^nB^n, else returns false if (w is empty string) return true else if (w.first=A and w.last=B) return isAnBn(w-first-last) else return false
Algebraic expressions • One of the primary tasks of a compiler is to recognize and evaluate algebraic expressions • A C++ compiler must determine if the right-hand side of the equation is a syntactically legal algebraic expression • If not, generate an error • If so, determine its value
Parentheses, parentheses everywhere, but not an expression to evaluate • Some expression definitions require that the user fully parenthesize their expressions • Eliminates ambiguity • Simplifies parser coding • Must write ((a*b)*c) instead of a*b*c • The stricter the definition, the easier to recognize a legal statement • The looser the definition, the easier for your users (developers)
Some examples • The languages we’re about to cover are easy to recognize and evaluate • Prefix • Infix • Postfix • However, they are generally inconvenient to use • They are good, non-trivial examples of how to use grammars
Infix expression • The algebraic expressions with which you are most familiar are those you learned in your secondary education • These expressions are called infix expressions, which implies that binary operators occur between the operands • For example, a+b • The operator (+) appears between the operands (a,b)
More on infix • There are associativity and precedence rules to eliminate the ambiguity inherent with this type of expression • Without these rules, what would be the correct evaluation of a+b*c? • With our rules, * is a higher precedence than + • So we have a+(b*c), where (b*c) is the second operand of the addition
Still more on infix • Even with precedence it is difficult to evaluate a/b*c • We know / and * have equal precedence • However, we associate from left to right, so the correct expression is (a/b)*c • All this is a bit cumbersome for the grammar interpreter, but convenient for the grammar user
Alternatives to infix • We have two alternatives to the infix expression a+b • Postfix, where the operator goes after the operands ab+ • Prefix, where the operator goes before the operands +ab • These expression eliminate the need for precedence and associativity rules!
a+b*c • Fully parenthesized, this expression would be a+(b*c) • The prefix expression becomes +a*bc • The postfix expression becomes *+abc
(a+b)*c • In prefix form, *+abc • In postfix form, ab+c* • In general, if the infix expression is fully parenthesized, converting to either prefix or postfix is fairly straightforward
((a+b)*c) • To convert this expression to prefix form, we move each operator to the position marked by its corresponding open parenthesis • Next, we remove all parenthesis to leave us with *+abc • Voila!
((a+b)*c) redux • We employ a similar strategy to convert this expression to postfix form • We move the operators to their corresponding close parenthesis • We then remove all parenthesis, leaving ab+c* • Voila!
Prefix/Postfix vs. Infix • As stated earlier, prefix/postfix remove the stricture of defining precedence and association rules for our grammar • The grammars are therefore very straightforward and simple • Moreover, the recognition algorithms for these expressions are equally facile.
Prefix expressions • A grammar for all prefix expressions: <prefix> = <identifier>|<operator><prefix><prefix> <operator> = + | - | * | / <identifier> = A|B|C|D|…|Z • If a string length is greater than 1, the expression must be of the form <operator><prefix><prefix> to be considered legal
Prefix recognition algorithm • The recognition algorithm must check that • The first character of the string is an operator • Subsequent characters must be two prefix expressions • The first task is trivial, but the second is a bit tricky • Difficult to recognize where one prefix expression ends and the other begins • Can resolve this problem with spaces, commas, tabs…
Evaluating prefix expressions • Since each operator is followed by its two operands, you can “scan ahead” in the expression for them • However, such operands can be prefix expressions as well which must be evaluated first • A recursive solution is quite natural for this problem
Pseudo-code for prefix expression evaluatePrefix(in strExp:string):float //Evaluates a prefix expression string to its //numeric equivalent //Precondition: strExp has a valid prefix expression //Postcondition: returns numerical value of prefix //expression ch = first character of strExp Delete first character from strExp if (ch is an identifier) return value of identifier //base case else if (ch is operator called op) operand1 = evaluatePrefix(strExp) operand2 = evaluatePrefix(strExp) return operand1 op operand2