Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

C++ For Mathematicians (2006) [eng]

.pdf
Скачиваний:
211
Добавлен:
16.08.2013
Размер:
31.64 Mб
Скачать

176

C++ for Mathematicians

9.5Create a class to represent Hamilton’s quaternions.

The quaternions are an extension to the complex numbers. Each quaternion can be written as a + bi + c j + dk where a,b,c,d R and i, j,k are special symbols with the following algebraic properties:

i2 = j2 = k2 = −1

i j = k

ji = −k

jk = i

k j = −i

ki = j

ik = − j

Addition is defined as expected:

(a + bi + c j + dk) + (a0 + b0i + c0 j + d0k) = (a + a0) + (b + b0)i + (c + c0) j + (d + d0)k.

Multiplication is not commutative (as evidenced by the fact that i j 6= ji), but otherwise follows the usual rules of algebra. For example,

(1 + 2i + 3 j −4k)(−2 + i + 2 j + 5k) = 10 + 20i −18 j + 14k.

The set of quaternions is denoted by H.

Include the standard operators +, - (unary and binary forms), *, and /, and the combined assignment forms +=, -=, *=, and /=.

Also include the standard comparison operators == and !=, plus an << operator for writing quaternions to the screen.

Chapter 10

The Projective Plane

In this chapter we develop C++ classes to represent points and lines in the projective plane. Because such points and lines share many properties the classes and methods for their classes are extremely similar. Rather than write two nearly identical classes, we first build a base “projective object” class that implements the common functionality. We then use the idea of inheritance to establish classes representing points and lines.

10.1 Introduction to the projective plane, RP2

The points of the Euclidean plane R2 are ordered pairs of real numbers (x,y). Lines are point sets of the form {(x,y) : ax + by = c} where a,b,c R and ab = 0.

The projective plane RP2 is an extension of the Euclidean plane. To the points in R2 we add additional points “at infinity.” These points are in one-to-one correspondence with the slopes of lines in the Euclidean plane. More formally, we say that two lines in the Euclidean plane are equivalent if they are parallel. The points at infinity of the projective plane are in one-to-one correspondence with the equivalence classes of parallel lines. In RP2, each line from the Euclidean plane is given one additional point corresponding to its slope. Finally, all the points at infinity are deemed a line as well, and this line is called the line at infinity.

Alternatively, the points in RP2 are in one-to-one correspondence with (ordinary) lines through the origin in R3. The lines of RP2 correspond to (ordinary) planes through the origin in R3.

This leads to a natural way to assign coordinates to the points of RP2. Every point in RP2 is assigned a triple of real numbers (x,y,z). Two triples name the same point provided they are nonzero scalar multiples of each other. For example, (2,4,−10) and (1,−2,5) name the same point. The triple (0,0,0) is disallowed.

The original points of the Euclidean plane correspond to triples in which z is nonzero. The point (x,y,1) RP2 is identified with the point (x,y) in the Euclidean plane. Points at infinity are identified with triples in which z = 0.

Lines in the projective plane are sets of the form {(x,y,z) RP2 : ax+by+cz = 0} where a,b,c R and are not all zero. For example, (1,2,3), (1,1,3), and (1,−2,0) are collinear points as they all lie on the line {(x,y,z) : 2x+yz = 0}. It is convenient

177

178 C++ for Mathematicians

to name lines as a triple [a,b,c]:

[a,b,c] = {(x,y,z) : ax + by + cz = 0}.

We use square brackets to name lines so we don’t confuse points and lines. Note that if [a ,b ,c ] is a nonzero scalar multiple of [a,b,c], then the two triples name the same line. The line at infinity is [0,0,1]. Only the triple [0,0,0] is disallowed.

To determine if a point (x,y,z) is on a line [a,b,c], we simply need to check if the dot product ax + by + cz equals zero.

From our discussion, we see that there is a duality between points and lines in the projective plane. Given any true statement about points and lines in RP2, when we exchange the words “point” and “line” (plus some minor grammar correction) we get another true statement. For example, the following dual statements are both true:

Given two distinct points of the projective plane, there is exactly one line that contains both of those points.

Given two distinct lines of the projective plane, there is exactly one point that is contained in both of those lines.

One of the celebrated results in projective geometry is the following.

Theorem 10.1 (Pappus). Let P1,P2,P3 be three distinct collinear points and let Q1,Q2,Q3 be three other distinct collinear points. Let Xi be the intersection of the lines PjQk and PkQj where i, j,k are distinct and 1 i, j,k 3. Then the three points X1,X2,X3 are collinear.

Pappus’s theorem is illustrated in Figure 10.1.

The dual statement to Pappus’s theorem is this.

Theorem 10.2 (Dual to Pappus). Let L1,L2,L3 be three distinct concurrent lines and let M1,M2,M3 be three other distinct concurrent lines. Let Xi be the line through the points of intersection Lj Mk and Lk Mj where i, j,k are distinct and satisfy 1 i, j,k 3. Then the three lines X1,X2,X3 are concurrent.

The dual to Pappus’s theorem is illustrated in Figure 10.2.

10.2 Designing the classes PPoint and PLine

Our goal is to create C++ classes to represent points and lines in the projective plane. We call these PPoint and PLine. We need to decide how to represent these objects (i.e., what data are needed to specify the objects) as well as the methods, operators, and procedures that act on these objects.

Here are our decisions.

 

The Projective Plane

179

 

 

 

P1

P2

P3

X3

X2

X1

Q1

Q2

Q3

Figure 10.1: An illustration of Pappus’s theorem.

L3 M1

X2

X1

X3

M2

L1

L2

M3

Figure 10.2: An illustration of the dual of Pappus’s theorem.

180

C++ for Mathematicians

Points are to be stored as triples (x,y,z) giving the homogeneous coordinates of the point.

Likewise, lines are to be stored as triples, [x,y,z].

We want to be able to test points for equality (and inequality) and sort by < so they can be held in C++ containers such as sets.

Likewise, we need to be able to test lines for the same relations.

Given two points, we want to be able to find the unique line that contains them.

However, if the two points are the same, we return an invalid point; we signal this with coordinates (0,0,0). For points X and Y, the notation X+Y is a good way to express the line through X and Y.

Likewise, given two lines, we want to be able to find the unique point of inter-

section of these lines. However, if the two lines are the same, then we return an invalid line; we signal this with the triple [0,0,0]. For lines L and M, the notation L*M is a good way to express the point of intersection.

Given a point and line, we want to be able to determine if the point lies on the line.

We want to be able to determine if three points are collinear. Likewise, we want to be able to determine if three lines are concurrent.

We want to be able to generate a random point or line in the projective plane.

In addition, given a line, we want to be able to choose a random point on the line. Likewise, given a point, we want to choose a random line through the point.

We want to be able to send points and lines to output streams, writing points in the form (x,y,z) and lines in the form [x,y,z].

Notice that nearly every requirement for points has a matching requirement for lines. Thus, the code we need to write in the two instances would be nearly identical. To cut our work load in half, we exploit C++’s inheritance mechanism. We create a parent class named PObject that has two children: PPoint and PLine. As much as possible, we embed the functionality we need in the parent class, and then the children access this functionality for their own purposes.

In addition to reducing the workload in creating the classes, putting the common functionality of the classes into the parent reduces our workload in maintaining the classes. If there is an error in an algorithm, or if we create a more efficient version of the algorithm, we only need to replace the code in the parent class; we do not need to edit two separate versions.

The Projective Plane

181

10.3 Inheritance

Before we create the classes PPoint and PLine, we illustrate how one class is derived from another. That is, a class (let’s call it Parent) is created first with certain properties. Then we create a new class (call it Child) that has all the properties of Parent plus additional properties.

For the toy example we are about to present, the Parent class houses two double real values, x and y. We provide a simple constructor to set x and y and two methods: sum() that calculates the sum x+y and print() that writes the object to the screen in the format (x,y).

The Child retains all the data and functionality of Parent but adds the following additional features. The new class has an additional data element: an integer k. It provides a new method value() that returns (x+y)*k and a new version of print() that writes the Child object to the screen in the format (x,y)*k.

Here is the code that accomplishes all these tasks.

Program 10.1: A program to illustrate inheritance.

1#include <iostream>

2using namespace std;

3

4class Parent {

5private:

6double x, y;

7public:

8Parent(double a, double b) { x = a; y = b; }

9double sum() const { return x+y; }

10void print() const { cout << "(" << x << "," << y << ")"; }

11};

12

13class Child : public Parent {

14private:

15int k;

16public:

17Child(double a, double b, int n) : Parent(a,b) {

18k = n;

19}

20double value() const { return sum()*k; }

21void print() const { Parent::print(); cout << "*" << k; }

22};

23

24int main() {

25Parent P(3., -2.);

26P.print();

27cout << " --> " << P.sum() << endl;

28

29Child C(-1., 3., 5);

30C.print();

31cout << " --> " << C.sum() << " --> " << C.value() << endl;

32

182

C++ for Mathematicians

33return 0;

34}

The Parent class is defined on lines 4–11. There is nothing new in this code; we have kept it short and simple to make it easy for you to read. Please look through it carefully before moving on.

The Child class is defined on lines 13–22 and there are several important features we need to address.

To begin, the class Child is declared to be a public subclass of the class Parent on line 13:

class Child : public Parent {

The words class Child announce that we are beginning a class definition. The colon signals that this class is to be derived from another class. (Ignore the word public for a moment.) And the word Parent gives the name of the class from which this class is to be derived.

The keyword public means that all the public parts of Parent are inherited as public parts of the derived class Child. The Child class has full access to all the public parts of Parent but does not have any access to the private parts of Parent.

(Aside: Had we written class Child : private Parent then the public parts of Parent would become private parts of Child. As in the case of public inheritance, the private parts of Public are not accessible to Child.)

On lines 14–15 we declare a private data element for Child: an integer k. The class Child therefore holds three data elements: x, y, and k.

A method in Child can access k but not x or y. The latter are private to Parent and children have no right to examine their parents’ private parts.

Next comes the constructor for the Child class (lines 17–19). The constructor takes three arguments: real values a and b (just as Parent does) and additional integer value n (to be saved in k).

What we want to do is save a, b, and n in the class variables x, y, and k, respectively. However, the following code is illegal.

Child(double a, double b, int n) { x = a; y = b; k = n;

}

The problem is that Child cannot access x or y.

Logically, what we want to do is this: first, we want to invoke the constructor for Parent with the arguments a and b. Second, we do the additional work special for the Child class, namely, assign n to k.

The Projective Plane

183

Look closely at line 17. Before the open brace for the method we see a colon and a call to Parent(a,b). By this syntax, a constructor for a derived class (Child) can pass its arguments to the constructor for the base class (Parent).

The general form for a constructor of a derived class is this:

derived_class(argument list) : base_class(argument_sublist) { more stuff to do for the derived class;

}

When the derived class’s constructor is invoked, it first calls its parent’s constructor (passing none, some, or all of the arguments up to its parent’s constructor). Once the base class’s constructor completes its work, the code inside the curly braces executes to do anything extra that is required by the derived class.

Next we implement the value() method (line 20). This procedure returns the

quantity (x+y)*k. Although we have no access to x and y, the sum() method of Parent is public and so we can use that to calculate x+y. Because sum() is a public method of Parent, it is automatically a public method of Child.

The expression sum()*k invokes the sum() method (to calculate x+y) and we multiply that by k (which is accessible to Child).

Finally, we implement a print() method for the Child class (line 21). Recall that Parent already has a print() method, so this new print() method overrides the former. If P is an object of type Parent and C is an object of type Child then P.print() invokes the print() method defined on line 10 and C.print() invokes the one on line 21.

The new print() method writes the object to the computer screen in the

format (x,y)*k. This print() cannot access x or y. Of course, we could add public getX() and getY() methods to Parent. However, there is another solution.

The print() method in Parent does most of the work already. Instead of rewriting the part of the code that prints (x,y), we just need to use Parent’s print() method. The following code, however, does not work.

void print() const { print(); cout << "*" << k; }

The problem is that this code is recursive—it invokes itself. We want the second appearance of print to refer to Parent’s version. We accomplish that by prepending Parent:: to the name of the method. The correct code for Child’s print() is this:

void print() const { Parent::print(); cout << "*" << k; }

When Child’s print() executes, it first calls Parent’s version of print() (which sends (x,y) to the screen). The remaining step (sending *k) occurs when the second statement executes.

A simple main follows (lines 24–34); here is the output of the program.

184

C++ for Mathematicians

 

 

 

 

 

(3,-2) --> 1

 

 

(-1,3)*5 --> 2 --> 10

 

 

 

 

 

10.4 Protected class members

When we derive a new class from a base class, the public members of the base class are inherited as public members of the derived class, but the private members of the base class are inaccessible to the derived class. C++ provides a third alternative to this all-or-nothing access inheritance. In addition to public and private sections, a class may have a protected section. Both data and methods may be declared protected.

A protected member of a class becomes a private member of a derived class. A child class can access the public and protected parts of its parent, but not the private parts. A grandchild of the base class cannot access the protected parts of the base class. Let’s look at an example.

Program 10.2: A program to illustrate the use of protected members of a class.

1#include <iostream>

2using namespace std;

3

4class Base {

5private:

6int a;

7protected:

8int b;

9int sum() const { return a+b; }

10public:

11Base(int x, int y) { a=x; b=y; }

12void print() const { cout << "(" << a << "," << b << ")"; }

13};

14

15class Child : public Base {

16public:

17Child(int x, int y) : Base(x,y) { }

18void increase_b() { b++; }

19void print() const { Base::print(); cout << "=" << sum(); }

20};

21

22class GrandChild : public Child {

23private:

24int c;

25public:

26GrandChild(int x, int y, int z) : Child(x,y) { c = z; }

27void print() const { Base::print(); cout << "/" << c; }

28};

29

The Projective Plane

185

30

int main() {

 

31

Base

B(1,2);

32

Child

C(3,4);

33

GrandChild D(5,6,7);

34

 

 

35B.print(); cout << endl;

36// cout << B.sum() << endl; // Illegal, sum is protected

37

38C.print(); cout << " --> ";

39C.increase_b();

40C.print(); cout << endl;

41

42D.print(); cout << " --> ";

43D.increase_b();

44D.print(); cout << endl;

45

46return 0;

47}

In this program we define three classes: Base, Child, and GrandChild; each is used to derive the next.

The Base class has two data members: a private integer a and a protected integer b. The class also includes a protected method named sum, a public constructor, and a public method named print.

Class Child has no additional data members. It has a public method increase_b that increases the data member b by one. Note that it would not be possible for Child to have a similar method for increasing a. The constructor for Child passes its arguments on to its parent, Base, but then takes no further action (hence the curly braces on line 17 do not enclose any statements).

The print method for Child uses Base’s print method and sum method.

Class GrandChild adds an extra private data element, c. The constructor for GrandChild passes its first two arguments to its parent’s constructor and then uses the third argument to set the value of c.

The print method for GrandChild uses its grandparent’s print method. Although Base::print() invokes the sum method, the GrandChild methods cannot directly call sum because it is protected in Base, hence implicitly private in Child, and hence inaccessible in GrandChild.

A main to illustrate all these ideas begins on line 30. The output of the program follows.

 

 

(1,2)

(3,4)=7 --> (3,5)=8 (5,6)/7 --> (5,7)/7