Puncte, linii, segmente... Geometrie analitică în C++orea HaitonicCunoașterea câtorva relații ale geometriei analitice (în plan sau în spațiu) poate ușura mult rezolvarea unor probleme cu caracter practic sau teoretic. Propunem un set de tipuri, constante și funcții care să faciliteze folosirea lor în programare. Pentru implementarea seturilor de tipuri, constante și funcții folosite pentru rezolvarea unor probleme de geometrie analitică am ales limbajul C++ și programarea orientată obiect, avantajoase prin ușurința în declararea și utilizarea variabilelor. Vom folosi și facilitatea oferită de C++ care permite redefinirea operatorilor după modelul tip operator simbol_operator(listă_argumente) astfel încât lucrul cu primitivele geometrice să fie cât mai intuitiv. Fișierul antet geom.h conține declarațiile tipurilor definite (clasele PUNCT, DREAPTA și SEGMENT) precum și prototipurile funcțiilor operator. Fișierul geom.cpp conține implementarea efectivă a funcțiilor (atât membrii claselor cât și funcțiile operator) definite în fișierul antet. Se adaugă fișierul graf.cpp conținând implementarea reprezentărilor grafice ale punctelor, dreptelor și segmentelor, grupate separat pentru a permite modificarea lor facilă (ca exemplu, s-a ales varianta cea mai simplă: vom folosi funcțiile BGI pentru a desena pe ecran, suprafața de lucru fiind un pătrat cu vârfurile în (0, 0), (0, 399), (399, 399) și (399, 0); factorul de mărire este precizat printr-o constantă reală pozitivă). Fișierul header va fi inclus în sursă printr-o linie de genul #include "geom.h", iar celelalte două fișiere vor fi grupate într-un proiect împreună cu programul principal (în exemplul din final acesta se găsește în fișierul test.cpp). Fișierele sursă sunt disponibile pentru download pe site-ul Web al Gazetei de Informatică (www.ginfo.ro). Vom explica pe scurt construcția bibliotecii de tipuri și funcții, rememorând pe parcurs formulele analitice utilizate. Prima clasă definită este PUNCT, având ca membri de tip dată coordonatele reale ale punctului în planul cartezian iar ca membri de tip funcție doar constructorii: - PUNCT(float xx, float yy) - creează o variabilă de tip PUNCT având în câmpurile x, y valorile transmise ca argumente. Corpul funcției este extrem de simplu: tot ce avem de făcut este să inițializăm membrii de tip dată cu valorile xx, yy. - PUNCT(void) - un constructor implicit, fără parametri, este necesar pentru a putea folosi clasa PUNCT în definirea clasei SEGMENT (care conține doi membri de tip PUNCT). Lipsa acestui constructor conduce la apariția erorii Cannot find default constructor to initialize member... (a se vedea Help-ul C++ pentru mai multe amănunte). Corpul funcției este vid, constructorul implicit fiind necesar doar din punct de vedere sintactic pentru a putea declara variabile de tip PUNCT fără a le inițializa.
O a doua clasă este DREAPTA (definită matematic prin trei numere reale a, b și c, reprezentând coeficienții ecuației generale a dreptei: ax + by + c = 0; pentru a = 0 obținem o dreaptă orizontală y = const, iar pentru b = 0 obținem o dreaptă verticală x = const). Funcțiile membre sunt: - Constructorul DREAPTA(float aa, float bb, float cc) creează o variabilă de tip DREAPTA având în câmpurile a, b, c valorile transmise prin parametrii aa, bb, respectiv cc. Corpul funcției este analog celui de la constructorul clasei PUNCT. - Și aici avem un constructor implicit: DREAPTA(void). - Funcția float Panta(void) returnează panta dreptei date prin coeficienții ei a, b, c. Panta este dată de raportul -a / b (dreptele verticale - pentru care b = 0 - au panta ±„, în funcție de semnul lui a).
Următoarea clasă definită este SEGMENT (reprezentat prin două puncte - extremitățile segmentului - care reprezintă datele membre ale acestei clase). Funcțiile membre sunt: - Constructorul SEGMENT(PUNCT pct1, PUNCT pct2) care are rolul de a inițializa datele membre p1 și p2 ale clasei cu valorile transmise ca argumente (pentru a putea folosi aici clasa PUNCT avem nevoie de constructorul implicit PUNCT(void), după cum s-a discutat anterior). Corpul funcției constructor al clasei SEGMENT este asemănător cu cel al celorlalți constructori definiți până aici. - Și pentru această clasă definim un constructor implicit: SEGMENT(void). - Funcția Lungime(void) returnează un număr real reprezentând lungimea segmentului, folosind o formulă bine cunoscută a geometriei analitice: distanța d dintre două puncte A(x1, y1) și B(x2, y2) este:
- Funcția DREAPTA Suport(void) returnează dreapta suport a unui segment; în corpul acestei funcții folosim operatorul + (redefinit de noi după cum se va vedea în continuare), care aplicat asupra a două valori de tip PUNCT returnează dreapta (unică) care conține cele două puncte date.
Cu aceasta am finalizat prezentarea celor trei clase definite și implementate în biblioteca noastră dedicată geometriei analitice. Urmează să prezentăm operatorii redefiniți pentru variabilele din clasele PUNCT, DREAPTA și SEGMENT, operatori care oferă posibilitatea unei exprimări mai concise și mai clare (comparativ cu folosirea unor funcții "obișnuite" sau a unor macro-uri). Inițial vom defini egalitatea a două variabile de tip PUNCT, DREAPTA sau SEGMENT prin egalitatea membrilor de tip dată omologi (două puncte sunt considerate egale dacă au abscisele, respectiv ordonatele, egale; două segmente sunt egale dacă punctele care reprezintă extremitățile lor coincid; două drepte sunt egale când coeficienții lor sunt proporționali). Egalitatea a două astfel de primitive geometrice este simbolizată prin operatorul redefinit ==, care poate fi aplicat unor valori ce fac parte din aceeași clasă (PUNCT, DREAPTA, SEGMENT). Următorul operator redefinit este + care, aplicat asupra a două variabile de tip punct, returnează o variabilă de tip DREAPTA care reprezintă dreapta unică ce conține cele două puncte. Acest operator a fost folosit în funcția SEGMENT::Suport() (aplicat pentru extremitățile segmentului va returna dreapta suport a segmentului). Pentru a găsi coeficienții a, b, c vom folosi ecuația dreptei care trece prin două puncte P1(x1, y1) și P2(x2, y2): Urmează operatorul || care verifică paralelismul a două drepte folosind pantele lor (două drepte sunt paralele dacă au aceeași pantă). Funcția int operator||(DREAPTA d1,DREAPTA d2) returnează 1 dacă dreptele sunt paralele și 0 în caz contrar. Operatorul != verifică perpendicularitatea a două drepte conform regulii binecunoscute: dreptele sunt perpendiculare dacă produsul pantelor lor este -1. Sunt considerate perpendiculare și două drepte dintre care una este orizontală (panta este 0) și una este verticală (panta este ±„). Corpul funcției este foarte simplu și nu necesită explicații. Operatorul redefinit && verifică apartenența unui punct la o dreaptă, respectiv la un segment (în funcție de tipul operanzilor, C++ va decide care dintre variante trebuie folosită). Verificarea apartenenței la o dreaptă este simplă: este necesar și suficient ca abscisa și ordonata punctului să verifice ecuația dreptei. Pentru a stabili dacă un punct aparține unui segment, în afară de apartenența la dreapta suport a segmentului (ușor de găsit folosind funcția SEGMENT::Suport()) mai trebuie verificată situarea coordonatelor sale între coordonatele extremităților segmentului: dacă P este de coordonate (xp, yp) iar extremitățile segmentului sunt A(x1, y1) și B(x2, y2), este necesar și suficient ca xp să se afle între x1 și x2 și ca yp să se afle între y1 și y2. Cu alte cuvinte trebuie să avem x1 Ł xp Ł x2 sau x2 Ł xp Ł x1 și, respectiv, y1 Ł yp Ł y2 sau y2 Ł yp Ł y1. Pe scurt, aceste condiții pot fi descrise prin inegalitățile (xp - x1)(xp - x2) Ł 0 și (yp - y1)(yp - y2) Ł 0. A mai rămas de implementat operatorul * corespunzător intersecției de drepte și segmente (și acesta este implementat în mai multe variante pentru a permite determinarea intersecției dintre două drepte, dintre două segmente sau dintre o dreaptă și un segment). Funcția va returna punctul de intersecție căutat, sau punctul de coordonate (INFINIT, INFINIT) dacă cele două primitive nu se intersectează. În cazul intersecției a două drepte, găsirea coordonatelor punctului revine la a rezolva sistemul de ecuații: unde a1, b1, c1 respectiv a2, b2, c2 sunt coeficienții celor două drepte. Pentru rezolvarea lui vom folosi regula lui Kramer. Avem:
Sistemul este compatibil dacă d č 0, unde d = det A = a1b2 - a2b1. Se observă că propoziția d č 0 este echivalentă cu , adică cele două drepte nu sunt paralele. La început vom verifica existența punctului de intersecție și vom returna punctul de coordonate (INFINIT, INFINIT) dacă dreptele sunt paralele. După stabilirea compatibilității sistemului, rezolvarea decurge de la sine. Se impune o corecție pentru a evita apariția unor valori de genul -0.000000, valori pe care C++ le consideră negative. Celelalte variante ale operatorului redefinit * se implementează analog: pentru cazul intersecției dintre o dreaptă și un segment se determină întâi punctul de intersecție al dreptei respective cu dreapta suport a segmentului (se folosește în acest sens operatorul definit anterior), iar apoi se face verificarea apartenenței punctului la segment folosind operatorul &&. Cazul intersecției a două segmente se tratează asemănător, aici fiind necesară verificarea situării punctului pe ambele segmente. În cazul în care punctul de intersecție nu există este returnat punctul de coordonate (INFINIT, INFINIT)).
Cele trei clase folosite conțin și câte o funcție membru void Af(int c), al cărei rol este reprezentarea pe ecran a elementului respectiv (punct, segment, dreaptă). Implementarea acestor trei funcții este realizată în fișierul graf.cpp, inclus în proiect, într-o variantă foarte simplă: se folosesc funcțiile grafice BGI ale firmei Borland, în modul grafic VGAHi (rezoluția este 640Ž480, iar adâncimea culorii de 4 biți, deci vom avea 24 = 16 culori la dispoziție). Vom considera ca zonă de reprezentare grafică pătratul determinat de (0, 0) și (399, 399). Culoarea în care va fi desenat elementul este dată prin parametrul c. Factorul de scalare scala este declarat în fișierul graf.cpp ca variabilă globală externă pentru a putea fi modificat în programul principal în funcție de cerințe, la fel ca și coordonatele ferestrei de reprezentare (variabilele globale wx1, wy1, wx2, wy2). La reprezentarea grafică a primitivelor trebuie făcute câteva precizări: - Trebuie întotdeauna aplicată transformarea coordonate reale - coordonate ecran (relative la fereastră):
- Se vor folosi funcțiile C++ void getviewsettings(struct viewporttype *v) și void setviewport(int x1, int y1, int x2, int y2, int clip) pentru a limita zona de pe ecran în care se poate desena (și anume dreptunghiul (wx1, wy1, wx2, wy2)). - Variabilele globale wx1, wy1, wx2, wy2 trebuie declarate și în modulul ce conține programul principal folosind clauza extern pentru a putea fi folosite de funcțiile ce se găsesc în acest fișier.
Încheiem aici prezentarea acestei biblioteci, cu precizarea că ea poate fi extinsă în cel puțin două direcții: adăugarea de noi primitive geometrice (semiplan, cerc, elipsă, poligon etc.) și extinderea într-un spațiu cu trei (sau chiar mai multe dimensiuni).
Înfășurătoarea convexă Pentru exemplificare vom rezolva, folosind funcțiile și tipurile definite, o problemă clasică în care intervin noțiuni de geometrie analitică: se dau n puncte în planul real și se cere încadrarea lor într-un poligon de perimetru minim. Aceasta este cunoscută sub numele de problema determinării înfășurătorii convexe a unei mulțimi de puncte. Algoritmi pentru rezolvarea acestei probleme au fost prezentați în GInfo cu diverse ocazii. În continuare vom descrie un algoritm simplu și ușor de implementat, precizând că există mulți algoritmi mai performanți care rezolvă această problemă. Laturile poligonului vor fi segmentele pentru care toate punctele date sunt situate în același semiplan închis (n puncte se află în același semiplan determinat de dreapta d: ax + by + c = 0 dacă valorile axi + byi + c au același semn pentru i = 1..n). Soluția este memorată în vectorul Pol[] ale cărui elemente sunt de tip SEGMENT (am reprezentat un poligon ca o mulțime de segmente ce corespund laturilor). În final se face o afișare a punctelor și a poligonului determinat folosind factorul de scalare scala, calculat în funcție de punctele date astfel încât reprezentarea grafică să încapă în fereastra-ecran (wx1, wy1, wx2, wy2). Pentru fiecare segment trebuie să verificăm dacă cele n puncte se află în același semiplan față de dreapta suport a segmentului. Acest lucru poate fi verificat într-un timp O(n). Cele n puncte determină n(n - 1) / 2 segmente distincte, deci pentru a determina aceste segmente avem nevoie de un timp O(n2). Ca urmare, complexitatea algoritmului descris este O(n3).
Horea Haitonic este elev la Liceul "George Coșbuc" din Năsăud; poate fi contactat prin e-mail la adresa horea_h@hotmail.com [cuprins] |