ECS 110: Data Structures and Programming Discussion Section Notes -- Week 3 Ted Krovetz (tdkrovetz@ucdavis.edu) John Black (blackj@cs.ucdavis.edu) We began with a brief discussion of the design aspects of hw1. We saw many people using only a clause class when the object of interest was an instance; most operations logically belonged under some sort of instance class since pretty much all the steps involved operated on an instance. Of course, these often boiled down to operations on clauses, which might or might not be a separate class for your design. Next we briefly discussed the design decisions we made when implementing our solutions to this problem and why. 1. HW 2: FUZZY LOGIC -------------------- With regular old logic, like we used in hw1, all variables took on values of either 0 (false) or 1 (true). Another system of logic, which has had many uses (particularly in the field of artificial intelligence), is called "fuzzy logic". In this system, we have 0 for false and 1 for true, but we also have all the real values in between. How can this be? For example, what "truth" value does 0.5 have? It is true or false? Well, the answer is both and neither. Instead of answering a question confidently and completely, fuzzy logic just expressed the "degree of truth" about a statement. Let's take an example... let's define a function which returns a real number between 0 and 1 (inclusive) which takes as its argument, the name of a person. Let's call this function "Old()". Now the oldest currently living person is Jeanne Louise Calment, and she's 121. Pretty much everyone here would agree that's old. But what about someone who's 30, is that old? Let's define the function Old() to return the age of the person divided by 100, but limited to the range 0 through 1. Let's compute a few values: Old(new born baby) = 0 Old(Richard Dreyfus) = .51 Old(Winona Ryder) = .26 Old(George Burns) = 1 Old(Your TA) = .3 You get the idea: the result of the function is a "degree of oldness" about the person in the parentheses. So now, let's define another function called "Rich()". This will be a "step function" just like the last one: / 0 if net worth < $1000 Rich(person) = < (net worth)/1,000,000 $1000 <= net worth <= $1,000,000 \ 1 if net worth > $1,000,000 Now we can make an expressions based on these functions, just like we did with normal boolean logic; we will have AND, OR, NOT, and IMPLIES. We know how these work for the boolean case, but in our fuzzy case we'll instead use min() for AND, max() for OR, 1-value for NOT, and the usual A implies B means NOT(A) or B. Ok, are we ready for an example? Old(TA) IMPLIES Rich(TA) Is this true or false? Well, remember, we're dealing with fuzzy logic, so the answer might not be true or false; it might be 0.712. But let's do the calculation. Old(TA) is .3, as we saw. And Rich(TA) is clearly 0. So we have NOT(.3) OR 0. The NOT() changes .3 to .7, so we have .7 OR 0. And since OR() is a max operation, we take .7 as the answer. Now .7 may seem kind of high to you intuitively; I mean here we have a poor TA who's getting on it years, and yet the truth of this statement is well above 1/2. Well, as in normal boolean logic, implication can be confusing. Remember, a false antecedent implies any consequent (so the statement "1+1=3" implies "my dog is from Mars"). Therefore, since the TA isn't really THAT old, the antecedent is mostly false, and therefore the implication is mostly true. Perhaps something more intuitive would be the expression Old(TA) AND Rich(TA). Using the same values as above we get MIN(.3,0) and the answer is 0. The TA is not both old and rich. Ok, now that we have some intuition about fuzzy logic, let's talk about the assignment. Here is a sample input file: age net_worth Winona 26 12,000,000 TA 30 14.97 OJ 48 22,000 . Old(Winona) and (Old(TA) or (Rich(TA)) and Rich(Winona). Rich(OJ). Ok, so the format is a list of attributes followed by a period. Then a list of people, one per line with real numbers for each attribute. Then a period on a line by itself to end the person list. Then we have a bunch of expressions, each ending with a period. You must detect end of file to know the expression list is ended. Note that in the first expression parens were used; you must use precedence from highest to lowest of NOT, AND, OR, and then IMPLIES, unless parens are used to override this. Error checking: if you ever cannot parse a line, print an error and continue to the next line. Your output will echo the expression, then convert it to postfix, then output the truth value. How will you get the truth value? We will supply you with two files called "Functions.h" and "Functions.cpp" which will define the functions to be used in any of the input files (if you don't find an appropriate function, print an error). So the steps for your program are: 1. Read in the person list and the attributes and store them 2. Read in an expression and echo it 3. Convert to postfix and echo it 4. Evaluate the expression using our functions as needed to get truth values 5. Goto 2 until EOF The parsing is detail-intensive, but not hard to understand. You may not get exactly how this function list we're handing you is supposed to work. There is a main.cpp file on the web page showing an example, but let's talk about it briefly. We're going to hand you a pointer to an array of structs which will contain a certain number (num_functions) of such pointers. Each pointer will point to a struct of type Function. Each struct of this type will contain the name of the function and a pointer to the actual function which implements it, which always takes an Arg_List reference as input and always returns a double (which will be the truth value of the expression). In order to call a function from the "functions" list, you do the following: 1. Search through the array of pointers from 0 to num_functions looking for the name of a function which matches the one you are trying to execute. 2. Once you have located it, you will have a struct with a pointer to the function you wish to call. Before you call it, you must package up the arguments you will be sending it. To do this, get the values (which will all be doubles) and put them into a type Arg_List struct which holds the number of arguments and pointers to arrays of doubles which is the attribute list for each person. Note that it's kind of hokey that each of the functions you call will know which attribute is which merely by it's position. 3. Finally, call the function passing in your Arg_List and storing the return value you get back. That may seem like a lot, but remember, you only code this once and it works for calling any functions. So question to the alert listener: why are we passing in an arg_count? 2. BACKTRACKING --------------- Backtracking is a general method of problem solving; the idea is kind of like trying to solve a maze: when presented with a choice for which you do not know the answer (e.g. the intersection point of a maze), you choose one and continue searching. If you ever hit a dead-end, you back up to the place where more unexplored choices still exist and try again from there. In general, this approach takes exponential time. In fact, we could have used a backtracking algorithm to solve the 3-CNF SAT problems; the advantage would have been that no random numbers would have been involved, and therefore the algorithm's upper bound would be finite (but huge). Remember that our WalkSAT algorithm could theoretically run forever. The general scheme for backtracking goes like this: { k = 0; do k++; select k-th candidate; if acceptable { record it; if (i < n) { recursively try(i+1); if (!successful) cancel recording; } } } while (!successful && k < total_candidates) } Today we're going to solve a different problem using backtracking; the problem is called "N queens." The problem is to place N queens on an N by N chessboard such that no two queens attack each other. C.F. Gauss first investigated this problem for N=8 in 1850 but wasn't able to completely solve it. But computers are patient and often can solve these problems better than people, even geniuses. Let's try N=4 with backtracking. ----------------- | Q | | | | |---+---+---+---| | | | | | |---+---+---+---| | | Q | | | |---+---+---+---| | | | | | |---+---+---+---| and here it's clear we can't place the 3rd queen in the 3rd column in a nonattacking position, so we give up and try moving the 2nd queen down one: ----------------- | Q | | | | |---+---+---+---| | | | Q | | |---+---+---+---| | | | | | |---+---+---+---| | | Q | | | |---+---+---+---| and again we see no solution, so we pick up the 3rd and 2nd queens and move the 1st queen down one: ----------------- | | | Q | | |---+---+---+---| | Q | | | | |---+---+---+---| | | | | Q | |---+---+---+---| | | Q | | | |---+---+---+---| and a solution is found!! Ok, let's write a C++ program to solve this problem for N=8: #include #include int x[8]; // x[i] is row of i-th queen int a[8]; // a[i] true if i-th row free int b[15]; // b[i] true if i-th down-left diag free int c[15]; // c[i] true if i-th down-right diag free void print(int *x) { for (int i=0; i < 8; i++) cout << setw(4) << x[i]; cout << endl; } void try(int i) { for (int j=0; j < 8; j++) if (a[j] && b[i+j] && c[i-j+7]) { x[i] = j; a[j] = b[i+j] = c[i-j+7] = 0; if (i < 7) try(i+1); else print(x); a[j] = b[i+j] = c[i-j+7] = 1; } } int main() { int i; for (i=0; i < 8; i++) a[i] = 1; for (i=0; i < 15; i++) b[i] = 1; for (i=0; i < 15; i++) c[i] = 1; try(0); } Now recursion can be slow, so instead of having the computer do the recursion, we'll re-implement the program using our Stack ADT from discussion #3 to keep track of where we've left off. #include #include #include "Stack.h" int x[8]; // x[i] is row of i-th queen int a[8]; // a[i] true if i-th row free int b[15]; // b[i] true if i-th down-left diag free int c[15]; // c[i] true if i-th down-right diag free Stack st; // stack of ints void print(int *x) { for (int i=0; i < 8; i++) cout << setw(4) << x[i]; cout << endl; } void try(int i) { restart: for (int j=0; j < 8; j++) if (a[j] && b[i+j] && c[i-j+7]) { x[i] = j; a[j] = b[i+j] = c[i-j+7] = 0; if (i < 7) { st.Push(i); st.Push(j); i++; goto restart; } else print(x); resume: a[j] = b[i+j] = c[i-j+7] = 1; } if (!st.Empty()) { j = st.Top(); st.Pop(); i = st.Top(); st.Pop(); goto resume; } } int main() { int i; for (i=0; i < 8; i++) a[i] = 1; for (i=0; i < 15; i++) b[i] = 1; for (i=0; i < 15; i++) c[i] = 1; try(0); } Well, this code represents what the compiler essentially does, and it works fine, but it obviously isn't very structured: there are goto's in there after all! So our final version re-writes things a little more nicely. Notice that i and j are packaged into a single struct and that the Stack is of this type. We won't discuss the details, but the code is included for completeness. #include #include #include "Stack.h" int x[8]; // x[i] is row of i-th queen int a[8]; // a[i] true if i-th row free int b[15]; // b[i] true if i-th down-left diag free int c[15]; // c[i] true if i-th down-right diag free struct Posrec {int i; int j;}; Stack st; // stack of position records void print(int *x) { for (int i=0; i < 8; i++) cout << setw(4) << x[i]; cout << endl; } void try(void) { Posrec pr = {0, 0}; do { while (!(a[pr.j] && b[pr.i+pr.j] && c[pr.i-pr.j+7]) && pr.j < 8) pr.j++; if (pr.j == 8 && !st.Empty()) { pr = st.Top(); st.Pop(); a[pr.j] = b[pr.i+pr.j] = c[pr.i-pr.j+7] = 1; pr.j++; } else { if (pr.j != 8) { x[pr.i] = pr.j; a[pr.j] = b[pr.i+pr.j] = c[pr.i-pr.j+7] = 0; if (pr.i == 7) { print(x); a[pr.j] = b[pr.i+pr.j] = c[pr.i-pr.j+7] = 1; pr.j++; } else { st.Push(pr); pr.i++; pr.j = 0; } } } } while (!st.Empty() || pr.j < 8); } int main() { int i; for (i=0; i < 8; i++) a[i] = 1; for (i=0; i < 15; i++) b[i] = 1; for (i=0; i < 15; i++) c[i] = 1; try(); }