Даны три точки в пространстве A, B, C. Предложите алгоритм найти угол между векторами AB и AC и обсудите устойчивость метода при вычислениях с плавающей запятой
Алгоритм (шаги): 1. Построить векторы от AAA: u=AB→=B−A\mathbf{u} = \overrightarrow{AB} = B - Au=AB=B−A, v=AC→=C−A\mathbf{v} = \overrightarrow{AC} = C - Av=AC=C−A. 2. Проверка нулевой длины: вычислить квадраты норм ∥u∥2\|\mathbf{u}\|^2∥u∥2, ∥v∥2\|\mathbf{v}\|^2∥v∥2. Если какая‑то равна 000, угол не определён (точки совпадают). 3. Надёжный способ получить угол — через atan2 \operatorname{atan2} atan2 с модулем векторного произведения и скалярным произведением: - скалярное произведение d=u⋅v=∑uivi \;d = \mathbf{u}\cdot\mathbf{v} = \sum u_i v_i\;d=u⋅v=∑uivi, - векторное произведение (в 3D) w=u×v \;\mathbf{w} = \mathbf{u}\times\mathbf{v}\;w=u×v и его длина s=∥w∥ \;s = \|\mathbf{w}\|\;s=∥w∥ (в 2D можно взять псевдовектор или вычислить эквивалентную величину), - угол θ=atan2(s, d)\displaystyle \theta = \operatorname{atan2}\bigl(s,\;d\bigr)θ=atan2(s,d). Этот результат даёт θ∈[0,π]\theta\in[0,\pi]θ∈[0,π]. Альтернатива (менее устойчива при краевых случаях): стандартная формула через косинус cosθ=u⋅v∥u∥ ∥v∥,θ=arccos(clamp(cosθ,−1,1)).
\cos\theta=\frac{\mathbf{u}\cdot\mathbf{v}}{\|\mathbf{u}\|\;\|\mathbf{v}\|},\qquad \theta=\arccos\bigl(\operatorname{clamp}(\cos\theta,-1,1)\bigr). cosθ=∥u∥∥v∥u⋅v,θ=arccos(clamp(cosθ,−1,1)). Устойчивость и практические рекомендации: - Арккосинус плохо устойчив при малых или близких к π\piπ углах, потому что производная arccos′(x)=−1/1−x2\arccos'(x)=-1/\sqrt{1-x^2}arccos′(x)=−1/1−x2 растёт при x→±1x\to\pm1x→±1. Небольшая ошибка в cosθ\cos\thetacosθ даёт большую ошибку в θ\thetaθ. - Формула с atan2(s,d)\operatorname{atan2}(s,d)atan2(s,d) обычно стабильнее: для малых θ\thetaθ получается θ≈s/d\theta\approx s/dθ≈s/d и относительная погрешность меньше. Для углов около π\piπ аналогично корректно даёт значение близкое к π\piπ. - Предотвращение переполнения/понижения: перед вычислением норм (и скалярного/векторного произведения) можно масштабировать векторы на один и тот же положительный множитель, например разделить компоненты на maxi{∣ui∣,∣vi∣}\max_i\{|u_i|,|v_i|\}maxi{∣ui∣,∣vi∣} (если он ненулевой). Это не меняет θ\thetaθ, но уменьшает риск переполнения/underflow при суммах квадратов. - Всегда делать clamp результата деления для cosθ\cos\thetacosθ в [−1,1][-1,1][−1,1] перед arccos\arccosarccos. - Проверять нулевые длины и возвращать ошибку/NaN для совпадающих точек. - Используйте тип с большей точностью (long double) или аппаратное FMA, если нужна повышенная точность. - В многомерном (не обязательно 3D) варианте можно использовать норму «внешнего произведения» через размерность (например, в 2D эквивалентно модулю псевдовектора); в размерностях >3 для sss можно вычислять ∥u∥∥v∥sinθ=∥u∥2∥v∥2−(u⋅v)2\|\mathbf{u}\|\|\mathbf{v}\|\sin\theta = \sqrt{\|\mathbf{u}\|^2\|\mathbf{v}\|^2 - (\mathbf{u}\cdot\mathbf{v})^2}∥u∥∥v∥sinθ=∥u∥2∥v∥2−(u⋅v)2 и затем θ=atan2(∥u∥2∥v∥2−(u⋅v)2, u⋅v)\theta=\operatorname{atan2}\bigl(\sqrt{\|\mathbf{u}\|^2\|\mathbf{v}\|^2 - (\mathbf{u}\cdot\mathbf{v})^2},\;\mathbf{u}\cdot\mathbf{v}\bigr)θ=atan2(∥u∥2∥v∥2−(u⋅v)2,u⋅v). Сводка: рекомендую метод с atan2(∥u×v∥, u⋅v)\operatorname{atan2}( \|\mathbf{u}\times\mathbf{v}\|,\; \mathbf{u}\cdot\mathbf{v} )atan2(∥u×v∥,u⋅v) плюс масштабирование и проверки на ноль — он более устойчив при вычислениях с плавающей запятой, чем прямой arccos\arccosarccos по нормированным векторaм.
1. Построить векторы от AAA: u=AB→=B−A\mathbf{u} = \overrightarrow{AB} = B - Au=AB=B−A, v=AC→=C−A\mathbf{v} = \overrightarrow{AC} = C - Av=AC=C−A.
2. Проверка нулевой длины: вычислить квадраты норм ∥u∥2\|\mathbf{u}\|^2∥u∥2, ∥v∥2\|\mathbf{v}\|^2∥v∥2. Если какая‑то равна 000, угол не определён (точки совпадают).
3. Надёжный способ получить угол — через atan2 \operatorname{atan2} atan2 с модулем векторного произведения и скалярным произведением:
- скалярное произведение d=u⋅v=∑uivi \;d = \mathbf{u}\cdot\mathbf{v} = \sum u_i v_i\;d=u⋅v=∑ui vi ,
- векторное произведение (в 3D) w=u×v \;\mathbf{w} = \mathbf{u}\times\mathbf{v}\;w=u×v и его длина s=∥w∥ \;s = \|\mathbf{w}\|\;s=∥w∥ (в 2D можно взять псевдовектор или вычислить эквивалентную величину),
- угол θ=atan2(s, d)\displaystyle \theta = \operatorname{atan2}\bigl(s,\;d\bigr)θ=atan2(s,d).
Этот результат даёт θ∈[0,π]\theta\in[0,\pi]θ∈[0,π].
Альтернатива (менее устойчива при краевых случаях): стандартная формула через косинус
cosθ=u⋅v∥u∥ ∥v∥,θ=arccos(clamp(cosθ,−1,1)). \cos\theta=\frac{\mathbf{u}\cdot\mathbf{v}}{\|\mathbf{u}\|\;\|\mathbf{v}\|},\qquad
\theta=\arccos\bigl(\operatorname{clamp}(\cos\theta,-1,1)\bigr).
cosθ=∥u∥∥v∥u⋅v ,θ=arccos(clamp(cosθ,−1,1)).
Устойчивость и практические рекомендации:
- Арккосинус плохо устойчив при малых или близких к π\piπ углах, потому что производная arccos′(x)=−1/1−x2\arccos'(x)=-1/\sqrt{1-x^2}arccos′(x)=−1/1−x2 растёт при x→±1x\to\pm1x→±1. Небольшая ошибка в cosθ\cos\thetacosθ даёт большую ошибку в θ\thetaθ.
- Формула с atan2(s,d)\operatorname{atan2}(s,d)atan2(s,d) обычно стабильнее: для малых θ\thetaθ получается θ≈s/d\theta\approx s/dθ≈s/d и относительная погрешность меньше. Для углов около π\piπ аналогично корректно даёт значение близкое к π\piπ.
- Предотвращение переполнения/понижения: перед вычислением норм (и скалярного/векторного произведения) можно масштабировать векторы на один и тот же положительный множитель, например разделить компоненты на maxi{∣ui∣,∣vi∣}\max_i\{|u_i|,|v_i|\}maxi {∣ui ∣,∣vi ∣} (если он ненулевой). Это не меняет θ\thetaθ, но уменьшает риск переполнения/underflow при суммах квадратов.
- Всегда делать clamp результата деления для cosθ\cos\thetacosθ в [−1,1][-1,1][−1,1] перед arccos\arccosarccos.
- Проверять нулевые длины и возвращать ошибку/NaN для совпадающих точек.
- Используйте тип с большей точностью (long double) или аппаратное FMA, если нужна повышенная точность.
- В многомерном (не обязательно 3D) варианте можно использовать норму «внешнего произведения» через размерность (например, в 2D эквивалентно модулю псевдовектора); в размерностях >3 для sss можно вычислять ∥u∥∥v∥sinθ=∥u∥2∥v∥2−(u⋅v)2\|\mathbf{u}\|\|\mathbf{v}\|\sin\theta = \sqrt{\|\mathbf{u}\|^2\|\mathbf{v}\|^2 - (\mathbf{u}\cdot\mathbf{v})^2}∥u∥∥v∥sinθ=∥u∥2∥v∥2−(u⋅v)2 и затем θ=atan2(∥u∥2∥v∥2−(u⋅v)2, u⋅v)\theta=\operatorname{atan2}\bigl(\sqrt{\|\mathbf{u}\|^2\|\mathbf{v}\|^2 - (\mathbf{u}\cdot\mathbf{v})^2},\;\mathbf{u}\cdot\mathbf{v}\bigr)θ=atan2(∥u∥2∥v∥2−(u⋅v)2 ,u⋅v).
Сводка: рекомендую метод с atan2(∥u×v∥, u⋅v)\operatorname{atan2}( \|\mathbf{u}\times\mathbf{v}\|,\; \mathbf{u}\cdot\mathbf{v} )atan2(∥u×v∥,u⋅v) плюс масштабирование и проверки на ноль — он более устойчив при вычислениях с плавающей запятой, чем прямой arccos\arccosarccos по нормированным векторaм.