격자를 위아래로 뒤집는 것과 행과 열을 맞바꾸는 것(전치)은 둘 다 거울에 비추는 반사이고, 이 둘을 이으면 90도 회전이 된다. 그래서 칸이 어디로 가는지 외우지 않고 행렬 곱으로 만들 수 있고, 두 동작의 순서를 바꾸면 왜 정확히 반대로 도는지도 안다. 다만 칸 번호를 격자 가운데가 아니라 왼쪽 위 모서리부터 세서 식에 평행이동이 붙으므로 엄밀히는 선형이 아니다. 또 90도의 배수만 되고, 직사각형 격자는 돌리면 가로세로가 바뀐다.
먼저 비교해 보기
표를 펼치기 전에 두 사례의 공통 구조와 대응 관계를 먼저 적어 본다. 특히 위아래 뒤집기와 전치의 행렬을 어떻게 곱하면 시계 방향 회전의 행렬이 나오는지, 두 동작의 순서를 바꾸면 왜 방향이 정확히 반대가 되는지 써 본다.
왼쪽은 구현과 시뮬레이션의 격자 돌리기를 a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]에 한 결과다. 오른쪽은 같은 동작을 칸 번호 \((r, c)\)(행, 열, 0부터)에 행렬을 곱하고 더하는 식으로 쓴 것이다.
알고리즘: 3 × 3 격자 a |
선형대수학: 칸 번호 \((r, c)\)에 행렬 곱하기 |
|---|---|
위아래 뒤집기 a[::-1] → [[7,8,9],[4,5,6],[1,2,3]] |
\(\begin{pmatrix}-1 & 0\\ 0 & 1\end{pmatrix}\begin{pmatrix}r\\ c\end{pmatrix} + \begin{pmatrix}2\\ 0\end{pmatrix}\) |
전치 zip(*a) → [[1,4,7],[2,5,8],[3,6,9]] |
\(\begin{pmatrix}0 & 1\\ 1 & 0\end{pmatrix}\begin{pmatrix}r\\ c\end{pmatrix}\) |
시계 방향 90° zip(*a[::-1]) → [[7,4,1],[8,5,2],[9,6,3]] |
\(\begin{pmatrix}0 & 1\\ -1 & 0\end{pmatrix}\begin{pmatrix}r\\ c\end{pmatrix} + \begin{pmatrix}0\\ 2\end{pmatrix}\) |
반시계 방향 90° list(zip(*a))[::-1] → [[3,6,9],[2,5,8],[1,4,7]] |
\(\begin{pmatrix}0 & -1\\ 1 & 0\end{pmatrix}\begin{pmatrix}r\\ c\end{pmatrix} + \begin{pmatrix}2\\ 0\end{pmatrix}\) |
시계 방향으로 네 번 돌리면 a 그대로 |
\(\begin{pmatrix}0 & 1\\ -1 & 0\end{pmatrix}^4 = I\) |
결과는 zip이 주는 튜플을 리스트로 바꾼 모양이다.
대응 관계
격자는 행 \(n\)개, 열 \(m\)개다. 칸 번호 \((r, c)\)를 벡터로 보면 격자 연산은 “2 × 2 행렬 곱 + 평행이동”이다1. \(R_\theta\)는 선형변환의 각 \(\theta\) 회전 행렬이다.
| 알고리즘 | 수학 | 공통 구조 |
|---|---|---|
a[::-1]: \((r, c) \to (n-1-r,\ c)\) |
\(\begin{pmatrix}-1 & 0\\ 0 & 1\end{pmatrix}\), 평행이동 \((n-1, 0)\) | 가로줄 거울에 비추기 |
zip(*a): \((r, c) \to (c, r)\) |
\(\begin{pmatrix}0 & 1\\ 1 & 0\end{pmatrix}\)(선형변환 예제의 반사) | 대각선 거울에 비추기 |
zip(*a[::-1]): \((r, c) \to (c,\ n-1-r)\) |
\(\begin{pmatrix}0 & 1\\ 1 & 0\end{pmatrix}\begin{pmatrix}-1 & 0\\ 0 & 1\end{pmatrix} = \begin{pmatrix}0 & 1\\ -1 & 0\end{pmatrix} = R_{-90°}\), 평행이동 \((0, n-1)\) | 반사 두 번 = 회전 |
list(zip(*a))[::-1]: \((r, c) \to (m-1-c,\ r)\) |
\(\begin{pmatrix}-1 & 0\\ 0 & 1\end{pmatrix}\begin{pmatrix}0 & 1\\ 1 & 0\end{pmatrix} = R_{90°}\), 평행이동 \((m-1, 0)\) | 순서를 바꾸면 거꾸로 돈다 |
| 식에 붙은 \(n-1\), \(m-1\) | \(\mathbf{x} \mapsto A\mathbf{x} + \mathbf{b}\)의 \(\mathbf{b}\)(아핀 함수, 행렬과 행렬-벡터 곱) | 원점을 옮기는 부분 |
| 방향 \((dr, dc)\)를 시계 방향으로 돌리기: \((dc, -dr)\) | \(R_{-90°}\)만 곱한다 | 방향은 두 칸의 차이라 평행이동이 지워진다(벡터) |
| 시계 방향 네 번 = 원래 격자 | \(R_{-90°}^4 = I\) | 거듭제곱으로 제자리 |
격자 값 전체를 행렬 \(A\)로 보면 평행이동 없이 행렬 곱과 전치만 남는다. \(J_k\)는 \(k \times k\) 단위행렬의 행 순서를 거꾸로 한 행렬이다. 대칭이라 \(J_k^\top = J_k\)(\(^\top\)는 행과 열을 바꾸는 전치)이고, 두 번 곱하면 제자리라 \(J_k^2 = I\)다.
| 알고리즘 | 수학 | 공통 구조 |
|---|---|---|
a[::-1] |
\(J_nA\) | 왼쪽에 곱하면 행 순서가 바뀐다 |
[row[::-1] for row in a] |
\(AJ_m\) | 오른쪽에 곱하면 열 순서가 바뀐다 |
zip(*a) |
\(A^\top\) | 전치 |
zip(*a[::-1]) |
\((J_nA)^\top = A^\top J_n^\top = A^\top J_n\) | 곱의 전치는 순서가 뒤집힌다(행렬 곱셈과 전치) |
list(zip(*a))[::-1] |
\(J_mA^\top\) | 시계 방향으로 돌린 \(m \times n\) 격자에 쓰면 \(J_n(A^\top J_n)^\top = J_n^2A = A\)라 제자리로 돌아온다 |
색칠한 1, 2, 3을 따라가면 된다. 가로 거울(위아래 뒤집기)에 비추면 첫 줄이 맨 아래로 간다. 이어서 대각선 거울(전치)에 비추면 그 줄이 오른쪽 열로 선다. 거울 두 번을 거친 결과가 시계 방향으로 90° 돈 격자다2.
어디까지 같은가
- 엄밀히는 아핀 변환이다. 칸 번호를 왼쪽 위 칸부터 세므로 원점 \((0, 0)\)이 격자 가운데가 아니라 왼쪽 위 칸이다. 그래서 식에 \(n - 1\), \(m - 1\) 같은 평행이동이 붙는다. 1부터 세어도 원점이 모서리 근처라 평행이동은 남는다. 평행이동은 원점을 옮기므로 선형이 아니다. 격자 가운데를 원점으로 잡으면 평행이동이 사라진다. 행 번호에서 \(\frac{n-1}{2}\), 열 번호에서 \(\frac{m-1}{2}\)를 빼는 것이다. 돌린 뒤의 \(m \times n\) 격자에서는 \(\frac{m-1}{2}\), \(\frac{n-1}{2}\)를 뺀다. 그러면 시계 방향 회전은 \(R_{-90°}\) 곱 하나다. 3 × 3이면 칸을 \((r - 1, c - 1)\)로 적는다.
- 90°의 배수만 된다. 가운데에서 오른쪽으로 한 칸 떨어진 칸을 45° 돌리면 가운데에서 가로, 세로로 약 0.707칸씩 떨어진 곳에 간다. 칸의 가운데가 아니다. 그림을 45° 돌릴 때는 빈자리를 주변 값으로 채운다(보간).
- 직사각형 격자는 모양이 바뀐다. 뒤집기와 회전 8가지(회전 넷, 반사 넷) 가운데 \(n \ne m\)인 격자의 모양을 지키는 것은 그대로, 180°, 위아래 뒤집기, 좌우 뒤집기 넷뿐이다. 전치, 반대각선 뒤집기, 시계·반시계 90°는 \(m \times n\) 격자를 만든다. 그래서 시계 방향으로 네 번 돌리면 평행이동이 \(n - 1\), \(m - 1\), \(n - 1\), \(m - 1\)로 번갈아 붙는다. 한 함수의 네제곱은 아니지만, 넷을 이으면 제자리다.
- 줄 길이가 다르면 대응하는 행렬이 없다.
zip(*a)는 가장 짧은 줄에서 멈춘다3.[[1, 2, 3], [4, 5]]를 전치하면 3이 오류 없이 사라진다. - 그림의 방향. \((r, c)\)에서 \(r\)은 아래로, \(c\)는 오른쪽으로 는다. 이 좌표계는 교과서의 \((x, y)\)(\(x\)는 오른쪽, \(y\)는 위)를 90° 돌린 것이다(\(r = -y\), \(c = x\)). 돌리기만 했으니 시계 방향과 반시계 방향이 그대로 맞고, \(R_{-90°}\)은 화면에서도 시계 방향이다. 다만 전치의 거울은 화면에서 왼쪽 위에서 오른쪽 아래로 내려가는 대각선이다. 같은 행렬이 선형변환 예제의 그림(위가 \(+y\))에서는 오른쪽 위로 올라가는 직선 \(y = x\)에 비추는 반사라, 두 그림을 맞대 보면 헷갈린다.
- 좌표 순서를 바꾸면 이름이 뒤바뀐다. 칸을 \((x, y) = (c, r)\)처럼 순서를 바꿔 적으면 거울에 비친 좌표계가 된다. 그러면 같은 시계 방향 회전이 \(\begin{pmatrix}0 & -1\\ 1 & 0\end{pmatrix}\)로 적혀 반시계 방향처럼 보인다. 회전인지 반사인지를 가르는 행렬식 부호는 그대로다.
이 연결로 얻는 것
알고리즘 쪽에서 얻는 것이다.
- 칸 번호 식을 외우지 않고 만든다. 선형 부분은 행렬로 정하고, 평행이동은 가장 작은 번호가 0이 되게 고른다. 예를 들어 180° 회전은 \(R_{180°} = -I\)라 \((r, c) \to (-r, -c)\)이다. \(-r\)은 \(-(n-1)\)부터 0까지, \(-c\)는 \(-(m-1)\)부터 0까지라 \((n - 1, m - 1)\)을 더하면 \((n - 1 - r,\ m - 1 - c)\)다.
- 순서를 바꾸면 정확히 반대로 도는 이유. 위아래 뒤집기를 \(F\), 전치를 \(T\)라 한다. 같은 반사를 두 번 하면 제자리라 \(F^{-1} = F\), \(T^{-1} = T\)다. 역행렬의 \((AB)^{-1} = B^{-1}A^{-1}\)로 \((TF)^{-1} = F^{-1}T^{-1} = FT\)다4. 시계 방향 \(TF\)(뒤집고 전치)를 되돌리는 것이 바로 반시계 방향 \(FT\)(전치하고 뒤집기)다.
- 몇 도 도는지는 두 거울 사이의 각으로 정해진다. 반사 두 번은 회전이다. 첫 거울에서 둘째 거울로 기우는 방향으로, 그 사이 각의 두 배만큼 돈다5.
| 첫 거울 | 둘째 거울 | 사이 각 | 결과 |
|---|---|---|---|
| 가로줄(위아래 뒤집기) | 왼쪽 위–오른쪽 아래 대각선(전치) | 시계 방향 45° | 시계 방향 90° |
| 대각선(전치) | 가로줄(위아래 뒤집기) | 반시계 방향 45° | 반시계 방향 90° |
| 가로줄(위아래 뒤집기) | 세로줄(좌우 뒤집기) | 90° | 180° |
| 같은 거울 | 같은 거울 | 0° | 제자리 |
- 회전만으로는 거울상이 나오지 않는다. 반사의 행렬식은 \(-1\), 회전은 \(+1\)이다(회전의 세 표현). 행렬식은 곱을 곱으로 보내므로(\(\det(AB) = \det A \cdot \det B\))4 회전을 몇 번 이어도 \(+1\)이다. 뒤집기와 회전 8가지도 행렬식 \(+1\)인 회전 넷과 \(-1\)인 반사 넷으로 정확히 나뉜다. 행렬식이 알려 주는 것은 여기까지다. \(+1\)이라도 몇 도 도는지는 알려 주지 않는다.
- 돌려서 나오는 열쇠. 자물쇠와 열쇠에서
zip(*key[::-1])을 몇 번 해도 그 동작은 반사가 되지 않는다. 그래서 열쇠를 네 거울(가로줄, 세로줄, 두 대각선) 어디에 비춰도 원래 모양과 다르면, 좌우가 뒤집힌 열쇠는 나오지 않는다. 어느 한 거울에 비춰도 그대로인 대칭 열쇠만 예외다. 예를 들어 대각선 대칭인[[1, 1, 0], [1, 0, 0], [0, 0, 0]]은 좌우로 뒤집은 모양이 시계 방향으로 한 번 돌린 모양과 같다. - 반시계 한 번 = 시계 세 번. \(R_{90°} = R_{-90°}^3 = R_{-90°}^{-1}\)이다. 그래서 시계 방향으로 네 번 돌려 본 모양 넷과 반시계 방향으로 네 번 돌려 본 모양 넷은 같은 집합이다. 자물쇠와 열쇠 7단계의 “네 방향을 다 보면 방향은 상관없다”가 이 때문이다.
수학 쪽에서 얻는 것이다.
- 전치와 합성을 한 줄 코드로 확인한다. \((J_nA)^\top = A^\top J_n\)의 왼쪽은 “뒤집고 전치”
zip(*a[::-1])이고, 오른쪽은 “전치하고 각 행 뒤집기”[row[::-1] for row in zip(*a)]다. 그래서 두 코드가 같은 시계 방향 회전이다. \(J_2\)는 행렬 곱셈과 전치 C1의 \(\begin{pmatrix}0 & 1\\ 1 & 0\end{pmatrix}\)이다. C1에서 본 “왼쪽에 곱하면 행, 오른쪽에 곱하면 열을 바꾼다”가 \(k \times k\)로 커진 것이다. - 평행이동까지 행렬 하나로 쓴다. 행렬과 행렬-벡터 곱의 동차 좌표 \((r, c, 1)\)을 쓰면 시계 방향 회전은 \(H_n = \begin{pmatrix}0 & 1 & 0\\ -1 & 0 & n-1\\ 0 & 0 & 1\end{pmatrix}\) 곱 하나다. 정사각 격자면 \(H_n^4 = I\)다. 직사각형이면 행 수가 번갈아 바뀌어 \(H_mH_nH_mH_n = I\)다.
전이 문제
사진 편집 앱에서 행 \(n\)개, 열 \(m\)개인 사진에 “좌우 반전”을 누른 뒤 “시계 방향 90° 회전”을 눌렀다.
- 두 동작을 합친 결과는 어떤 동작 하나와 같은가?
- 원래 사진으로 되돌리는 방법을 두 가지 들라.
- “회전” 버튼만 몇 번 눌러서 되돌릴 수 있는가?
답
- 반대각선(오른쪽 위–왼쪽 아래 대각선) 거울에 비추는 뒤집기 하나다. 선형 부분을 곱하면 \(R_{-90°}\begin{pmatrix}1 & 0\\ 0 & -1\end{pmatrix} = \begin{pmatrix}0 & -1\\ -1 & 0\end{pmatrix}\), 곧 \((r, c) \to (-c, -r)\)이다. 가장 작은 번호를 0으로 맞추면 \((m - 1 - c,\ n - 1 - r)\)이고, 사진은 \(m \times n\)이 된다. 거울로 보면 더 빠르다. 시계 방향 회전은 “세로 거울(좌우 반전) 뒤 반대각선 거울”이다(세로줄에서 반대각선까지 시계 방향 45°). 그 앞에 좌우 반전을 하나 더 붙이면 좌우 반전 두 번이 지워지고 반대각선 뒤집기만 남는다. 예:
[[1, 2, 3], [4, 5, 6]]→[[6, 3], [5, 2], [4, 1]]. - 같은 반대각선 뒤집기를 한 번 더 한다. 같은 반사를 두 번 하면 제자리다. 또는 나중에 한 동작부터 거꾸로 되돌린다(\((XY)^{-1} = Y^{-1}X^{-1}\)). 시계 방향 회전을 먼저 되돌리고(반시계 방향 90°), 이어서 좌우 반전을 되돌린다(좌우 반전 한 번 더).
- 보통은 안 된다. 되돌리는 동작은 반대각선 뒤집기처럼 행렬식이 \(-1\)인 반사여야 하는데, 회전은 몇 번을 이어도 행렬식이 \(+1\)이다. 원래 사진이 어느 한 거울에 비춰도 그대로인 대칭 사진일 때만 예외다. 예를 들어 좌우 대칭인 사진은 처음의 좌우 반전이 아무것도 바꾸지 않아서, 반시계 방향으로 한 번(시계 방향으로 세 번) 돌리면 돌아온다.
확인 문제
C1 zip(*a[::-1])와 list(zip(*a))[::-1]는 같은 두 동작(위아래 뒤집기, 전치)을 순서만 바꿔 쓴다. 왜 회전 방향이 그냥 다른 것이 아니라 정확히 반대인가?
답: 두 동작이 모두 반사라 같은 동작을 두 번 하면 제자리다(\(F^2 = T^2 = I\)). 그래서 \((TF)^{-1} = F^{-1}T^{-1} = FT\)이고, 순서를 바꾼 것이 곧 되돌리기다. “행렬 곱은 교환되지 않는다”는 흔한 답은 결과가 다르다는 것까지만 말한다. 정확히 반대인 것은 각 동작이 자기 자신을 되돌리기 때문이다.
C2 “반사를 두 번 하면 늘 제자리다”가 틀렸음을 [[1, 2], [3, 4]]로 보이고, 행렬식이 어디까지 알려 주는지 말하라.
답: 위아래로 뒤집으면 [[3, 4], [1, 2]], 이어서 좌우로 뒤집으면 [[4, 3], [2, 1]]이다. 원래와 다르고, 180° 돌린 것과 같다. 가로줄 거울과 세로줄 거울이 90° 벌어져 있어 그 두 배만큼 돈다. 행렬식은 \((-1)(-1) = +1\)이라 “회전이다”까지만 알려 주고 몇 도인지는 알려 주지 않는다. 제자리가 되는 것은 같은 거울에 두 번 비출 때뿐이다.
C3 행 \(n\)개, 열 \(m\)개인 격자를 반시계 방향으로 90° 돌릴 때 칸 \((r, c)\)가 가는 곳을 “2 × 2 행렬 곱 + 평행이동”으로 쓰고, [[1, 2, 3], [4, 5, 6]]에서 확인하라.
답: 선형 부분은 \(R_{90°} = \begin{pmatrix}0 & -1\\ 1 & 0\end{pmatrix}\)이라 \((r, c) \to (-c, r)\)이다. \(-c\)가 \(-(m-1)\)부터 0까지라 \((m - 1, 0)\)을 더해 \((m - 1 - c,\ r)\)이다. 이 2 × 3 격자에서는 \((2 - c, r)\)이고, list(zip(*a))[::-1]은 [(3, 6), (2, 5), (1, 4)]다. 3은 \((0, 2)\)에서 \((0, 0)\)으로, 4는 \((1, 0)\)에서 \((2, 1)\)로 간다. 흔한 실수는 \(n - 1 - c\)로 쓰는 것이다. 정사각 격자에서만 맞는다.
-
문서 전체가 알고리즘 트랙과 선형대수학을 잇는 보충이다. 칸 번호 식, 값 행렬 \(J_nA\) 꼴의 식, 대칭 열쇠·대칭 사진의 예외, 전이 문제의 답은 36_grid-rotation-linear_verify.py에서 무작위 직사각형 격자와 작은 0/1 격자 전부로 확인했다. ↩
-
36_grid-rotation-linear_plot.py로 그렸고, 뒤집은 격자
[[7, 8, 9], [4, 5, 6], [1, 2, 3]], 돌린 격자[[7, 4, 1], [8, 5, 2], [9, 6, 3]], 칸 번호 식 \((r, c) \to (c,\ n-1-r)\), 두 반사 행렬의 곱이 \(R_{-90°}\)라는 것을 같은 코드로 확인했다. ↩ -
Python 3 공식 튜토리얼 5.1.4 “Nested List Comprehensions”:
list(zip(*matrix))로 행렬의 행과 열을 바꾼다. Python 3 표준 라이브러리 문서, Built-in Functions의zip: 가장 짧은 입력이 끝나면 멈춘다. ↩ -
Strang, Introduction to Linear Algebra 5판, 2.4절 “Rules for Matrix Operations”(교환 불가), 2.5절 “Inverse Matrices”(\((AB)^{-1} = B^{-1}A^{-1}\)), 2.7절 “Transposes and Permutations”(\((AB)^\top = B^\top A^\top\), 치환 행렬), 5.1절 “The Properties of Determinants”(\(\det(AB) = \det A \cdot \det B\)), 8.1~8.2절(회전·반사 행렬). ↩ ↩2
-
거울선이 첫 축에서 각 \(\alpha\)만큼 기운 반사는 \(S_\alpha = \begin{pmatrix}\cos 2\alpha & \sin 2\alpha\\ \sin 2\alpha & -\cos 2\alpha\end{pmatrix}\)이다. 둘을 곱해 덧셈정리를 쓰면 \(S_\alpha S_\beta = R_{2(\alpha - \beta)}\), 곧 \(\beta\) 거울 뒤 \(\alpha\) 거울은 \(2(\alpha - \beta)\)만큼의 회전이다. 평면 기하의 표준 사실이다. \((r, c)\) 좌표에서 가로줄 거울은 \(\alpha = 90°\), 전치의 대각선은 \(45°\), 세로줄은 \(0°\), 반대각선은 \(-45°\)이고, 검증 스크립트에서 이 네 값과 무작위 각 1,000개로 확인했다. ↩