Предложите и обоснуйте алгоритм проверки, является ли произвольный многоугольник на плоскости ортогональным (все углы 90°), используя координатные методы и методы проверки на целые повороты/дикреты; обсудите погрешности численных реализаций
Коротко: под «ортогональным многоугольником» обычно понимают многоугольник с поворотами, кратными 90∘90^\circ90∘ (см. замечание о строго «все углы 90∘90^\circ90∘» ниже). Основная координатная идея — проверять перпендикулярность соседних рёбер через скалярное произведение и/или проверять, что каждый вектор ребра получается из предыдущего поворотом на ±90∘\pm90^\circ±90∘ (дискретные повороты). Далее — алгоритмы, обоснование и рекомендации по погрешностям. Определение/замечание - Если требовать, чтобы все внутренние углы строго равнялись 90∘90^\circ90∘, то из формулы суммы углов 180(n−2)=90n180(n-2)=90n180(n−2)=90n следует n=4n=4n=4 — т.е. единственные такие простые многоугольники — прямоугольники. В большинстве практических задач под «ортогональным» подразумевают, что каждый поворот между соседними рёбрами равен ±90∘\pm90^\circ±90∘ (т.е. рёбра имеют только две взаимно перпендикулярные направления); далее я использую это понятие. Вход: упорядоченный замкнутый список вершин p1,…,pnp_1,\dots,p_np1,…,pn (считаем pn+1=p1p_{n+1}=p_1pn+1=p1). Вектора рёбер vi=pi+1−pi\,v_i=p_{i+1}-p_ivi=pi+1−pi. Алгоритм A — простая координатная проверка (скалярные произведения) 1. Предобработка: удалить повторяющиеся подряд вершины; если после этого какие‑то vi=(0,0)v_i=(0,0)vi=(0,0) — либо удалить, либо считать некорректным вводом. 2. Для каждого i вычислить вектор viv_ivi и длины ∥vi∥\|v_i\|∥vi∥. 3. Для каждого i проверить перпендикулярность соседних рёбер: проверить условие ∣vi⋅vi+1∣≤ε ∥vi∥ ∥vi+1∥
|v_i\cdot v_{i+1}|\le\varepsilon\,\|v_i\|\,\|v_{i+1}\| ∣vi⋅vi+1∣≤ε∥vi∥∥vi+1∥
где vi⋅vi+1=xixi+1+yiyi+1v_i\cdot v_{i+1}=x_i x_{i+1}+y_i y_{i+1}vi⋅vi+1=xixi+1+yiyi+1. 4. Дополнительно можно проверить замкнутость ∥∑i=1nvi∥≤εpos
\Big\|\sum_{i=1}^n v_i\Big\|\le\varepsilon_{\text{pos}} i=1∑nvi≤εpos
(обычно натурально выполняется, если вход корректный). 5. Если все проверки пройдены — многоугольник ортогонален (в смысле поворотов ±90∘\pm90^\circ±90∘). Обоснование: для двух векторов перпендикулярность эквивалентна vi⋅vi+1=0v_i\cdot v_{i+1}=0vi⋅vi+1=0. Т.к. последовательность перпендикулярностей гарантирует, что направления ребёр получаются поворотом на ±90∘\pm90^\circ±90∘ от предыдущего, все рёбра лежат в двух взаимно перпендикулярных направлениях. Алгоритм B — проверка целых поворотов (дискретная проверка поворота на ±90∘\pm90^\circ±90∘) 1. Как выше найти viv_ivi. 2. Для каждого i можно вычислить ориентированный «поворот‑коэффициент» через скаляр и псевдоскаляр (в 2D псевдоскаляр = детерминант): ai=vi⋅vi+1,bi=vi×vi+1=xiyi+1−yixi+1.
a_i = v_i\cdot v_{i+1},\qquad b_i = v_i\times v_{i+1}=x_i y_{i+1}-y_i x_{i+1}. ai=vi⋅vi+1,bi=vi×vi+1=xiyi+1−yixi+1.
3. Нормализовать: положим ri=ai+ibi∥vi∥ ∥vi+1∥.
r_i = \frac{a_i + i b_i}{\|v_i\|\,\|v_{i+1}\|}. ri=∥vi∥∥vi+1∥ai+ibi.
В точном случае перпендикулярного поворота ri∈{i,−i}r_i\in\{i,-i\}ri∈{i,−i}. Проверяем дистанцию до этих значений: min(∣ℜri∣, ∣ ∣ℑri∣−1 ∣)≤ε.
\min\big(|\Re r_i|,\,|\;|\Im r_i|-1\;|\big)\le\varepsilon. min(∣ℜri∣,∣∣ℑri∣−1∣)≤ε.
или проще: требуем ∣ai∣≤ε∥vi∥∥vi+1∥|a_i|\le\varepsilon\|v_i\|\|v_{i+1}\|∣ai∣≤ε∥vi∥∥vi+1∥ и ∣bi∣|b_i|∣bi∣ не чрезмерно мал (чтобы исключить вырожденность). 4. Также можно фиксировать знак поворота по знаку bib_ibi (лeво/право). Преимущество этого подхода — явная дискретизация поворота; удобно для последующей логики (счёт лево/право, связность). Специальный случай при целых координатах - Если вершины имеют целые координаты, то все выражения vi⋅vi+1v_i\cdot v_{i+1}vi⋅vi+1 и vi×vi+1v_i\times v_{i+1}vi×vi+1 целы; перпендикулярность проверяется точно условием vi⋅vi+1=0.
v_i\cdot v_{i+1}=0. vi⋅vi+1=0.
Это устраняет проблемы с округлением — используйте целочисленные 64‑/128‑бит операции или bigint при необходимости. Обработка вырожденных/нежелательных случаев - Последовательные коллинеарные рёбра дают внутренний угол 180∘180^\circ180∘ — их надо либо считать некорректными, либо соединять в одно ребро. - Если нужно, чтобы все внутренние углы были ровно 90∘90^\circ90∘ (а не 270∘270^\circ270∘), дополнительно проверяйте ориентацию внутреннего угла: для простого многоугольника вычислите ориентацию полигона (сумма поворотов) и используйте знак детерминанта, чтобы отличать выпуклые/вогнутые углы. Погрешности численных реализаций — рекомендации - Использовать относительную нормированную погрешность. Вместо ∣vi⋅vi+1∣≤δ|v_i\cdot v_{i+1}|\le\delta∣vi⋅vi+1∣≤δ применять ∣vi⋅vi+1∣≤ε ∥vi∥ ∥vi+1∥.
|v_i\cdot v_{i+1}|\le\varepsilon\,\|v_i\|\,\|v_{i+1}\|. ∣vi⋅vi+1∣≤ε∥vi∥∥vi+1∥.
Для double обычно берут ε\varepsilonε в диапазоне от 10−1210^{-12}10−12 до 10−810^{-8}10−8 в зависимости от расстояний/масштаба данных; можно выбрать ε=C⋅epsmach⋅S2,
\varepsilon = C\cdot \text{eps}_{\text{mach}}\cdot S^2, ε=C⋅epsmach⋅S2,
где S=maxi∥vi∥S=\max_i\|v_i\|S=maxi∥vi∥ и CCC — небольшая константа (например 10210^2102). - Лучше нормализовать: проверять ∣cosθ∣≤ε|\cos\theta|\le\varepsilon∣cosθ∣≤ε с cosθ=vi⋅vi+1∥vi∥ ∥vi+1∥\cos\theta=\dfrac{v_i\cdot v_{i+1}}{\|v_i\|\,\|v_{i+1}\|}cosθ=∥vi∥∥vi+1∥vi⋅vi+1. - Если вход результат цифровой обработки (сканирование, округление), целесообразно сначала «привести к сетке»: поворачивать/сдвигать/округлять углы к ближайшим кратным 90∘90^\circ90∘ при условии, что отклонение мало. - Для точного результата при рациональных/целых координатах — использовать точную арифметику (целочисленные или рациональные, bigint), или использовать одностороннюю проверку через модульные/целочисленные операции. - При работе с очень «плохими» данными (малые длины рёбер, близкие к 0) удалить или объединить мелкие ребра, т.к. относительная погрешность там растёт. Сложность - О(n) по времени и памяти: один проход по рёбрам вычисляет нужные скалярные и псевдоскалярные произведения. Краткая сводка (практическая реализация) - Удалить дубликаты и нулевые вектора. - Для i=1..n вычислить viv_ivi, проверить ∥vi∥>0\|v_i\|>0∥vi∥>0. - Проверить для всех i условие перпендикулярности ∣vi⋅vi+1∣≤ε ∥vi∥ ∥vi+1∥.
|v_i\cdot v_{i+1}|\le\varepsilon\,\|v_i\|\,\|v_{i+1}\|. ∣vi⋅vi+1∣≤ε∥vi∥∥vi+1∥.
- При целых входных координатах — проверка точная: vi⋅vi+1=0v_i\cdot v_{i+1}=0vi⋅vi+1=0. - При обнаружении нарушения — не ортогонален; иначе — ортогонален (в смысле поворотов ±90∘\pm90^\circ±90∘). Если нужно, могу прислать компактный пример кода (C++/Python) с указаниями выбора ε\varepsilonε.
Определение/замечание
- Если требовать, чтобы все внутренние углы строго равнялись 90∘90^\circ90∘, то из формулы суммы углов 180(n−2)=90n180(n-2)=90n180(n−2)=90n следует n=4n=4n=4 — т.е. единственные такие простые многоугольники — прямоугольники. В большинстве практических задач под «ортогональным» подразумевают, что каждый поворот между соседними рёбрами равен ±90∘\pm90^\circ±90∘ (т.е. рёбра имеют только две взаимно перпендикулярные направления); далее я использую это понятие.
Вход: упорядоченный замкнутый список вершин p1,…,pnp_1,\dots,p_np1 ,…,pn (считаем pn+1=p1p_{n+1}=p_1pn+1 =p1 ). Вектора рёбер vi=pi+1−pi\,v_i=p_{i+1}-p_ivi =pi+1 −pi .
Алгоритм A — простая координатная проверка (скалярные произведения)
1. Предобработка: удалить повторяющиеся подряд вершины; если после этого какие‑то vi=(0,0)v_i=(0,0)vi =(0,0) — либо удалить, либо считать некорректным вводом.
2. Для каждого i вычислить вектор viv_ivi и длины ∥vi∥\|v_i\|∥vi ∥.
3. Для каждого i проверить перпендикулярность соседних рёбер:
проверить условие
∣vi⋅vi+1∣≤ε ∥vi∥ ∥vi+1∥ |v_i\cdot v_{i+1}|\le\varepsilon\,\|v_i\|\,\|v_{i+1}\|
∣vi ⋅vi+1 ∣≤ε∥vi ∥∥vi+1 ∥ где vi⋅vi+1=xixi+1+yiyi+1v_i\cdot v_{i+1}=x_i x_{i+1}+y_i y_{i+1}vi ⋅vi+1 =xi xi+1 +yi yi+1 .
4. Дополнительно можно проверить замкнутость
∥∑i=1nvi∥≤εpos \Big\|\sum_{i=1}^n v_i\Big\|\le\varepsilon_{\text{pos}}
i=1∑n vi ≤εpos (обычно натурально выполняется, если вход корректный).
5. Если все проверки пройдены — многоугольник ортогонален (в смысле поворотов ±90∘\pm90^\circ±90∘).
Обоснование: для двух векторов перпендикулярность эквивалентна vi⋅vi+1=0v_i\cdot v_{i+1}=0vi ⋅vi+1 =0. Т.к. последовательность перпендикулярностей гарантирует, что направления ребёр получаются поворотом на ±90∘\pm90^\circ±90∘ от предыдущего, все рёбра лежат в двух взаимно перпендикулярных направлениях.
Алгоритм B — проверка целых поворотов (дискретная проверка поворота на ±90∘\pm90^\circ±90∘)
1. Как выше найти viv_ivi .
2. Для каждого i можно вычислить ориентированный «поворот‑коэффициент» через скаляр и псевдоскаляр (в 2D псевдоскаляр = детерминант):
ai=vi⋅vi+1,bi=vi×vi+1=xiyi+1−yixi+1. a_i = v_i\cdot v_{i+1},\qquad b_i = v_i\times v_{i+1}=x_i y_{i+1}-y_i x_{i+1}.
ai =vi ⋅vi+1 ,bi =vi ×vi+1 =xi yi+1 −yi xi+1 . 3. Нормализовать: положим
ri=ai+ibi∥vi∥ ∥vi+1∥. r_i = \frac{a_i + i b_i}{\|v_i\|\,\|v_{i+1}\|}.
ri =∥vi ∥∥vi+1 ∥ai +ibi . В точном случае перпендикулярного поворота ri∈{i,−i}r_i\in\{i,-i\}ri ∈{i,−i}. Проверяем дистанцию до этих значений:
min(∣ℜri∣, ∣ ∣ℑri∣−1 ∣)≤ε. \min\big(|\Re r_i|,\,|\;|\Im r_i|-1\;|\big)\le\varepsilon.
min(∣ℜri ∣,∣∣ℑri ∣−1∣)≤ε. или проще: требуем ∣ai∣≤ε∥vi∥∥vi+1∥|a_i|\le\varepsilon\|v_i\|\|v_{i+1}\|∣ai ∣≤ε∥vi ∥∥vi+1 ∥ и ∣bi∣|b_i|∣bi ∣ не чрезмерно мал (чтобы исключить вырожденность).
4. Также можно фиксировать знак поворота по знаку bib_ibi (лeво/право).
Преимущество этого подхода — явная дискретизация поворота; удобно для последующей логики (счёт лево/право, связность).
Специальный случай при целых координатах
- Если вершины имеют целые координаты, то все выражения vi⋅vi+1v_i\cdot v_{i+1}vi ⋅vi+1 и vi×vi+1v_i\times v_{i+1}vi ×vi+1 целы; перпендикулярность проверяется точно условием
vi⋅vi+1=0. v_i\cdot v_{i+1}=0.
vi ⋅vi+1 =0. Это устраняет проблемы с округлением — используйте целочисленные 64‑/128‑бит операции или bigint при необходимости.
Обработка вырожденных/нежелательных случаев
- Последовательные коллинеарные рёбра дают внутренний угол 180∘180^\circ180∘ — их надо либо считать некорректными, либо соединять в одно ребро.
- Если нужно, чтобы все внутренние углы были ровно 90∘90^\circ90∘ (а не 270∘270^\circ270∘), дополнительно проверяйте ориентацию внутреннего угла: для простого многоугольника вычислите ориентацию полигона (сумма поворотов) и используйте знак детерминанта, чтобы отличать выпуклые/вогнутые углы.
Погрешности численных реализаций — рекомендации
- Использовать относительную нормированную погрешность. Вместо ∣vi⋅vi+1∣≤δ|v_i\cdot v_{i+1}|\le\delta∣vi ⋅vi+1 ∣≤δ применять
∣vi⋅vi+1∣≤ε ∥vi∥ ∥vi+1∥. |v_i\cdot v_{i+1}|\le\varepsilon\,\|v_i\|\,\|v_{i+1}\|.
∣vi ⋅vi+1 ∣≤ε∥vi ∥∥vi+1 ∥. Для double обычно берут ε\varepsilonε в диапазоне от 10−1210^{-12}10−12 до 10−810^{-8}10−8 в зависимости от расстояний/масштаба данных; можно выбрать
ε=C⋅epsmach⋅S2, \varepsilon = C\cdot \text{eps}_{\text{mach}}\cdot S^2,
ε=C⋅epsmach ⋅S2, где S=maxi∥vi∥S=\max_i\|v_i\|S=maxi ∥vi ∥ и CCC — небольшая константа (например 10210^2102).
- Лучше нормализовать: проверять ∣cosθ∣≤ε|\cos\theta|\le\varepsilon∣cosθ∣≤ε с cosθ=vi⋅vi+1∥vi∥ ∥vi+1∥\cos\theta=\dfrac{v_i\cdot v_{i+1}}{\|v_i\|\,\|v_{i+1}\|}cosθ=∥vi ∥∥vi+1 ∥vi ⋅vi+1 .
- Если вход результат цифровой обработки (сканирование, округление), целесообразно сначала «привести к сетке»: поворачивать/сдвигать/округлять углы к ближайшим кратным 90∘90^\circ90∘ при условии, что отклонение мало.
- Для точного результата при рациональных/целых координатах — использовать точную арифметику (целочисленные или рациональные, bigint), или использовать одностороннюю проверку через модульные/целочисленные операции.
- При работе с очень «плохими» данными (малые длины рёбер, близкие к 0) удалить или объединить мелкие ребра, т.к. относительная погрешность там растёт.
Сложность
- О(n) по времени и памяти: один проход по рёбрам вычисляет нужные скалярные и псевдоскалярные произведения.
Краткая сводка (практическая реализация)
- Удалить дубликаты и нулевые вектора.
- Для i=1..n вычислить viv_ivi , проверить ∥vi∥>0\|v_i\|>0∥vi ∥>0.
- Проверить для всех i условие перпендикулярности
∣vi⋅vi+1∣≤ε ∥vi∥ ∥vi+1∥. |v_i\cdot v_{i+1}|\le\varepsilon\,\|v_i\|\,\|v_{i+1}\|.
∣vi ⋅vi+1 ∣≤ε∥vi ∥∥vi+1 ∥. - При целых входных координатах — проверка точная: vi⋅vi+1=0v_i\cdot v_{i+1}=0vi ⋅vi+1 =0.
- При обнаружении нарушения — не ортогонален; иначе — ортогонален (в смысле поворотов ±90∘\pm90^\circ±90∘).
Если нужно, могу прислать компактный пример кода (C++/Python) с указаниями выбора ε\varepsilonε.