The textbook's chapter 6 covers defining templatized classes. Chapter 7 discusses stacks. Be sure to read Section 7.4, concentrating on reverse Polish notation expressions.
You are to implement a stack data structure capable of holding any element type. Then you are to write code using the stack to evaluate a Boolean expression written in reverse Polish notation. Combining this with distributed source code tautology-checker.cpp yields a program that checks Boolean expressions for tautologies.
A stack is a last-in/first-out data structure with objects arranged in linear order. That is, it permits easy access only from one end. Entries can be added or removed only at the rightmost end. For example, the STL stack class class implements a stack.
Your implementation should support the operations listed in the following table. These operations are similar but not identical to those provided by the STL stack class.
Function prototype | Example use | Explanation |
---|---|---|
value_type | value_type x; | type of items on stack. |
size_type | size_type n; | type for size of stack (number of elements). |
stack<T>(void) | stack<int> s; | creates a stack of elements with type T but no items. |
bool empty(void) const; | bool b = s.empty(); | returns true if stack has no elements, false otherwise. |
size_type size(void) const; | stack<int>::size_type sz = s.size(); | returns number of elements currently in stack. |
void push(const value_type & x); | s.push(x); | adds x to stack. |
void pop(void); | s.pop(); | pops (removes) top element of stack. Nothing is returned. It is the user's responsibility to ensure the stack is not empty before calling this function. |
value_type top(void) const; | int x = s.top(); | returns top element of stack without changing the stack. It is the user's responsibility to ensure the stack is not empty before calling this function. |
You can choose any implementation strategy you like for your stack class except that you may not use the STL stack class. It should be possible to use your templatized class to create and manipulate stacks of ints, doubles, strings, bools, etc., with any number of elements. Your implementation should correctly use dynamic memory (i.e., deep rather than shallow copies, no memory leaks, etc.). Observe, however, that you may be able to achieve this goal with very little effort, if you implement your class using a class that already uses dynamic memory correctly (as we did when we defined a double-ended queue class deque.h using our doubly-linked-list (dll) class). You may similarly use any class we have defined in lecture, or any STL class (except for stack).
typedef string value_type;Then templatize the class and test using a different type. Using a different type will check that all occurrences of value_type were actually found.
The typename keyword is also useful whenever the compiler cannot determine that an expression is a type, as in the following example:
typedef typename stack<T>::size_type foo;Exactly when and why this is necessary is apparently only obvious to people who have compilers inside their heads. Heuristic: If the compiler becomes terribly confused about a type, and the type contains a template parameter, try adding the keyword typename before the expression.
A Boolean expression consists of variables, true, and false connected together by Boolean operators &&, ||, =>, !, and ==, and possibly parentheses. For example,
(x && y) || (! x && ! y)and
! p || trueare Boolean expressions. Using infix notation, where the Boolean operators appear between their operands, can require using parentheses. Instead, we will use reverse Polish notation. Using this notation, the previous expressions are written as
x y && x ! y ! && ||and
p ! true ||Reverse Polish notation first lists the two operands (using reverse Polish notation, if they are expressions) and then the operator. For example: In the second example, the first operand is ! p, the operator is ||, and the second operand is true. The reverse Polish notation for the first operand is p !. Listing the two operands and then the operator yields the expression.
Intuitively, a well-formed expression has the correct number of operands and operators arranged in the correct order. It is defined recursively:
A well-formed expression is either true, false, a variable, or an expression p q &&, p q ||, p q =>, p q ==, or p !, where p and q are well-formed expressions.
To evaluate Boolean expressions, we need to be able to evaluate the simplest Boolean expressions, as follows.
We could also express these rules in the form of truth tables as follows:
&& | true | false |
true | true | false |
false | false | false |
|| | true | false |
true | true | true |
false | true | false |
! | |
true | false |
false | true |
=> | true | false |
true | true | false |
false | true | true |
== | true | false |
true | true | false |
false | false | true |
Evaluating an expression in reverse Polish notation is easy using a stack, as in the following example.
Step | Stack | Expression left to scan |
---|---|---|
1 | $ | true false && true ! false ! && || $ |
2 | $ true | false && true ! false ! && || $ |
3 | $ true false | && true ! false ! && || $ |
4 | $ false | true ! false ! && || $ |
5 | $ false true | ! false ! && || $ |
6 | $ false false | false ! && || $ |
7 | $ false false false | ! && || $ |
8 | $ false false true | && || $ |
9 | $ false false | || $ |
10 | $ false | $ |
Initially, the stack is empty; for expositional purposes, we use $ to denote the bottom of the stack so we can tell it is empty. Initially, we start with the entire expression; we mark its end using a $. The rules are:
For example, the first two steps move Booleans from the expression to the stack. In the third step, the && operator beginning the expression is removed, the top two Boolean expressions are popped off the stack, and the result is pushed on the stack. In step 10, the entire expression has been processed. Since there is one Boolean on the stack, it is the value of the expression and the expression was well-formed.
In addition to Boolean expressions involving only true, false, and the operators described earlier, we can write Boolean expressions involving variables. Such an expression has a value for any assignment of Boolean values to its variables. For example, consider the expression p q ||. There are 22 ways of assigning values to its two variables, since each variable can be either true or false. For each way of assigning values to p and q, we can then evaluate the resulting expression. The result is false if both p and q are false and true for the other three choices.
A tautology is a Boolean expression that evaluates to true for all possible ways of assigning values to its variables. For example,
trueis a tautology, as are
x x ==and
x ! x ||and
x x ==y y == &&since all evaluate to true for any way of assigning values to their variables. However,
xand
x y =>are not tautologies, because there is some way of assigning values to their variables that makes them evaluate to false.
Given a Boolean expression with N variables, one way of determining whether it is a tautology is to evaluate the 2N possible expressions resulting from assigning different combinations of Boolean values to the N variables. If all of them evaluate to true, the original expression is a tautology; otherwise it is not.
In this part of the assignment, you are to add code to program tautology-checker.cpp.
Specifically, you are to write a function evaluate() evaluating a Boolean expression without any variables. The function is to take as input an expression in reverse Polish notation, represented as a vector<string>; it is to return a pair of Booleans, the first indicating whether the expression was well-formed and, if well-formed, the second indicating the expression's value.
The provided code reads a Boolean expression with variables from the standard input and cycles through all possible variable assignments, invoking evaluate() to determine the expression's value. If the expression is true for all assignments, the program indicates that it is a tautology. Otherwise, the program indicates that it is not a tautology or is not well-formed.
The user-provided expression must be in reverse Polish notation with all variables, operators, and keywords separated by whitespace. Any whitespace-delimited sequence of characters other than an operator, true, or false is considered to be a variable. Here are some examples of possible input expressions, each involving two variables:
x y ! ||Observe that the last two expressions are not well-formed. This should be detected by your evaluate() function.
hello goodbye &&
hello goodbye
hello goodbye && &&
Notice that although input to the program can include variables, input to your evaluate() function will consist of true, false, and operators only. The end of the expression is indicated by the end of the vector; that is, there is no explicit marker $ as there was in the example shown earlier.
For the first part of the assignment (writing a templatized stack class), you may start from scratch, or you may make use of the following files:
For the second part of the assignment (completing the tautology-checker program), you will need the following file:
Add to this file an evaluate() function and any needed helper functions. A prototype for evaluate() is already included. You should not need to make any changes in this program other than adding code for the evaluate() function and possibly some helper functions.
Submit the following two source-code files:
You do not need to submit a test program for the stack class.
Given a Boolean expression with v variables and n operators, our tautology checker requires time roughly proportional to 2vn time. While exponential running times are acceptable for small values of v, they quickly become infeasible. If you want to see this in action, you can use Perl program generate-tautology.pl to generate input for the tautology checker. It takes one command-line argument specifying the number of variables. (To use this program, save it into a file and make the file executable with the command chmod +x generate-tautology.pl.) To time the tautology checker program, you can use the timer() function in timer.h.
Can we find a faster algorithm? No one has yet been successful. There is a family of NP-complete problems, all of which are currently thought to be difficult to solve. We can prove that if any of these problems can be solved in polynomial time, i.e., in time roughly proportional to nk for some fixed k, then all these problems can be solved in polynomial time; conversely, if one of these problems can be proved to require more than polynomial time, then they all do. Satisfiability, i.e., answering the question ``is there an assignment making the Boolean expression true?,'' is the most famous NP-complete problem. The tautology problem is at least as hard as, or harder than, satisfiability. so do not be frustrated by not finding a faster algorithm. (You would become very famous among computer scientists if you found one!) For more information, read Foundations of Computer Science, by Alfred V. Aho and Jeffrey D. Ullman, ISBN 0-7176-8233-2, p. 649.
The gdb debugger allows you to run your program in stop-motion form, i.e., to step through it a line at a time, examining variables as you go. This section attempts to present just enough information about gdb to get you started; for more information, see J. Oldham's short introduction, or the complete on-line manual.
To use gdb, proceed as follows.
g++ -g -Wall -pedantic foo.cc -o fooThis causes the compiler to write information used by the debugger.
gdb foo(Replace foo with the name of your executable, e.g., a.out.)
break mainIf your program needs command-line arguments, include them in the run command, e.g.,
run
run anArgument anotherArgument
x = foo(10);n will take you to the line outputting the value of x, while s will take you to the first line of function foo.
cout << x << endl;
int x;then the following commands should all work:
double y[10];
pair<char,char> z;
p x
p y[5]
p z.first
Just pressing return repeats the most recent command again.
gdb also runs very nicely under emacs and xemacs; the main editor window is split into two windows, one for gdb commands and output and the other showing source code (with an arrow indicating the next line to execute). To try this out, start emacs or xemacs and type M-x gdb. (The M-x is ``meta-x'', probably either Alt-x or ESC-x on your keyboard.) You will be prompted for the name of the program; type in the name of your executable (e.g., a.out or foo).
You might also want to try xxgdb, which provides a graphical interface for gdb. Start it up by typing xxgdb foo, where foo is the name of your executable.