# ---------------------------------------------------------
# 안드로이드 패턴 잠금의 경우의 수를 완전탐색(백트래킹)으로 직접 세는 코드
#
# 3x3 격자, 점 번호는 아래와 같이 배치되어 있다고 가정한다.
#   1 2 3
#   4 5 6
#   7 8 9
#
# 규칙 두 가지:
#   1) 4개 이상 9개 이하의 점을 순서대로 연결한다 (한 번 쓴 점은 재사용 불가)
#   2) 두 점을 잇는 직선 위에 '아직 지나가지 않은' 점이 있으면 그 두 점을 바로 연결할 수 없다
#      (예: 1과 3 사이에는 2가 있으므로, 2를 먼저 지나지 않으면 1->3 은 불가능)
# ---------------------------------------------------------

# '건너뛰기 금지' 규칙을 딕셔너리로 정의한다: (점A, 점B) -> 그 사이에 낀 점
# 직선 위에 점이 끼는 경우는 가로 3쌍, 세로 3쌍, 대각선 2쌍 -> 총 8개 조합(양방향이라 16개 키)
SKIP = {
    (1, 3): 2, (3, 1): 2,   # 1행: 1과 3 사이에 2가 있다
    (4, 6): 5, (6, 4): 5,   # 2행: 4와 6 사이에 5가 있다
    (7, 9): 8, (9, 7): 8,   # 3행: 7과 9 사이에 8이 있다
    (1, 7): 4, (7, 1): 4,   # 1열: 1과 7 사이에 4가 있다
    (2, 8): 5, (8, 2): 5,   # 2열: 2와 8 사이에 5가 있다
    (3, 9): 6, (9, 3): 6,   # 3열: 3과 9 사이에 6이 있다
    (1, 9): 5, (9, 1): 5,   # 대각선: 1과 9 사이에 5가 있다
    (3, 7): 5, (7, 3): 5,   # 대각선: 3과 7 사이에 5가 있다
}

MIN_LEN = 4   # 패턴은 최소 4개의 점을 연결해야 유효하다
MAX_LEN = 9   # 점은 최대 9개까지 연결할 수 있다


def count_patterns_from(start_point, length_breakdown=None):
    """
    start_point에서 출발하는 모든 유효한 패턴의 개수를 백트래킹으로 센다.
    length_breakdown 딕셔너리를 넘기면, 길이(연결한 점의 개수)별 개수도 함께 누적한다.
    """
    total = 0  # 이 시작점에서 나온 유효 패턴의 총 개수를 담을 변수

    def backtrack(path, visited):
        nonlocal total
        # 지금까지 이어온 길이가 4 이상이면, 이 경로 자체가 유효한 패턴 하나로 인정된다
        if len(path) >= MIN_LEN:
            total += 1
            if length_breakdown is not None:
                length_breakdown[len(path)] = length_breakdown.get(len(path), 0) + 1

        if len(path) == MAX_LEN:
            return  # 9개를 다 썼으면 더 이어갈 점이 없으므로 종료

        last = path[-1]  # 방금 도착한 점 (다음 연결의 출발 기준)

        for candidate in range(1, 10):        # 1번부터 9번까지 다음 후보를 하나씩 검토
            if candidate in visited:
                continue                       # 이미 지나온 점은 다시 쓸 수 없다

            skip_point = SKIP.get((last, candidate))  # 두 점 사이에 낀 점이 있는지 확인
            if skip_point is not None and skip_point not in visited:
                continue                       # 낀 점을 아직 안 지났다면 이 후보는 불가능 (가지치기)

            visited.add(candidate)             # 이 후보를 방문 처리하고
            path.append(candidate)             # 경로에 추가한 뒤
            backtrack(path, visited)           # 재귀적으로 다음 단계를 탐색한다
            path.pop()                         # 되돌아올 때는 추가했던 것을 되돌리고 (백트랙)
            visited.remove(candidate)          # 방문 표시도 지운다

    backtrack([start_point], {start_point})    # 시작점 하나로 탐색을 시작한다
    return total


def unrestricted_baseline():
    """제약이 없다고 가정했을 때의 경우의 수: 9P4 + 9P5 + ... + 9P9 (합의 법칙)"""
    def nPr(n, r):
        result = 1
        for i in range(r):
            result *= (n - i)   # n부터 하나씩 줄여가며 r번 곱한다 (순열 계산)
        return result

    return sum(nPr(9, k) for k in range(MIN_LEN, MAX_LEN + 1))


if __name__ == "__main__":
    print("=" * 50)
    print("1. 제약이 없다면? (단순 순열, 9P4~9P9의 합)")
    print("=" * 50)
    baseline = unrestricted_baseline()
    print(f"결과: {baseline:,}가지\n")

    print("=" * 50)
    print("2. 실제 규칙(건너뛰기 금지)을 적용해 9곳 모두에서 직접 세기")
    print("=" * 50)
    total_all = 0
    length_totals = {}
    for start in range(1, 10):
        total_all += count_patterns_from(start, length_totals)
    print(f"길이별 경우의 수: {dict(sorted(length_totals.items()))}")
    print(f"총 경우의 수: {total_all:,}가지\n")

    print("=" * 50)
    print("3. 대칭성을 이용해 대표점 3개만 계산 후 재구성")
    print("=" * 50)
    corner_rep = count_patterns_from(1)   # 모서리 대표: 1번
    edge_rep = count_patterns_from(2)     # 변 대표: 2번
    center_rep = count_patterns_from(5)   # 중앙: 5번

    print(f"모서리(1,3,7,9) 대표값 = {corner_rep:,}  ->  ×4 = {corner_rep * 4:,}")
    print(f"변(2,4,6,8) 대표값     = {edge_rep:,}  ->  ×4 = {edge_rep * 4:,}")
    print(f"중앙(5) 값             = {center_rep:,}  ->  ×1 = {center_rep:,}")

    reconstructed = corner_rep * 4 + edge_rep * 4 + center_rep
    print(f"\n재구성한 합계: {reconstructed:,}가지")
    print(f"9곳 전체 계산 결과와 일치하는가? {reconstructed == total_all}")

    print("\n" + "=" * 50)
    print("결론 비교")
    print("=" * 50)
    print(f"제약 없을 때 (단순 순열)     : {baseline:,}가지")
    print(f"실제 규칙 적용 (정답)        : {total_all:,}가지")
    print(f"차이                         : {baseline - total_all:,}가지가 규칙 때문에 무효화됨")

Embed on website

To embed this project on your website, copy the following code and paste it into your website's HTML: