CS 1321 (Principles of Algorithm Design II):
Homework #7

Assigned:
March 31, 2000
Due:
April 7, 2000, at the beginning of class. Note: Not accepted later than 11:59pm April 7.
Credit:
20 points.



Before getting started

Reading

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.

Program debugging

The gdb debugger permits running programs in ``slow motion,'' permitting examination of how a program runs. For information how to use gdb, see Jeffrey Oldham's short introduction or the notes at Stanford.

Here is a very quick introduction to gdb.

  1. Compile your program using the -g compiler flag, e.g.,

            g++ -g -Wall -pedantic foo.cc -o foo
    

    This causes the compiler to write information used by the debugger.

  2. Run gdb inside xemacs or emacs by typing ``Alt-x gdb'' or ``ESC x gdb''. In response to the prompt asking for the command to use for running gdb, type ``gdb'' and the name of your executable (e.g., a.out).

  3. In the gdb window, type ``run''. (Note that the ``window'' is a text-based window managed by emacs, so this will work in a telnet session too.)

  4. To obtain help, type ``help''.

  5. If you're not familiar with emacs, the following commands may be of interest: ``Control-x o'' moves the cursor to the other window. ``Control-x 1'' closes other windows, leaving only the one containing the cursor. ``Control-x Control-c'' exits emacs.

Note that you can also run gdb from the Unix command prompt; the command is gdb followed by the name of your executable. If you do this, you won't get an emacs window showing program source; to view source, you can use the gdb list command (type ``help list'' after starting gdb for more information).

Problem statement

You and possibly a CS 1321 classmate 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.cc yields a program that checks Boolean expressions for tautologies.

Stacks

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 implements a stack.

Your implementation should support all the operations listed in Table 1. These operations are similar but not identical to those provided by the STL stack class.


   
Table 1: stack member functions and types
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; create 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.
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.

Stack implementation

You can choose any implementation strategy you like for your stack class except that you may not use the STL stack class or any implementation similar to the source code in the textbook. 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 (but why implement it using dynamic memory yourself?).

Stack implementation tips

1.
First implement a stack with a typedef statement declaring the type of its elements:

    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 C++ keyword provides alternative syntax for defining templates. In the textbook and in class, template parameters are declared using syntax like <class T>. An alternative syntax is <typename T>.

    The typename keyword is also useful whenever the compiler cannot determine that an expression is a type. For example, it is sometimes necessary to write

        typedef typename stack<T>::size_type foo;
    

    for reasons only apparent 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.

    Boolean expressions

    Boolean expressions and reverse Polish notation

    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 || true
    

    are 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 lists the two operands (using reverse Polish notation) first 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.

    Well-formed expressions

    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.

    Truth tables

    To evaluate Boolean expressions, we need to be able to evaluate the simplest Boolean expressions:

    • o1 o2 && is true if and only if both o1 and o2 are true.
    • o1 o2 || is false if and only if both o1 and o2 are false.
    • o1 ! is the ``opposite'' of o1.
    • o1 o2 => reads as ``if o1, then o2''. It is true if o1 is false or o2 is true. It is false if and only if o1 is true and o2 is false.
    • o1 o2 == is true if o1 and o2 have the same truth value.
    Truth tables with the first operand on the left side and the second operand on the right side appear in Figure 1.


      
    Figure 1: Truth tables for Boolean expressions
    \begin{figure}\begin{center}
\begin{tabular}{c\vert cc}
\texttt{\&\&}& true & fa...
...ue & true & false \\
false & false & true
\end{tabular}\end{center}\end{figure}

    Determining a Boolean expression's value

    Evaluating a reverse Polish notation is easy using a stack. See Figure 2.


      
    Figure 2: Example evaluation of a reverse Polish expression
    \begin{figure}\begin{center}
\begin{tabular}{rlr}
step & \multicolumn{1}{c}{stac...
...xttt{\vert\vert}}\ \$ \\
10&\$ false & \$
\end{tabular}\end{center}\end{figure}

    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:

    1. If the next token is true or false, move it to the stack.
    2. If the next token is a binary operator, i.e., one requiring two operands, remove the two operands from the stack, replacing them with the result of applying the operator to them, and remove the operator from the expression. If fewer than two operands are present on the stack, the Boolean expression is not well-formed, i.e., it has incorrect syntax.
    3. If the next token is a unary operator, i.e., one requiring one operand, remove the operand from the stack, replacing it with the result of applying the operator to it, and remove the operator from the expression. If the stack is empty, the Boolean expression is not well-formed, i.e., it has incorrect syntax.
    4. If the next token is $, stop. If the stack has one value, it is the expression's value. Otherwise, the expression is not well-formed.
    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 onto 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.

    Tautologies

    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,

        true
    

    is 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,

        x
    

    and

        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 its N variables. If all of them evaluate to true, the original expression is a tautology; otherwise it is not.

    Programming an evaluator

    Write a function evaluate evaluating a Boolean expression without any variables. Given an expression in reverse Polish notation represented as vector of strings, it should 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 tautology-checker.cc 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.

    What files do I need?

    For the templatized stack class you are to write, we have provided a minimal stack.h and test program test-stack.cc.

    For the tautology checker program, start with our tautology checker program and add an evaluate() function and any helper functions. A prototype for evaluate() is already included.

    Rules

    Programming rules

    As for previous homeworks, working with other people during your planning phase is encouraged. For this homework, you are permitted to write the code, i.e., program, with one other person in CS 1321. To learn the material, both of you should be actively involved in the programming. Failure to do so will almost certainly hurt your comprehension of the material in the rest of the course.

    Submission rules

    Each one- or two-person team of programmers should submit only its completed implementation, consisting of the files stack.h and tautology-checker.cc. You do not need to send any other files. Please send only text documents, do not send Microsoft Word documents, PDF documents, HTML documents, etc. Please include both your names and email addresses at the top of your program.

    We will test your stack implementation using our own programs and our own choice of stack elements; we will also test your completed tautology-checker program. Please be sure your code compiles without warning when using g++ -Wall -pedantic. You should not need to change anything in the tautology checker program other than adding code for the evaluate() function and possibly some helper functions, but if you are unable to resist tinkering with our code, please do not change its interface, i.e., what input it accepts and what output it produces.

    See the guidelines for programming assignments for instructions on how to e-mail the programs. For this assignment use a subject line of ``cs1321 homework 7''.

    If a team of two are in different sections, submit exactly once to one of the two permissible email addresses.

    Running time of the tautology checker

    Given a Boolean expression with v variables and n operators, our tautology checker requires O(2v n) time. While exponential running times are acceptable for small values of v, they quickly become infeasible for larger values. 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 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., O(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., ``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. For more information, read Foundations of Computer Science, by Alfred V. Aho and Jeffrey D. Ullman, ISBN 0-7176-8233-2, p. 649.


    Copyright notice

    ©2000 Jeffrey D. Oldham . All rights reserved. This document may not be redistributed in any form without the express permission of the author.