Skip to content

Bài 63: Góc & Phép Quay

Tác giả: FPTOJ Team
Nội dung tham khảo từ: CP-Algorithms, VNOI Wiki

0. Tại sao phải học?

Bài toán mở đầu: Cho điểm \(P(1, 0)\). Quay \(P\) quanh gốc tọa độ \(90°\) ngược chiều kim đồng hồ được điểm nào? Bằng trực giác: \((0, 1)\). Nhưng quay \(P(3, 2)\) đi \(37°\) thì sao — không thể vẽ tay. Công thức quay cho đáp án ngay, và nó chỉ là tổ hợp tuyến tính của 2 vector cơ sở: trục \(x\) \((1,0)\) quay thành \((\cos\theta, \sin\theta)\), trục \(y\) \((0,1)\) quay thành \((-\sin\theta, \cos\theta)\). Mọi điểm \(P = x \cdot e_1 + y \cdot e_2\) sau quay vẫn là tổ hợp với cùng hệ số \(x, y\) — đó chính là ma trận quay ở §2.

1. Góc giữa hai vector

1.1 Dùng atan2

Hàm atan2(y, x) trả về góc (radian) từ trục \(x\) dương đến vector \((x, y)\), trong khoảng \((-\pi, \pi]\).

double angleOfVector(Point A) {
    return atan2(A.y, A.x); // góc từ trục x đến vector A
}

double angleBetween(Point A, Point B) {
    double a = atan2(A.y, A.x);
    double b = atan2(B.y, B.x);
    double diff = abs(a - b);
    if (diff > M_PI) diff = 2 * M_PI - diff;
    return diff; // góc nhỏ nhất giữa 2 vector
}
import math

def angle_of_vector(A):
    return math.atan2(A[1], A[0])

def angle_between(A, B):
    a = math.atan2(A[1], A[0])
    b = math.atan2(B[1], B[0])
    diff = abs(a - b)
    if diff > math.pi:
        diff = 2 * math.pi - diff
    return diff

1.2 Dùng dot product

\[\cos(\theta) = \frac{A \cdot B}{|A| \cdot |B|}\]
1
2
3
4
5
double angleBetweenDot(Point A, Point B) {
    double cosTheta = dot(A, B) / (magnitude(A) * magnitude(B));
    cosTheta = max(-1.0, min(1.0, cosTheta)); // clamp
    return acos(cosTheta);
}

2. Phép quay điểm

2.1 Quay quanh gốc tọa độ

Quay điểm \((x, y)\) một góc \(\theta\) (ngược chiều kim đồng hồ, đơn vị radian):

\[x' = x\cos\theta - y\sin\theta\]
\[y' = x\sin\theta + y\cos\theta\]

Đọc công thức bằng lời: tọa độ mới \(x'\) = (hình chiếu của \(P\) lên trục \(x\) đã quay) \(-\) (hình chiếu lên trục \(y\) đã quay). Khi \(\theta = 90°\): \(\cos = 0, \sin = 1\) → \((x,y) \to (-y, x)\) — khớp trực giác (điểm \((1,0)\) thành \((0,1)\) ✓).

Minh họa trực quan (Phép quay điểm quanh gốc tọa độ):

1
2
3
4
5
6
7
8
// Quay điểm P quanh gốc tọa độ góc theta (radian, CCW)
Point rotate(Point P, double theta) {
    double c = cos(theta), s = sin(theta);
    return {
        P.x * c - P.y * s, // x' = x·cos − y·sin
        P.x * s + P.y * c  // y' = x·sin + y·cos
    };
}
1
2
3
4
5
6
7
8
import math

def rotate(P, theta):
    x, y = P
    return (
        x * math.cos(theta) - y * math.sin(theta),
        x * math.sin(theta) + y * math.cos(theta)
    )

2.2 Quay quanh điểm bất kỳ

Quay điểm \(P\) quanh tâm \(O\) góc \(\theta\):

\[P' = O + \text{rotate}(P - O, \theta)\]
1
2
3
4
5
6
// Quay P quanh tâm O: tịnh tiến O về gốc → quay → tịnh tiến ngược lại
Point rotateAround(Point P, Point O, double theta) {
    Point translated = {P.x - O.x, P.y - O.y}; // đưa O về gốc
    Point rotated = rotate(translated, theta); // quay quanh gốc
    return {rotated.x + O.x, rotated.y + O.y}; // trả O về chỗ cũ
}

3. Phép quay 90°

3.1 Quay 90° ngược chiều kim đồng hồ (CCW)

\((x, y) \to (-y, x)\)

3.2 Quay 90° theo chiều kim đồng hồ (CW)

\((x, y) \to (y, -x)\)

Point rotate90CCW(Point P) { return {-P.y, P.x}; }
Point rotate90CW(Point P) { return {P.y, -P.x}; }

4. Ứng dụng: Điểm đối xứng qua trục

4.1 Đối xứng qua trục tung \(Oy\)

\((x, y) \to (-x, y)\)

4.2 Đối xứng qua trục hoành \(Ox\)

\((x, y) \to (x, -y)\)

4.3 Đối xứng qua đường thẳng bất kỳ

Xem Bài 61 - phần reflectPoint.


5. Kiểm tra điểm nằm trong góc

5.1 Bài toán

Cho 3 điểm \(O, A, B\). Kiểm tra điểm \(P\) có nằm trong góc \(\angle AOB\) không.

5.2 Ý tưởng

Sử dụng cross product để kiểm tra \(P\) nằm cùng phía \(A\) và \(B\) so với \(O\).

// Kiểm tra P nằm trong góc AOB (góc < 180°)
bool pointInAngle(Point O, Point A, Point B, Point P) {
    double crossOA = cross(A - O, P - O);
    double crossOB = cross(B - O, P - O);
    if (cross(A - O, B - O) >= 0) {
        return crossOA >= -1e-9 && crossOB <= 1e-9;
    } else {
        return crossOA >= -1e-9 || crossOB <= 1e-9;
    }
}

6. Xoay hình học

6.1 Xoay đa giác

Xoay toàn bộ đa giác quanh tâm \(O\) góc \(\theta\):

1
2
3
4
5
6
vector<Point> rotatePolygon(vector<Point>& poly, Point O, double theta) {
    vector<Point> result;
    for (auto& P : poly)
        result.push_back(rotateAround(P, O, theta));
    return result;
}

6.2 Ứng dụng: Bài toán minimum bounding rectangle

Xoay đa giác các góc khác nhau để tìm hình chữ nhật bao nhỏ nhất.


7. Lỗi thường gặp

1
2
3
4
5
// SAI: truyền độ vào hàm cần radian
rotate(P, 90); // quay 90 radian!

// ĐÚNG: đổi sang radian trước
rotate(P, 90 * M_PI / 180.0);
  • Nhầm chiều quay: công thức trên là CCW (ngược kim đồng hồ). Muốn quay CW (thuận kim đồng hồ) góc \(\theta\) thì truyền \(-\theta\).
  • M_PI không có trên MSVC: thêm #define _USE_MATH_DEFINES trước #include <bits/stdc++.h>, hoặc tự định nghĩa const double PI = acos(-1);.
  • So sánh == với số thực: sau quay, tọa độ là số thực — kiểm tra bằng nhau phải dùng abs(a - b) < 1e-9, không dùng ==.
  • Quên tịnh tiến khi quay quanh điểm bất kỳ: rotate(P, theta) luôn quay quanh gốc tọa độ. Muốn quay quanh \(O\) phải dùng rotateAround (tịnh tiến → quay → tịnh tiến ngược).
  • acos nhận giá trị ngoài \([-1, 1]\): do sai số làm tròn, dot/(|A||B|) có thể ra \(1.0000000002\) → acos trả NaN. Luôn clamp như code §1.2.

8. Bài tập luyện tập (FPTOJ)

Bài Nền tảng Độ khó Kiểu bài tập (Bản chất)
rotate-point Quay điểm quanh gốc tọa độ ⭐ Phép quay
rotate-angle Góc giữa hai vector ⭐ Góc vector
rotate-ccw Xác định hướng quay ⭐ Cross product
rotate-around Quay điểm quanh điểm bất kỳ ⭐⭐ Phép quay
rotate-sort-angle Sắp xếp theo góc ⭐⭐ Sắp xếp góc cực
rotate-clock-angle Góc quay đồng hồ ⭐⭐ Góc giữa hai vector
rotate-polar Chuyển đổi tọa độ ⭐ Tọa độ cực
rot-tri-rotate Diện tích tam giác sau quay ⭐⭐⭐ Phép quay + diện tích

9. Bài tập tự luận

Bài 1: Quay điểm

Đề bài: Cho điểm \(P(x,y)\) và góc \(\theta\) (độ). Quay \(P\) quanh gốc tọa độ một góc \(\theta\) ngược chiều kim đồng hồ.

Input: 3 số thực \(x, y, \theta\) \((|x|,|y| \leq 10^4, 0 \leq \theta \leq 360)\)

Output: Tọa độ điểm sau khi quay, làm tròn 4 chữ số thập phân.

Ví dụ:

Input Output
1 0 90 0.0000 1.0000
1 1 45 -0.0000 1.4142
Lời giải

\(x' = x\cos\theta - y\sin\theta\), \(y' = x\sin\theta + y\cos\theta\). Đổi \(\theta\) từ độ sang radian.

#include <bits/stdc++.h>
using namespace std;
int main() {
    double x, y, theta;
    cin >> x >> y >> theta;
    double rad = theta * M_PI / 180.0;
    double nx = x * cos(rad) - y * sin(rad);
    double ny = x * sin(rad) + y * cos(rad);
    cout << fixed << setprecision(4) << nx << " " << ny << "\n";
}

Bài 2: Góc giữa 2 vector

Đề bài: Cho 2 vector \(A(x_1,y_1)\) và \(B(x_2,y_2)\). Tính góc giữa chúng (độ).

Input: 4 số thực \(x_1, y_1, x_2, y_2\)

Output: Góc giữa 2 vector (độ), làm tròn 4 chữ số thập phân.

Ví dụ:

Input Output
1 0 0 1 90.0000
1 0 1 1 45.0000
Lời giải

\(\theta = \arccos\left(\frac{A \cdot B}{|A| \cdot |B|}\right)\), đổi sang độ.

#include <bits/stdc++.h>
using namespace std;
int main() {
    double x1, y1, x2, y2;
    cin >> x1 >> y1 >> x2 >> y2;
    double dot = x1 * x2 + y1 * y2;
    double mag = hypot(x1, y1) * hypot(x2, y2);
    double angle = acos(max(-1.0, min(1.0, dot / mag)));
    cout << fixed << setprecision(4) << angle * 180.0 / M_PI << "\n";
}

Bài 3: Điểm đối xứng

Đề bài: Cho điểm \(P(x,y)\) và đường thẳng \(Ax + By + C = 0\). Tìm điểm đối xứng của \(P\) qua đường thẳng.

Input: 5 số thực \(x, y, A, B, C\)

Output: Tọa độ điểm đối xứng, làm tròn 4 chữ số thập phân.

Ví dụ:

Input Output
1 0 1 -1 0 0.0000 1.0000
0 0 1 0 -3 6.0000 0.0000
Lời giải

Chiếu \(P\) lên đường thẳng \(H\), rồi \(P' = 2H - P\).

#include <bits/stdc++.h>
using namespace std;
int main() {
    double x, y, A, B, C;
    cin >> x >> y >> A >> B >> C;
    double d = (A * x + B * y + C) / (A * A + B * B);
    double nx = x - 2 * A * d;
    double ny = y - 2 * B * d;
    cout << fixed << setprecision(4) << nx << " " << ny << "\n";
}

Bài 4: Kiểm tra quay

Đề bài: Cho 3 điểm \(A, B, C\) theo thứ tự. Xác định \(C\) nằm bên trái, bên phải, hay thẳng hàng so với vector \(AB\).

Input: 6 số thực \(x_A, y_A, x_B, y_B, x_C, y_C\)

Output: LEFT, RIGHT, hoặc COLLINEAR.

Ví dụ:

Input Output
0 0 1 0 0 1 LEFT
0 0 1 0 0 -1 RIGHT
0 0 1 0 2 0 COLLINEAR
Lời giải

Tính cross product \((B-A) \times (C-A)\). Dương = trái, âm = phải, 0 = thẳng hàng.


💬 Bình luận