격자 회전 ↔ 선형변환 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""36.격자 회전 ↔ 선형변환 문서의 주장을 확인한다.

칸 번호는 0-based (r, c)이고, 격자는 행 n개, 열 m개다.
2 × 2 행렬은 열벡터 (r, c)에 왼쪽에서 곱한다.
"""
import math
import random
from fractions import Fraction


def check(cond, msg):
    if not cond:
        raise AssertionError(msg)


# ---------- 격자 연산 (코드 쪽) ----------
def flip_ud(a):            # 위아래 뒤집기 a[::-1]
    return [list(row) for row in a[::-1]]


def transpose(a):          # zip(*a)
    return [list(x) for x in zip(*a)]


def flip_lr(a):            # 각 행 뒤집기
    return [row[::-1] for row in a]


def cw(a):                 # zip(*a[::-1])
    return [list(x) for x in zip(*a[::-1])]


def ccw(a):                # list(zip(*a))[::-1]
    return [list(x) for x in list(zip(*a))[::-1]]


def cw_alt(a):             # 전치한 뒤 각 행 뒤집기
    return [list(row)[::-1] for row in zip(*a)]


def anti_transpose(a):     # 반대각선 거울
    return [list(r)[::-1] for r in zip(*a)][::-1]


def rand_grid(rng, n, m):
    return [[rng.randint(0, 99) for _ in range(m)] for _ in range(n)]


# ---------- 행렬 (수학 쪽) ----------
def matmul(A, B):
    return [[sum(A[i][k] * B[k][j] for k in range(len(B))) for j in range(len(B[0]))]
            for i in range(len(A))]


def mpow(A, k):
    n = len(A)
    R = [[int(i == j) for j in range(n)] for i in range(n)]
    for _ in range(k):
        R = matmul(R, A)
    return R


def det2(A):
    return A[0][0] * A[1][1] - A[0][1] * A[1][0]


def tr(A):
    return [list(x) for x in zip(*A)]


def J(k):                  # 단위행렬의 행 순서를 거꾸로 한 행렬
    return [[int(j == k - 1 - i) for j in range(k)] for i in range(k)]


def apply(M, v):
    return (M[0][0] * v[0] + M[0][1] * v[1], M[1][0] * v[0] + M[1][1] * v[1])


I2 = [[1, 0], [0, 1]]
F = [[-1, 0], [0, 1]]       # 위아래 뒤집기의 선형 부분
T = [[0, 1], [1, 0]]        # 전치의 선형 부분
L = [[1, 0], [0, -1]]       # 좌우 뒤집기의 선형 부분
AT = [[0, -1], [-1, 0]]     # 반대각선 뒤집기의 선형 부분
RCW = [[0, 1], [-1, 0]]     # R_{-90°}
RCCW = [[0, -1], [1, 0]]    # R_{90°}
NEG = [[-1, 0], [0, -1]]    # R_{180°}


def rot_float(theta):
    c, s = math.cos(theta), math.sin(theta)
    return [[c, -s], [s, c]]


def refl_float(alpha):     # 거울선의 각이 alpha인 반사 S_alpha
    c, s = math.cos(2 * alpha), math.sin(2 * alpha)
    return [[c, s], [s, -c]]


def close(A, B, eps=1e-9):
    return all(abs(A[i][j] - B[i][j]) < eps for i in range(len(A)) for j in range(len(A[0])))


def main():
    rng = random.Random(36)

    # ---------- 1. 먼저 비교해 보기: 3 × 3 표 ----------
    a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    check(flip_ud(a) == [[7, 8, 9], [4, 5, 6], [1, 2, 3]], "표: 위아래 뒤집기")
    check(transpose(a) == [[1, 4, 7], [2, 5, 8], [3, 6, 9]], "표: 전치")
    check(cw(a) == [[7, 4, 1], [8, 5, 2], [9, 6, 3]], "표: 시계 방향")
    check(ccw(a) == [[3, 6, 9], [2, 5, 8], [1, 4, 7]], "표: 반시계 방향")
    check(cw(cw(cw(cw(a)))) == a, "표: 시계 방향 네 번")
    check(mpow(RCW, 4) == I2, "표: R^4 = I")
    # 표 오른쪽 칸: n = 3에서 행렬 + 평행이동이 값을 옮기는 자리와 같다
    table = [(flip_ud, F, (2, 0)), (transpose, T, (0, 0)), (cw, RCW, (0, 2)), (ccw, RCCW, (2, 0))]
    for op, M, b in table:
        out = op(a)
        for r in range(3):
            for c in range(3):
                x, y = apply(M, (r, c))
                check(out[x + b[0]][y + b[1]] == a[r][c], "표: 행렬 + 평행이동")

    # ---------- 2. 대응 관계: 칸 번호 식 (직사각형 n × m) ----------
    formulas = {
        "ud": (flip_ud, lambda r, c, n, m: (n - 1 - r, c)),
        "tr": (transpose, lambda r, c, n, m: (c, r)),
        "cw": (cw, lambda r, c, n, m: (c, n - 1 - r)),
        "ccw": (ccw, lambda r, c, n, m: (m - 1 - c, r)),
        "lr": (flip_lr, lambda r, c, n, m: (r, m - 1 - c)),
        "180": (lambda g: flip_lr(flip_ud(g)), lambda r, c, n, m: (n - 1 - r, m - 1 - c)),
        "anti": (anti_transpose, lambda r, c, n, m: (m - 1 - c, n - 1 - r)),
    }
    linear = {"ud": F, "tr": T, "cw": RCW, "ccw": RCCW, "lr": L, "180": NEG, "anti": AT}
    shift = {"ud": lambda n, m: (n - 1, 0), "tr": lambda n, m: (0, 0), "cw": lambda n, m: (0, n - 1),
             "ccw": lambda n, m: (m - 1, 0), "lr": lambda n, m: (0, m - 1),
             "180": lambda n, m: (n - 1, m - 1), "anti": lambda n, m: (m - 1, n - 1)}
    for _ in range(500):
        n, m = rng.randint(1, 7), rng.randint(1, 7)
        g = rand_grid(rng, n, m)
        for key, (op, f) in formulas.items():
            out = op(g)
            for r in range(n):
                for c in range(m):
                    x, y = f(r, c, n, m)
                    check(out[x][y] == g[r][c], f"칸 번호 식 {key}")
                    lx, ly = apply(linear[key], (r, c))
                    sx, sy = shift[key](n, m)
                    check((lx + sx, ly + sy) == (x, y), f"선형 부분 + 평행이동 {key}")
            # 평행이동 = 가장 작은 번호가 0이 되게 고른 값
            imgs = [apply(linear[key], (r, c)) for r in range(n) for c in range(m)]
            check((-min(p[0] for p in imgs), -min(p[1] for p in imgs)) == shift[key](n, m),
                  f"평행이동은 최솟값을 0으로 {key}")
        # 1부터 세어도 평행이동은 남는다: 시계 방향은 (r, c) → (c, n + 1 - r)
        out = cw(g)
        for r in range(1, n + 1):
            for c in range(1, m + 1):
                x, y = c, n + 1 - r
                check(out[x - 1][y - 1] == g[r - 1][c - 1], "1-based 시계 방향 식")
                lx, ly = apply(RCW, (r, c))
                check((x - lx, y - ly) == (0, n + 1) != (0, 0), "1-based에서도 평행이동 (0, n + 1)")
    # 반사 두 번 = 회전 (선형 부분의 곱)
    check(matmul(T, F) == RCW, "T·F = R_-90")
    check(matmul(F, T) == RCCW, "F·T = R_90")
    check(RCW == [[round(v) for v in row] for row in rot_float(-math.pi / 2)], "R_-90 성분")
    check(RCCW == [[round(v) for v in row] for row in rot_float(math.pi / 2)], "R_90 성분")
    check(matmul(F, L) == NEG and matmul(L, F) == NEG, "위아래·좌우 = 180°")
    check(matmul(F, F) == I2 and matmul(T, T) == I2, "같은 반사 두 번 = 제자리")

    # ---------- 3. 값의 행렬 A로 보기 (06) ----------
    for _ in range(500):
        n, m = rng.randint(1, 7), rng.randint(1, 7)
        A = rand_grid(rng, n, m)
        Jn, Jm = J(n), J(m)
        check(flip_ud(A) == matmul(Jn, A), "a[::-1] = J_n A")
        check(flip_lr(A) == matmul(A, Jm), "행마다 뒤집기 = A J_m")
        check(transpose(A) == tr(A), "zip(*a) = A^T")
        check(cw(A) == tr(matmul(Jn, A)) == matmul(tr(A), Jn), "cw = (J_n A)^T = A^T J_n")
        check(cw_alt(A) == cw(A), "전치한 뒤 각 행 뒤집기 = 시계 방향")
        check(ccw(A) == matmul(Jm, tr(A)) == tr(matmul(A, Jm)), "ccw = J_m A^T = (A J_m)^T")
        check(flip_lr(flip_ud(A)) == matmul(matmul(Jn, A), Jm), "180° = J_n A J_m")
        check(tr(Jn) == Jn and matmul(Jn, Jn) == [[int(i == j) for j in range(n)] for i in range(n)],
              "J는 대칭이고 J^2 = I")
        check(ccw(cw(A)) == A and cw(ccw(A)) == A, "반시계는 시계 방향을 되돌린다")
        B = matmul(tr(A), Jn)                     # 시계 방향으로 돌린 m × n 격자
        check(matmul(Jn, tr(B)) == matmul(matmul(Jn, Jn), A) == A, "J_n (A^T J_n)^T = J_n^2 A = A")
        check(ccw(B) == matmul(Jn, tr(B)), "m × n 격자의 반시계는 J_n B^T")
        check([list(row[::-1]) for row in zip(*A)] == cw(A), "[row[::-1] for row in zip(*a)] = 시계 방향")
        check(flip_ud(flip_ud(A)) == A and transpose(transpose(A)) == A, "반사는 두 번이면 제자리")
    # 06 C1의 B는 J_2: 왼쪽 곱은 행, 오른쪽 곱은 열을 바꾼다
    A2 = [[1, 2], [3, 4]]
    check(J(2) == [[0, 1], [1, 0]], "J_2 = 06 C1의 B")
    check(matmul(J(2), A2) == [[3, 4], [1, 2]] and matmul(A2, J(2)) == [[2, 1], [4, 3]], "06 C1")
    # 역의 순서: (TF)^-1 = F^-1 T^-1 = FT
    check(matmul(matmul(T, F), matmul(F, T)) == I2, "FT·TF = I")
    check(matmul(matmul(F, T), matmul(T, F)) == I2, "TF·FT = I")
    check(matmul(T, F) != matmul(F, T), "TF ≠ FT")

    # ---------- 4. 방향 (dr, dc): 평행이동이 지워진다 ----------
    d = (-1, 0)                                   # 위
    seq = []
    for _ in range(4):
        d = (d[1], -d[0])                         # 시계 방향 (dc, -dr)
        seq.append(d)
    check(seq == [(0, 1), (1, 0), (0, -1), (-1, 0)], "위 → 오른쪽 → 아래 → 왼쪽 → 위")
    for _ in range(300):
        n, m = rng.randint(1, 8), rng.randint(1, 8)
        r1, c1, r2, c2 = rng.randrange(n), rng.randrange(m), rng.randrange(n), rng.randrange(m)
        p = (c1, n - 1 - r1)
        q = (c2, n - 1 - r2)
        diff = (q[0] - p[0], q[1] - p[1])
        check(diff == apply(RCW, (r2 - r1, c2 - c1)), "차이는 선형 부분만 받는다")
        check(apply(RCW, (r2 - r1, c2 - c1)) == (c2 - c1, -(r2 - r1)), "(dr, dc) → (dc, -dr)")

    # ---------- 5. 어디까지 같은가 ----------
    # (a) 가운데를 원점으로 잡으면 평행이동이 사라진다
    for n in range(1, 7):
        for m in range(1, 7):
            for r in range(n):
                for c in range(m):
                    u = (Fraction(r) - Fraction(n - 1, 2), Fraction(c) - Fraction(m - 1, 2))
                    x, y = c, n - 1 - r                    # cw 뒤의 칸 (m × n 격자)
                    v = (Fraction(x) - Fraction(m - 1, 2), Fraction(y) - Fraction(n - 1, 2))
                    check(v == apply(RCW, u), "가운데 원점에서 cw = R_-90")
    # 3 × 3: (r - 1, c - 1)
    for r in range(3):
        for c in range(3):
            x, y = c, 2 - r
            check((x - 1, y - 1) == apply(RCW, (r - 1, c - 1)), "3 × 3 가운데 원점")
    # (b) 45°는 칸을 칸으로 보내지 않는다: 가운데에서 (0, 1) 떨어진 칸
    R45 = rot_float(math.pi / 4)
    v = (R45[0][0] * 0 + R45[0][1] * 1, R45[1][0] * 0 + R45[1][1] * 1)
    check(all(abs(2 * t - round(2 * t)) > 0.1 for t in v), "45°: 칸 가운데로 가지 않음")
    check(abs(abs(v[0]) - math.sqrt(2) / 2) < 1e-12 and abs(abs(v[1]) - math.sqrt(2) / 2) < 1e-12, "45°: 0.707")
    # (c) 직사각형: 모양을 지키는 대칭은 넷
    g = rand_grid(rng, 2, 3)
    keep = [k for k, (op, _) in formulas.items() if len(op(g)) == 2 and len(op(g)[0]) == 3]
    swap = [k for k, (op, _) in formulas.items() if len(op(g)) == 3 and len(op(g)[0]) == 2]
    check(sorted(keep) == sorted(["ud", "lr", "180"]), "2 × 3: 모양 유지 (그대로 제외)")
    check(sorted(swap) == sorted(["tr", "anti", "cw", "ccw"]), "2 × 3: 모양이 3 × 2로")
    # 시계 방향 네 번: 평행이동이 n - 1, m - 1로 번갈아 붙고 합치면 제자리
    for _ in range(200):
        n, m = rng.randint(1, 7), rng.randint(1, 7)
        g = rand_grid(rng, n, m)
        check(cw(cw(cw(cw(g)))) == g, "직사각형 cw 네 번 = 제자리")
        dims, h = [], g
        for _ in range(4):
            dims.append(len(h) - 1)               # 이번 단계의 평행이동 (행 수 - 1)
            h = cw(h)
        check(dims == [n - 1, m - 1, n - 1, m - 1], "평행이동이 번갈아 붙음")

    def H(k):                                     # 동차 좌표의 시계 방향 회전 (행 수 k)
        return [[0, 1, 0], [-1, 0, k - 1], [0, 0, 1]]
    I3 = [[int(i == j) for j in range(3)] for i in range(3)]
    for n in range(1, 8):
        check(mpow(H(n), 4) == I3, "정사각: H^4 = I")
        for m in range(1, 8):
            check(matmul(H(m), matmul(H(n), matmul(H(m), H(n)))) == I3, "직사각: 번갈아 곱하면 I")
            if n != m:
                check(matmul(H(n), H(n)) != matmul(H(m), H(n)), "직사각: 같은 함수의 제곱이 아님")
        for r in range(n):
            for c in range(n):
                out = matmul(H(n), [[r], [c], [1]])
                check([row[0] for row in out] == [c, n - 1 - r, 1], "H (r, c, 1) = (c, n - 1 - r, 1)")
    # (d) 길이가 다른 줄: zip은 짧은 줄에 맞춰 자른다
    check(list(zip(*[[1, 2, 3], [4, 5]])) == [(1, 4), (2, 5)], "들쭉날쭉한 리스트")
    # (e) (r, c) 좌표계 = 교과서 (x, y)를 90° 돌린 것: (r, c) = (-y, x)
    C = [[0, -1], [1, 0]]                         # (x, y) → (r, c)
    check(C == RCCW and det2(C) == 1, "(r, c) = R_90 (x, y)")
    Cinv = [[0, 1], [-1, 0]]
    check(matmul(C, Cinv) == I2, "C 역행렬")
    for M in [RCW, RCCW, F, T]:
        conj = matmul(Cinv, matmul(M, C))         # 같은 변환을 (x, y)로 적은 행렬
        check(det2(conj) == det2(M), "좌표계를 돌려도 행렬식 같음")
    check(matmul(Cinv, matmul(RCW, C)) == RCW, "시계 방향은 (x, y)에서도 R_-90")
    # 전치의 거울선: (r, c)에서 (1, 1) 방향 = 화면의 왼쪽 위 → 오른쪽 아래
    check(apply(T, (1, 1)) == (1, 1), "전치의 거울 방향 (1, 1)")
    r_, c_ = 1, 1
    x_, y_ = c_, -r_                               # 화면 좌표: 오른쪽 +x, 위 +y
    check(x_ > 0 and y_ < 0, "(1, 1)은 화면에서 오른쪽 아래")
    check(apply(matmul(Cinv, matmul(T, C)), (1, -1)) == (1, -1), "(x, y) 그림에서 거울은 y = -x")
    # (x, y) = (c, r)로 적으면 이름이 뒤바뀐다
    P = [[0, 1], [1, 0]]
    check(matmul(P, matmul(RCW, P)) == RCCW, "P R_-90 P = R_90")
    for M in [RCW, RCCW, F, T, L, AT, NEG, I2]:
        check(det2(matmul(P, matmul(M, P))) == det2(M), "순서를 바꿔도 행렬식 같음")

    # ---------- 6. 이 연결로 얻는 것 ----------
    # 8가지 대칭 = 부호 붙은 치환 행렬 8개, 행렬식 +1 넷과 -1 넷
    group = {tuple(map(tuple, I2))}
    frontier = [I2]
    while frontier:
        nxt = []
        for M in frontier:
            for G in (F, T):
                P2 = matmul(G, M)
                key = tuple(map(tuple, P2))
                if key not in group:
                    group.add(key)
                    nxt.append(P2)
        frontier = nxt
    check(len(group) == 8, "F와 T로 만든 대칭은 8개")
    signed_perm = set()
    for (i, j) in [(0, 1), (1, 0)]:
        for s1 in (1, -1):
            for s2 in (1, -1):
                M = [[0, 0], [0, 0]]
                M[0][i], M[1][j] = s1, s2
                signed_perm.add(tuple(map(tuple, M)))
    check(group == signed_perm, "8개 = 부호 붙은 치환 행렬")
    dets = sorted(det2([list(r) for r in M]) for M in group)
    check(dets == [-1] * 4 + [1] * 4, "행렬식 +1 넷, -1 넷")
    rot = {tuple(map(tuple, mpow(RCW, k))) for k in range(4)}
    check(rot == {tuple(map(tuple, M)) for M in [I2, RCW, NEG, RCCW]}, "회전 넷")
    check(all(det2([list(r) for r in M]) == 1 for M in rot), "회전의 행렬식 +1")
    check(all(det2(M) == -1 for M in [F, T, L, AT]), "반사의 행렬식 -1")
    # 회전을 몇 번 이어도 +1: 회전끼리의 곱은 회전 안에 머문다
    for M1 in rot:
        for M2 in rot:
            check(tuple(map(tuple, matmul([list(r) for r in M1], [list(r) for r in M2]))) in rot, "회전끼리 닫힘")
    # 자물쇠와 열쇠: 돌려서는 거울상 열쇠가 나오지 않는다
    key = [[1, 1, 0], [0, 1, 0], [0, 1, 1]]       # S자 모양
    rots, h = [], key
    for _ in range(4):
        h = cw(h)
        rots.append(h)
    check(flip_lr(key) not in rots, "S자 열쇠를 돌려서 Z자가 나오지 않음")
    # 돌려서 거울상이 나온다 ⇔ 열쇠가 네 거울 중 하나에 대해 대칭
    REFLS = [flip_ud, flip_lr, transpose, anti_transpose]

    def rotations(g):
        out, h = [], g
        for _ in range(4):
            h = cw(h)
            out.append(h)
        return out

    def mirror_symmetric(g):
        return any(S(g) == g for S in REFLS)

    n_sym = n_asym = 0
    for k in (1, 2, 3):
        for bits in range(2 ** (k * k)):
            g = [[(bits >> (r * k + c)) & 1 for c in range(k)] for r in range(k)]
            check((flip_lr(g) in rotations(g)) == mirror_symmetric(g), "거울상이 나옴 ⇔ 거울 대칭 (전수)")
            if mirror_symmetric(g):
                n_sym += 1
            else:
                n_asym += 1
    check(n_sym > 0 and n_asym > 0, "대칭·비대칭 열쇠가 모두 있음")
    for _ in range(2000):
        k = rng.randint(1, 5)
        g = [[rng.randint(0, 1) for _ in range(k)] for _ in range(k)]
        check((flip_lr(g) in rotations(g)) == mirror_symmetric(g), "거울상이 나옴 ⇔ 거울 대칭 (무작위)")
    diag_key = [[1, 1, 0], [1, 0, 0], [0, 0, 0]]
    check(transpose(diag_key) == diag_key, "예: 대각선 대칭 열쇠")
    check(flip_lr(diag_key) == cw(diag_key) and flip_lr(diag_key) != diag_key, "예: 좌우 뒤집기 = 시계 방향 한 번")
    # 반시계 = 시계 세 번 = 시계의 역: 네 방향의 집합이 같다 (직사각형도)
    check(mpow(RCW, 3) == RCCW and matmul(RCW, RCCW) == I2 and mpow(RCW, 2) == NEG, "R_90 = R_-90^3")
    for n in range(1, 7):
        for m in range(1, 7):
            for _ in range(15):
                g = rand_grid(rng, n, m)
                check(ccw(g) == cw(cw(cw(g))), "ccw = cw^3")
                s_cw, s_ccw, h1, h2 = [], [], g, g
                for _ in range(4):
                    h1, h2 = cw(h1), ccw(h2)
                    s_cw.append(h1)
                    s_ccw.append(h2)
                check(sorted(map(str, s_cw)) == sorted(map(str, s_ccw)), "네 방향 집합이 같음")
    # 반사 두 번 = 거울 사이 각의 두 배만큼 회전: S_a S_b = R_{2(a - b)}
    for _ in range(1000):
        al, be = rng.uniform(-4, 4), rng.uniform(-4, 4)
        check(close(matmul(refl_float(al), refl_float(be)), rot_float(2 * (al - be))), "S_a S_b = R_2(a-b)")
    deg = math.pi / 180
    rounded = lambda M: [[round(v) for v in row] for row in M]
    check(rounded(refl_float(90 * deg)) == F, "가로 거울 = 각 90°")
    check(rounded(refl_float(45 * deg)) == T, "대각선 거울 = 각 45°")
    check(rounded(refl_float(0)) == L, "세로 거울 = 각 0°")
    check(rounded(refl_float(-45 * deg)) == AT, "반대각선 거울 = 각 -45°")
    check(rounded(matmul(refl_float(45 * deg), refl_float(90 * deg))) == RCW, "가로 → 대각선: 시계 90°")
    check(rounded(matmul(refl_float(90 * deg), refl_float(0))) == NEG, "세로 → 가로: 180°")

    # ---------- 7. 전이 문제: 좌우 반전 뒤 시계 방향 ----------
    check(matmul(RCW, L) == AT, "선형 부분 R_-90 · diag(1, -1) = 반대각선 반사")
    check(matmul(AT, L) == RCW, "시계 방향 = 세로 거울 뒤 반대각선 거울")
    check(rounded(matmul(refl_float(-45 * deg), refl_float(0))) == RCW, "각 0° → -45°: 시계 90°")
    for _ in range(500):
        n, m = rng.randint(1, 7), rng.randint(1, 7)
        g = rand_grid(rng, n, m)
        out = cw(flip_lr(g))
        check(out == anti_transpose(g), "좌우 반전 뒤 시계 = 반대각선 뒤집기")
        check(len(out) == m and (m == 0 or len(out[0]) == n), "결과는 m × n")
        check(cw(g) == anti_transpose(flip_lr(g)), "cw = 좌우 반전 뒤 반대각선 뒤집기")
        check(flip_lr(flip_lr(g)) == g, "좌우 반전 두 번 = 제자리")
        check(anti_transpose(out) == g, "반대각선 뒤집기 두 번 = 제자리")
        check(flip_lr(ccw(out)) == g, "반시계 뒤 좌우 반전으로 되돌림")
    photo = [[1, 2, 3], [4, 5, 6]]
    edited = cw(flip_lr(photo))
    check(edited == [[6, 3], [5, 2], [4, 1]], "전이 문제 2 × 3 예")
    h, seen = edited, []
    for _ in range(4):
        h = cw(h)
        seen.append(h)
    check(photo not in seen, "회전만으로는 되돌릴 수 없음")
    check(tuple(map(tuple, AT)) not in rot, "반대각선 반사는 회전이 아님")
    # 회전만으로 되돌릴 수 있다 ⇔ 원래 사진이 거울 대칭 (직사각형 포함)
    for _ in range(4000):
        n, m = rng.randint(1, 4), rng.randint(1, 4)
        g = [[rng.randint(0, 1) for _ in range(m)] for _ in range(n)]
        check((g in rotations(cw(flip_lr(g)))) == mirror_symmetric(g), "회전으로 되돌림 ⇔ 거울 대칭 사진")
    sym_photo = [[1, 2, 1], [3, 4, 3]]            # 좌우 대칭
    check(flip_lr(sym_photo) == sym_photo, "좌우 대칭 사진")
    e = cw(flip_lr(sym_photo))
    check(ccw(e) == sym_photo and cw(cw(cw(e))) == sym_photo, "대칭 사진은 반시계 한 번(시계 세 번)으로 돌아옴")

    # ---------- 8. 확인 문제 ----------
    # C1: 순서를 바꾸면 역회전 (위 3절에서 FT·TF = I 확인)
    # C2: 위아래 뒤 좌우 = 180°
    g = [[1, 2], [3, 4]]
    check(flip_ud(g) == [[3, 4], [1, 2]], "C2 위아래")
    check(flip_lr(flip_ud(g)) == [[4, 3], [2, 1]], "C2 좌우")
    check(flip_lr(flip_ud(g)) == cw(cw(g)) and flip_lr(flip_ud(g)) != g, "C2 = 180°, 제자리 아님")
    check(det2(matmul(L, F)) == 1, "C2 행렬식 +1")
    # C3: 2 × 3 격자 반시계
    b = [[1, 2, 3], [4, 5, 6]]
    out = ccw(b)
    check(out == [[3, 6], [2, 5], [1, 4]], "C3 결과")
    check(list(zip(*b))[::-1] == [(3, 6), (2, 5), (1, 4)], "C3: 튜플 그대로의 출력")
    check(out[0][0] == 3 and out[2][1] == 4, "C3: 3은 (0, 0), 4는 (2, 1)")
    check(formulas["ccw"][1](0, 2, 2, 3) == (0, 0) and formulas["ccw"][1](1, 0, 2, 3) == (2, 1), "C3 식")
    wrong = 0
    for r in range(2):
        for c in range(3):
            x, y = 2 - 1 - c, r                   # 틀린 식 (n - 1 - c, r)
            if not (0 <= x < 3 and 0 <= y < 2) or out[x][y] != b[r][c]:
                wrong += 1
    check(wrong > 0, "C3: n - 1 - c는 직사각형에서 틀림")
    bad_total = 0
    for _ in range(300):
        n, m = rng.randint(1, 7), rng.randint(1, 7)
        if n == m:
            continue
        g = rand_grid(rng, n, m)
        o = ccw(g)
        for r in range(n):
            for c in range(m):
                x, y = n - 1 - c, r
                if not (0 <= x < m and 0 <= y < n) or o[x][y] != g[r][c]:
                    bad_total += 1
    check(bad_total > 0, "n - 1 - c 식은 직사각형에서 깨짐")

    print("ALL CHECKS PASSED")


if __name__ == "__main__":
    main()