Перейти до основного вмісту

Базова геометрія

У цій статті ми розглянемо базові операції над точками в евклідовому просторі, які лежать в основі всієї аналітичної геометрії. Для кожної точки r\mathbf r ми розглядатимемо вектор r\vec{\mathbf r}, спрямований із 0\mathbf 0 у r\mathbf r. Надалі ми не розрізнятимемо r\mathbf r і r\vec{\mathbf r} та вживатимемо термін точка як синонім до вектора.

Лінійні операції

Як двовимірні, так і тривимірні точки утворюють лінійний простір, а це означає, що для них визначені сума точок та множення точки на деяке число. Ось ці базові реалізації для 2D:

struct point2d {
ftype x, y;
point2d() {}
point2d(ftype x, ftype y): x(x), y(y) {}
point2d& operator+=(const point2d &t) {
x += t.x;
y += t.y;
return *this;
}
point2d& operator-=(const point2d &t) {
x -= t.x;
y -= t.y;
return *this;
}
point2d& operator*=(ftype t) {
x *= t;
y *= t;
return *this;
}
point2d& operator/=(ftype t) {
x /= t;
y /= t;
return *this;
}
point2d operator+(const point2d &t) const {
return point2d(*this) += t;
}
point2d operator-(const point2d &t) const {
return point2d(*this) -= t;
}
point2d operator*(ftype t) const {
return point2d(*this) *= t;
}
point2d operator/(ftype t) const {
return point2d(*this) /= t;
}
};
point2d operator*(ftype a, point2d b) {
return b * a;
}

А також для 3D точок:

struct point3d {
ftype x, y, z;
point3d() {}
point3d(ftype x, ftype y, ftype z): x(x), y(y), z(z) {}
point3d& operator+=(const point3d &t) {
x += t.x;
y += t.y;
z += t.z;
return *this;
}
point3d& operator-=(const point3d &t) {
x -= t.x;
y -= t.y;
z -= t.z;
return *this;
}
point3d& operator*=(ftype t) {
x *= t;
y *= t;
z *= t;
return *this;
}
point3d& operator/=(ftype t) {
x /= t;
y /= t;
z /= t;
return *this;
}
point3d operator+(const point3d &t) const {
return point3d(*this) += t;
}
point3d operator-(const point3d &t) const {
return point3d(*this) -= t;
}
point3d operator*(ftype t) const {
return point3d(*this) *= t;
}
point3d operator/(ftype t) const {
return point3d(*this) /= t;
}
};
point3d operator*(ftype a, point3d b) {
return b * a;
}

Тут ftype — це деякий тип, що використовується для координат, зазвичай int, double або long long.

Скалярний добуток

Означення

Скалярний добуток ab\mathbf a \cdot \mathbf b векторів a\mathbf a та b\mathbf b можна означити двома еквівалентними способами. Геометрично це добуток довжини першого вектора на довжину проєкції другого вектора на перший. Як видно із зображення нижче, ця проєкція — це не що інше, як acosθ|\mathbf a| \cos \theta, де θ\theta — кут між a\mathbf a та b\mathbf b. Отже, ab=acosθb\mathbf a\cdot \mathbf b = |\mathbf a| \cos \theta \cdot |\mathbf b|.

Скалярний добуток має кілька важливих властивостей:

  1. ab=ba\mathbf a \cdot \mathbf b = \mathbf b \cdot \mathbf a
  2. (αa)b=α(ab)(\alpha \cdot \mathbf a)\cdot \mathbf b = \alpha \cdot (\mathbf a \cdot \mathbf b)
  3. (a+b)c=ac+bc(\mathbf a + \mathbf b)\cdot \mathbf c = \mathbf a \cdot \mathbf c + \mathbf b \cdot \mathbf c

Тобто це комутативна функція, лінійна за обома аргументами. Позначимо одиничні вектори як

ex=(100),ey=(010),ez=(001).\mathbf e_x = \begin{pmatrix} 1 \\ 0 \\ 0 \end{pmatrix}, \mathbf e_y = \begin{pmatrix} 0 \\ 1 \\ 0 \end{pmatrix}, \mathbf e_z = \begin{pmatrix} 0 \\ 0 \\ 1 \end{pmatrix}.

З цим позначенням ми можемо записати вектор r=(x;y;z)\mathbf r = (x;y;z) як r=xex+yey+zezr = x \cdot \mathbf e_x + y \cdot \mathbf e_y + z \cdot \mathbf e_z. А оскільки для одиничних векторів

exex=eyey=ezez=1,exey=eyez=ezex=0\mathbf e_x\cdot \mathbf e_x = \mathbf e_y\cdot \mathbf e_y = \mathbf e_z\cdot \mathbf e_z = 1,\\ \mathbf e_x\cdot \mathbf e_y = \mathbf e_y\cdot \mathbf e_z = \mathbf e_z\cdot \mathbf e_x = 0

ми бачимо, що в координатах для a=(x1;y1;z1)\mathbf a = (x_1;y_1;z_1) та b=(x2;y2;z2)\mathbf b = (x_2;y_2;z_2) виконується

ab=(x1ex+y1ey+z1ez)(x2ex+y2ey+z2ez)=x1x2+y1y2+z1z2\mathbf a\cdot \mathbf b = (x_1 \cdot \mathbf e_x + y_1 \cdot\mathbf e_y + z_1 \cdot\mathbf e_z)\cdot( x_2 \cdot\mathbf e_x + y_2 \cdot\mathbf e_y + z_2 \cdot\mathbf e_z) = x_1 x_2 + y_1 y_2 + z_1 z_2

Це також є алгебраїчним означенням скалярного добутку. Звідси ми можемо записати функції, які його обчислюють.

ftype dot(point2d a, point2d b) {
return a.x * b.x + a.y * b.y;
}
ftype dot(point3d a, point3d b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}

Розв'язуючи задачі, для обчислення скалярних добутків варто користуватися алгебраїчним означенням, але тримати в голові геометричне означення та властивості, щоб ним користуватися.

Властивості

За допомогою скалярного добутку ми можемо визначити багато геометричних властивостей. Наприклад

  1. Норма a\mathbf a (квадрат довжини): a2=aa|\mathbf a|^2 = \mathbf a\cdot \mathbf a
  2. Довжина a\mathbf a: a=aa|\mathbf a| = \sqrt{\mathbf a\cdot \mathbf a}
  3. Проєкція a\mathbf a на b\mathbf b: abb\dfrac{\mathbf a\cdot\mathbf b}{|\mathbf b|}
  4. Кут між векторами: arccos(abab)\arccos \left(\dfrac{\mathbf a\cdot \mathbf b}{|\mathbf a| \cdot |\mathbf b|}\right)
  5. З попереднього пункту видно, що скалярний добуток додатний, якщо кут між векторами гострий, від'ємний, якщо кут тупий, і дорівнює нулю, якщо вектори ортогональні, тобто утворюють прямий кут.

Зауважимо, що всі ці функції не залежать від кількості вимірів, а отже, вони будуть однаковими для випадків 2D та 3D:

ftype norm(point2d a) {
return dot(a, a);
}
double abs(point2d a) {
return sqrt(norm(a));
}
double proj(point2d a, point2d b) {
return dot(a, b) / abs(b);
}
double angle(point2d a, point2d b) {
return acos(dot(a, b) / abs(a) / abs(b));
}

Щоб побачити наступну важливу властивість, розгляньмо множину точок r\mathbf r, для яких ra=C\mathbf r\cdot \mathbf a = C для деякої фіксованої сталої CC. Можна побачити, що ця множина точок — це саме множина точок, для яких проєкція на a\mathbf a є точкою Caa2C \cdot \dfrac{\mathbf a}{|\mathbf a| ^ 2}, і вони утворюють гіперплощину, ортогональну до a\mathbf a. На зображенні нижче ви можете побачити вектор a\mathbf a разом із кількома такими векторами, що мають із ним однаковий скалярний добуток у 2D:

Vectors having same dot product with a

У 2D ці вектори утворять пряму, у 3D — площину. Зауважимо, що цей результат дозволяє нам означити пряму в 2D як rn=C\mathbf r\cdot \mathbf n=C або (rr0)n=0(\mathbf r - \mathbf r_0)\cdot \mathbf n=0, де n\mathbf n — вектор, ортогональний до прямої, r0\mathbf r_0 — будь-який вектор, що вже лежить на прямій, а C=r0nC = \mathbf r_0\cdot \mathbf n. Так само можна означити й площину в 3D.

Векторний добуток

Означення

Припустимо, у вас є три вектори a\mathbf a, b\mathbf b та c\mathbf c у тривимірному просторі, об'єднані в паралелепіпед, як на зображенні нижче:

Three vectors

Як би ви обчислили його об'єм? Зі школи ми знаємо, що треба помножити площу основи на висоту, якою є проєкція a\mathbf a на напрямок, ортогональний до основи. Це означає, що якщо ми означимо b×c\mathbf b \times \mathbf c як вектор, ортогональний одночасно до b\mathbf b та c\mathbf c, довжина якого дорівнює площі паралелограма, утвореного b\mathbf b та c\mathbf c, то a(b×c)|\mathbf a\cdot (\mathbf b\times\mathbf c)| дорівнюватиме об'єму паралелепіпеда. Для цілісності будемо вважати, що b×c\mathbf b\times \mathbf c завжди спрямований так, щоб поворот від вектора b\mathbf b до вектора c\mathbf c, якщо дивитися з боку b×c\mathbf b\times \mathbf c, завжди був проти годинникової стрілки (див. зображення нижче).

cross product

Це означує векторний добуток b×c\mathbf b\times \mathbf c векторів b\mathbf b та c\mathbf c, а також мішаний добуток a(b×c)\mathbf a\cdot(\mathbf b\times \mathbf c) векторів a\mathbf a, b\mathbf b та c\mathbf c.

Деякі важливі властивості векторного та мішаного добутків:

  1. a×b=b×a\mathbf a\times \mathbf b = -\mathbf b\times \mathbf a

  2. (αa)×b=α(a×b)(\alpha \cdot \mathbf a)\times \mathbf b = \alpha \cdot (\mathbf a\times \mathbf b)

  3. Для будь-яких b\mathbf b та c\mathbf c існує рівно один вектор r\mathbf r такий, що a(b×c)=ar\mathbf a\cdot (\mathbf b\times \mathbf c) = \mathbf a\cdot\mathbf r для будь-якого вектора a\mathbf a.
    Справді, якщо існують два таких вектори r1\mathbf r_1 та r2\mathbf r_2, то a(r1r2)=0\mathbf a\cdot (\mathbf r_1 - \mathbf r_2)=0 для всіх векторів a\mathbf a, що можливо лише тоді, коли r1=r2\mathbf r_1 = \mathbf r_2.

  4. a(b×c)=b(c×a)=a(c×b)\mathbf a\cdot (\mathbf b\times \mathbf c) = \mathbf b\cdot (\mathbf c\times \mathbf a) = -\mathbf a\cdot( \mathbf c\times \mathbf b)

  5. (a+b)×c=a×c+b×c(\mathbf a + \mathbf b)\times \mathbf c = \mathbf a\times \mathbf c + \mathbf b\times \mathbf c. Справді, для всіх векторів r\mathbf r виконується ланцюжок рівностей:

    r((a+b)×c)=(a+b)(c×r)=a(c×r)+b(c×r)=r(a×c)+r(b×c)=r(a×c+b×c)\mathbf r\cdot( (\mathbf a + \mathbf b)\times \mathbf c) = (\mathbf a + \mathbf b) \cdot (\mathbf c\times \mathbf r) = \mathbf a \cdot(\mathbf c\times \mathbf r) + \mathbf b\cdot(\mathbf c\times \mathbf r) = \mathbf r\cdot (\mathbf a\times \mathbf c) + \mathbf r\cdot(\mathbf b\times \mathbf c) = \mathbf r\cdot(\mathbf a\times \mathbf c + \mathbf b\times \mathbf c)

    Що доводить (a+b)×c=a×c+b×c(\mathbf a + \mathbf b)\times \mathbf c = \mathbf a\times \mathbf c + \mathbf b\times \mathbf c завдяки пункту 3.

  6. a×b=absinθ|\mathbf a\times \mathbf b|=|\mathbf a| \cdot |\mathbf b| \sin \theta, де θ\theta — кут між a\mathbf a та b\mathbf b, оскільки a×b|\mathbf a\times \mathbf b| дорівнює площі паралелограма, утвореного a\mathbf a та b\mathbf b.

З урахуванням усього цього та того, що для одиничних векторів виконується рівність

ex×ex=ey×ey=ez×ez=0,ex×ey=ez, ey×ez=ex, ez×ex=ey\mathbf e_x\times \mathbf e_x = \mathbf e_y\times \mathbf e_y = \mathbf e_z\times \mathbf e_z = \mathbf 0,\\ \mathbf e_x\times \mathbf e_y = \mathbf e_z,~\mathbf e_y\times \mathbf e_z = \mathbf e_x,~\mathbf e_z\times \mathbf e_x = \mathbf e_y

ми можемо обчислити векторний добуток a=(x1;y1;z1)\mathbf a = (x_1;y_1;z_1) та b=(x2;y2;z2)\mathbf b = (x_2;y_2;z_2) у координатній формі:

a×b=(x1ex+y1ey+z1ez)×(x2ex+y2ey+z2ez)=\mathbf a\times \mathbf b = (x_1 \cdot \mathbf e_x + y_1 \cdot \mathbf e_y + z_1 \cdot \mathbf e_z)\times (x_2 \cdot \mathbf e_x + y_2 \cdot \mathbf e_y + z_2 \cdot \mathbf e_z) = (y1z2z1y2)ex+(z1x2x1z2)ey+(x1y2y1x2)ez(y_1 z_2 - z_1 y_2)\mathbf e_x + (z_1 x_2 - x_1 z_2)\mathbf e_y + (x_1 y_2 - y_1 x_2)\mathbf e_z

Що також можна записати в елегантнішій формі:

a×b=exeyezx1y1z1x2y2z2, a(b×c)=x1y1z1x2y2z2x3y3z3\mathbf a\times \mathbf b = \begin{vmatrix}\mathbf e_x & \mathbf e_y & \mathbf e_z \\ x_1 & y_1 & z_1 \\ x_2 & y_2 & z_2 \end{vmatrix},~a\cdot(b\times c) = \begin{vmatrix} x_1 & y_1 & z_1 \\ x_2 & y_2 & z_2 \\ x_3 & y_3 & z_3 \end{vmatrix}

Тут | \cdot | позначає визначник матриці.

Деякий аналог векторного добутку (а саме псевдоскалярний добуток) можна реалізувати й у двовимірному випадку. Якщо ми хочемо обчислити площу паралелограма, утвореного векторами a\mathbf a та b\mathbf b, ми обчислили б ez(a×b)=x1y2y1x2|\mathbf e_z\cdot(\mathbf a\times \mathbf b)| = |x_1 y_2 - y_1 x_2|. Інший спосіб отримати той самий результат — помножити a|\mathbf a| (основу паралелограма) на висоту, якою є проєкція вектора b\mathbf b на вектор a\mathbf a, повернутий на 9090^\circ, що, своєю чергою, дорівнює a^=(y1;x1)\widehat{\mathbf a}=(-y_1;x_1). Тобто обчислити a^b=x1y2y1x2|\widehat{\mathbf a}\cdot\mathbf b|=|x_1y_2 - y_1 x_2|.

Якщо враховувати знак, то площа буде додатною, якщо поворот від a\mathbf a до b\mathbf b (тобто з боку точки ez\mathbf e_z) відбувається проти годинникової стрілки, і від'ємною інакше. Це означує псевдоскалярний добуток. Зауважимо, що він також дорівнює absinθ|\mathbf a| \cdot |\mathbf b| \sin \theta, де θ\theta — кут від a\mathbf a до b\mathbf b, відлічуваний проти годинникової стрілки (і від'ємний, якщо поворот за годинниковою стрілкою).

Реалізуймо все це!

point3d cross(point3d a, point3d b) {
return point3d(a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x);
}
ftype triple(point3d a, point3d b, point3d c) {
return dot(a, cross(b, c));
}
ftype cross(point2d a, point2d b) {
return a.x * b.y - a.y * b.x;
}

Властивості

Щодо векторного добутку, то він дорівнює нульовому вектору тоді й лише тоді, коли вектори a\mathbf a та b\mathbf b колінеарні (лежать на одній прямій, тобто паралельні). Те саме виконується для мішаного добутку: він дорівнює нулю тоді й лише тоді, коли вектори a\mathbf a, b\mathbf b та c\mathbf c компланарні (лежать в одній площині).

Звідси ми можемо отримати універсальні рівняння, що задають прямі та площини. Пряму можна задати через її напрямний вектор d\mathbf d та початкову точку r0\mathbf r_0 або через дві точки a\mathbf a та b\mathbf b. Вона задається як (rr0)×d=0(\mathbf r - \mathbf r_0)\times\mathbf d=0 або як (ra)×(ba)=0(\mathbf r - \mathbf a)\times (\mathbf b - \mathbf a) = 0. Щодо площин, то площину можна задати трьома точками a\mathbf a, b\mathbf b та c\mathbf c як (ra)((ba)×(ca))=0(\mathbf r - \mathbf a)\cdot((\mathbf b - \mathbf a)\times (\mathbf c - \mathbf a))=0 або початковою точкою r0\mathbf r_0 та двома напрямними векторами, що лежать у цій площині, d1\mathbf d_1 та d2\mathbf d_2: (rr0)(d1×d2)=0(\mathbf r - \mathbf r_0)\cdot(\mathbf d_1\times \mathbf d_2)=0.

У 2D псевдоскалярний добуток також можна використати, щоб перевірити взаємну орієнтацію двох векторів, бо він додатний, якщо поворот від першого до другого вектора відбувається проти годинникової стрілки, і від'ємний інакше. І, звісно, його можна використовувати для обчислення площ многокутників, що описано в окремій статті. Мішаний добуток можна використати з тією самою метою в тривимірному просторі.

Вправи

Перетин прямих

Існує багато способів задати пряму в 2D, і не варто вагатися їх комбінувати. Наприклад, у нас є дві прямі, і ми хочемо знайти точки їхнього перетину. Можна сказати, що всі точки першої прямої можна параметризувати як r=a1+td1\mathbf r = \mathbf a_1 + t \cdot \mathbf d_1, де a1\mathbf a_1 — початкова точка, d1\mathbf d_1 — напрямок, а tt — деякий дійсний параметр. Щодо другої прямої, то всі її точки мають задовольняти (ra2)×d2=0(\mathbf r - \mathbf a_2)\times \mathbf d_2=0. Звідси ми легко знаходимо параметр tt:

(a1+td1a2)×d2=0t=(a2a1)×d2d1×d2(\mathbf a_1 + t \cdot \mathbf d_1 - \mathbf a_2)\times \mathbf d_2=0 \quad\Rightarrow\quad t = \dfrac{(\mathbf a_2 - \mathbf a_1)\times\mathbf d_2}{\mathbf d_1\times \mathbf d_2}

Реалізуймо функцію перетину двох прямих.

point2d intersect(point2d a1, point2d d1, point2d a2, point2d d2) {
return a1 + cross(a2 - a1, d2) / cross(d1, d2) * d1;
}

Перетин площин

Однак іноді скористатися геометричними міркуваннями буває важко. Наприклад, вам задано три площини через початкові точки ai\mathbf a_i та напрямки di\mathbf d_i, і ви хочете знайти точку їхнього перетину. Можна помітити, що достатньо просто розв'язати систему рівнянь:

{rn1=a1n1,rn2=a2n2,rn3=a3n3\begin{cases}\mathbf r\cdot \mathbf n_1 = \mathbf a_1\cdot \mathbf n_1, \\ \mathbf r\cdot \mathbf n_2 = \mathbf a_2\cdot \mathbf n_2, \\ \mathbf r\cdot \mathbf n_3 = \mathbf a_3\cdot \mathbf n_3\end{cases}

Замість того щоб думати над геометричним підходом, можна виробити алгебраїчний, який отримуємо одразу. Наприклад, якщо ви вже реалізували клас точки, вам буде легко розв'язати цю систему за допомогою правила Крамера, бо мішаний добуток — це просто визначник матриці, отриманої з векторів, що є її стовпцями:

point3d intersect(point3d a1, point3d n1, point3d a2, point3d n2, point3d a3, point3d n3) {
point3d x(n1.x, n2.x, n3.x);
point3d y(n1.y, n2.y, n3.y);
point3d z(n1.z, n2.z, n3.z);
point3d d(dot(a1, n1), dot(a2, n2), dot(a3, n3));
return point3d(triple(d, y, z),
triple(x, d, z),
triple(x, y, d)) / triple(n1, n2, n3);
}

Тепер ви можете спробувати самостійно знайти підходи до поширених геометричних операцій, щоб звикнути до всього цього.

Відеоматеріали