칠자각득을 확장해 보자

by gg582 · 2026-07-08 06:08:46 · 71 views

七子開連(칠자개련): 칠자각득의 3차원 확장

칠자각득(七子各得)은 5개 방향에 7개 숫자씩, 각 방향 합이 120이 되는 한국 산학 퍼즐이다. 이 글은 그것을 7개 방향으로 확장하고, 다시 5개의 확장체를 묶어 3차원 구면 위에 배치하는 **七子開連(칠자개련)**을 제안한다.

HTML 다운로드 chilja_1.png cj.png


1. 칠자각득의 구조 복습

칠자각득은 이름 그대로 "일곱(七) 개씩 각(各) 얻는(得)" 구조이다.

  • 5개 방향: 상(上)·하(下)·좌(左)·우(右)·중(中)
  • 각 방향 7개 숫자: 중심 1개 + 주변 6개
  • 총 숫자: 35개 (1~35)
  • 불변량: 각 방향의 7개 숫자 합 = 120
  • 중심 숫자: 1, 2, 3, 4, 5 (mod 5 완전 커버)

이 구조의 핵심은 **"방향별 합 불변량"**이다. 스도쿠가 "각 행·열·박스에 1~9가 한 번씩"인 것처럼, 칠자각득은 "각 방향 합이 120"이라는 단일 조건으로 퍼즐의 완결성을 보장한다.


2. 왜 7개 방향인가

칠자각득은 평면의 5방향(상하좌우중)을 사용한다. 그런데 **칠자(七子)**라는 이름을 그대로 유지하면서 방향을 늘리면 어떨까?

5방향에서 7방향으로 가는 것은 단순한 숫자 늘리기가 아니다. 7은 구면 위에서 균등 분포가 가능한 최소 점 수와 관련이 있다. 정이십면체(icosahedron)의 정점은 12개이지만, 그중 상반부 7개를 선택하면 북극을 중심으로 한 균등한 7방향 배치가 가능하다.

또한 7은 소수이기 때문에 mod 7 컬러링이나 군론적 해석에도 적합하다. 5방향의 칠자각득이 mod 5를 쓴다면, 7방향 확장은 자연스럽게 mod 7로 이어진다.


3. 七子開連(칠자개련)의 정의

3.1 이름의 의미

이름은 동아시아 근세 수학에서 유래한 퍼즐인만큼 그 당시의 학술어를 패러디해서 작성하였다.

  • 七子(칠자): "일곱 개씩" — 각 클러스터의 크기 유지
  • 開連(개련): "열어 연결한다" — 확장과 연결

"각득(各得)"에서 "개련(開連)"으로 바뀐 것은 단순한 명명의 변화가 아니다. "얻는다"는 개별 행위에서 "연결한다"는 상호 작용으로 패러다임이 전환된 것이다. 각 클러스터가 독립적으로 존재하는 것이 아니라, 상위 구조와 연결되어 하나의 유기체를 이룬다.

3.2 파라미터 정의

Π(7, 6, 175)로 정의한다:

파라미터 의미
p 7 클러스터(방향)의 수
q 6 각 클러스터의 주변 슬롯 수
T 175 각 클러스터의 목표 합

칠자각득은 Π(5, 6, 120)이므로, 七子開連은 p만 5→7로, T만 120→175로 변경한 확장이다. q=6은 유지한다.

3.3 숫자 범위와 합 조건

  • 총 숫자: 7개 클러스터 × 7개 숫자 = 49개
  • 숫자 범위: 1 ~ 49 (중첩 불허)
  • 1~49 합: 49 × 50 / 2 = 1225
  • 각 클러스터 합: 1225 / 7 = 175 (정수로 떨어짐 ✓)
  • 중심 숫자: 1, 2, 3, 4, 5, 6, 7 (mod 7 완전 커버)
  • 주변 6개 합: 175 - 중심값
중심 주변 합 주변 개수
1 174 6
2 173 6
3 172 6
4 171 6
5 170 6
6 169 6
7 168 6

주변 숫자는 8~49 (42개)에서 선택한다. 각 중심별로 6개씩, 총 42개를 정확히 소진해야 한다.


4. 3차원 구면 배치

4.1 7방향 벡터

평면의 상하좌우중은 직관적이지만, 7방향은 3차원 구면으로 가야 한다. 7개 점을 구면 위에 균등 배치하는 방법은 여러 가지가 있지만, 여기서는 북극 + 위도고 6개 방식을 사용한다.

  • 방향 0: 북극 (0, 0, 1)
  • 방향 1~6: 위도 60°, 경도 0°, 60°, 120°, 180°, 240°, 300°

이 배치는 **삼각비례점(triangular antiprism)**의 상단에 가깝다. 중심(방향 0)이 북극을 향하고, 나머지 6개가 그 주위를 둘러싼다. 이는 칠자각득의 "중(中)"이 다른 4방향과 구별되었던 것과 같은 계층적 구조를 3D로 확장한 것이다.

4.2 상위 구조: 5개의 七子開連

이제 한 단계 더 나아간다. 5개의 七子開連를 묶어 더 큰 구조를 만든다.

  • 총 숫자: 5 × 49 = 245개 (1~245, 중첩 불허)
  • 각 七子開連: 독립된 숫자 범위 (1~49, 50~98, 99~147, 148~196, 197~245)
  • 상위 클러스터 수: 5개
  • 상위 합: 1~245 합 = 245 × 246 / 2 = 30135 → 30135 / 5 = 6027 (정수로 떨어짐 ✓)

상위 5개 七子開連 역시 구면 위에 배치한다. 5개 점의 균등 분포는 **삼각이십면체(icosahedron)**의 정점 중 상반부 5개를 선택하는 방식으로 구현한다.

4.3 좌표 계산

각 숫자의 3D 좌표는 2단계 계층으로 결정된다:

  1. 슈퍼 클러스터 위치: 상위 5개 방향 벡터 중 하나를 선택
  2. 클러스터 위치: 해당 슈퍼 클러스터 내부의 7개 방향 벡터 중 하나를 선택
  3. 최종 좌표: 슈퍼 방향 × 2 + 클러스터 방향 × 0.5, 정규화 후 반경 2.5

주변 숫자는 중심 좌표 주위로 구면 파이(polar) 좌표로 살짝 퍼지게 배치한다. 각 주변 숫자는 중심을 중심으로 한 작은 구 위에 위치하며, 6개가 60° 간격으로 배치된다.


5. 연결 구조

5.1 클러스터 내부 연결 (파란 선)

각 중심 숫자는 자신의 6개 주변 숫자와 직접 연결된다. 이는 칠자각득의 "중심-주변" 관계를 그래프로 표현한 것이다.

  • 총 연결: 7개 중심 × 6개 주변 = 42개 (슈퍼 클러스터당)
  • 전체: 5 × 42 = 210개

5.2 슈퍼 클러스터 내부 연결 (주황 선)

각 슈퍼 클러스터 내부의 7개 중심은 **완전그래프(K₇)**로 연결된다. 이는 "이 7개 중심이 하나의 슈퍼 클러스터를 이룬다"는 시각적 강조이다.

  • 총 연결: C(7, 2) = 21개 (슈퍼 클러스터당)
  • 전체: 5 × 21 = 105개

5.3 전체 연결

  • 총 노드: 245개
  • 총 엣지: 210 + 105 = 315개

6. 생성 알고리즘

6.1 핵심 문제

주변 42개 숫자(8~49)를 7개 중심에 각 6개씩 배분해야 한다. 각 중심의 주변 6개 합이 175 - 중심값이 되어야 한다.

이는 정수 분할 + 조합 선택 문제이다. 42개 숫자 중 6개를 고르는 조합은 C(42, 6) ≈ 5,245,786개. 7개 중심에 대해 순차적으로 선택하면, 최악의 경우 5백만⁷이라는 천문학적 수가 나온다.

6.2 최적화 기법

가지치기(Pruning):

  • 각 단계에서 남은 숫자로 목표 합을 만들 수 없으면 즉시 백트래킹
  • 최소/최대 가능 합으로 빠른 거르기

Gosper's Hack:

  • 6-bit combination enumeration을 비트마스크로 수행
  • __builtin_popcountll로 정확히 6개 선택 확인
  • 다음 같은 popcount의 비트마스크를 O(1)에 생성

SIMD 가속 (C/AVX2):

  • 6개 숫자의 합을 _mm256_hadd_epi32로 한 번에 계산
  • Horizontal add 2회로 8→4→2→1 축소

캐싱:

  • (available, target, k) 조합을 메모이제이션
  • 동일한 하위 문제 반복 계산 방지

6.3 실제 생성 결과

슈퍼 클러스터 1 (1~49)의 예:

중심 주변 숫자
1 8, 9, 13, 47, 48, 49 175
2 10, 11, 17, 44, 45, 46 175
3 12, 14, 20, 41, 42, 43 175
4 15, 16, 23, 38, 39, 40 175
5 18, 19, 25, 35, 36, 37 175
6 21, 22, 27, 32, 33, 34 175
7 24, 26, 28, 29, 30, 31 175

모든 중심의 주변 합이 정확히 175이며, 8~49를 중복 없이 완전히 소진한다.


7. 검증

7.1 클러스터 수준

  • 각 클러스터 합 = 175 ✓
  • 중심 숫자 1~7, 각각 한 번씩 ✓
  • 주변 숫자 8~49, 중복 없이 소진 ✓

7.2 슈퍼 클러스터 수준

  • 각 슈퍼 클러스터 합 = 6027 ✓
  • 5개 슈퍼 클러스터: 1~49, 50~98, 99~147, 148~196, 197~245 ✓

7.3 전체 수준

  • 1~245 중복 없이 완전 사용 ✓
  • 총 합 30135 = 5 × 6027 = 35 × 175 ✓

8. Π(p, q, T) 패밀리로의 편입

기존 제안했던 Π(p, q, T) 파라미터화에 따르면:

퍼즐 Π 표기 특징
칠자각득 Π(5, 6, 120) 원형, 평면, 5방향
사오도 Π(5, 3, T_d) 불균일, 5색 그룹
七子開連 Π(7, 6, 175) 확장, 3D, 7방향
七子開連×5 Π(5, 49, 6027) 메타 구조, 상위 5개

七子開連는 Π(7, 6, 175)로, 칠자각득 Π(5, 6, 120)의 직계 확장이다. p만 5→7로 늘리고, T는 120→175로 자동 조정되었다. q=6은 유지되어 "6개 주변 슬롯"이라는 구조적 상수가 보존된다.

상위 5개 구조는 Π(5, 49, 6027)로, 여기서 "49"는 하나의 七子開連 전체를 하나의 "슈퍼 숫자"로 본 것이다. 즉 퍼즐의 퍼즐이다.


9. 시각화

3D 구면 위에 배치된 七子開連는 마우스 드래그로 회전하고, 스크롤로 줌할 수 있다.

  • 흰색 점: 중심 숫자 (1, 2, 3...)
  • 색상 점: 주변 숫자 (슈퍼 클러스터별 구분: 빨강/초록/파랑/노랑/보라)
  • 파란 선: 클러스터 내부 연결 (중심-주변)
  • 주황 선: 슈퍼 클러스터 내부 중심 연결 (완전그래프)

회전하면 5개 슈퍼 클러스터가 구면 위에 떠 있는 것을 볼 수 있다. 각 슈퍼 클러스터는 7개 중심으로 별 모양을 이루고, 그 주변에 6개 주변 숫자가 궤도를 돈다.


10. 확장 가능성

10.1 더 큰 p

Π(11, 6, T)나 Π(13, 6, T)로 갈 수 있을까? 11방향이나 13방향의 구면 균등 분포는 더 복잡해지지만, **버킹엄 다면체(Buckminsterfullerene)**의 정점 배치(60개)에서 영감을 받을 수 있다.

10.2 q 변화

Π(7, 4, T)나 Π(7, 8, T)는 주변 슬롯 수를 변경한다. q=4는 "사오도식" 확장이고, q=8은 더 복잡한 주변 구조를 만든다.

10.3 메타-메타 구조

Π(5, 49, 6027)를 다시 한 번 묶어 Π(7, 245, T₃)로 갈 수 있을까? 이론적으로는 가능하지만, 숫자 범위가 1~1715로 늘어나고 기하적 배치가 4차원으로 가야 할 수도 있다.


11. 왜 이것이 퍼즐인가

七子開連는 "풀어야 할" 문제인가, 아니면 "이미 완성된 도식"인가?

두 가지 모두다.

  • 이미 완성된 도식으로: 245개 숫자가 배치되어 있고, 모든 합 조건이 만족된다. 이것만으로도 하나의 수학적 오브제다.
  • 풀어야 할 문제로: "1~245를 35개 클러스터(각 7개)로 나누되, 각 클러스터 합이 175이 되도록 배치하라"는 문제로도 볼 수 있다. 이건 조합 최적화 문제이다.
  • 디자인 문제로: "Π(7, 6, 175)가 존재하는가? 존재한다면 해법은 유일한가?" 하는 질문은 퍼즐 디자인 이론의 핵심이다.

12. 산학 전통과의 연결

12.1 명명법

七子開連는 산학 서적의 명명 관례를 따른다.

  • 수(數): 七 — 숫자의 개수
  • 자(子): 개(個)의 의미, 단위
  • 동사 구조: 開連(개련) — "열어 연결한다"

구수략의 **지수귀문도(地數龜文圖)**도 "지수(地數) + 귀문(龜文) + 도(圖)" 구조다. 사오도(四吾道)도 "사(四) + 오(吾) + 도(道)"다. 七子開連은 이 전통을 그대로 이어 받는다.

12.2 오행과의 거리

사오도는 오행(五行)과 직접 연결되지만, 七子開連는 그렇지 않다. 7방향은 오행의 5요소와 맞지 않는다. 이건 의도적 탈동아시아화가 아니라, 구조적 확장의 논리적 결과다. 5→7은 p의 변화이지, 문화적 의미의 변화는 아니다.

다만 mod 7 컬러링을 도입하면, 7개 중심 숫자를 7개 색으로 분류하는 새로운 시각화가 가능하다. 이건 현대적 해석이지, 고대 의도의 재현은 아니다.


13. 구현

Python, Rust, Go, C+SIMD로 구현했다. 모두 동일한 고정 초기 조건(1~245, 중첩 불허, T1=175)에서 동일한 결과를 생성한다.

최적화 포인트:

  • Gosper's hack으로 combination enumeration을 O(1)에
  • AVX2 _mm256_hadd_epi32로 6개 숫자 합을 SIMD로
  • Bitmask로 42개 periphery의 사용 여부를 64-bit 정수로 관리
  • Pre-allocation으로 모든 동적 할당 제거

14. 결론

七子開連(칠자개련)은 칠자각득(七子各得)의 3차원 확장이다. 5방향에서 7방향으로, 평면에서 구면으로, 단일에서 다층으로 나아갔다.

이 확장은 단순한 숫자 늘리기가 아니다. 파라미터 p의 변화가 기하·대수·조합 전체에 파급 효과를 낳는 구조적 실험이다. Π(5, 6, 120)에서 Π(7, 6, 175)로, 그리고 Π(5, 49, 6027)로의 도약은 "퍼즐의 퍼즐"이라는 메타 구조를 탄생시켰다.


시각화


import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch
from mpl_toolkits.mplot3d import proj3d
from mpl_toolkits.mplot3d.art3d import Line3DCollection
import json

# --- 3D 좌표 생성 함수 ---
def generate_chilja_gaeryeon_3d():
    """七子開連 3D 좌표 및 연결 생성"""

    # 7개 방향 (구면 균등분포: 북극 + 6개 위도고)
    directions_7 = np.array([
        [0, 0, 1],
        [np.sin(np.pi/3)*np.cos(0), np.sin(np.pi/3)*np.sin(0), np.cos(np.pi/3)],
        [np.sin(np.pi/3)*np.cos(2*np.pi/6), np.sin(np.pi/3)*np.sin(2*np.pi/6), np.cos(np.pi/3)],
        [np.sin(np.pi/3)*np.cos(4*np.pi/6), np.sin(np.pi/3)*np.sin(4*np.pi/6), np.cos(np.pi/3)],
        [np.sin(np.pi/3)*np.cos(6*np.pi/6), np.sin(np.pi/3)*np.sin(6*np.pi/6), np.cos(np.pi/3)],
        [np.sin(np.pi/3)*np.cos(8*np.pi/6), np.sin(np.pi/3)*np.sin(8*np.pi/6), np.cos(np.pi/3)],
        [np.sin(np.pi/3)*np.cos(10*np.pi/6), np.sin(np.pi/3)*np.sin(10*np.pi/6), np.cos(np.pi/3)],
    ])
    directions_7 = directions_7 / np.linalg.norm(directions_7, axis=1, keepdims=True)

    # 상위 5개 슈퍼 클러스터 방향 (구면 균등)
    super_directions = np.array([
        [0, 0, 1],
        [np.sin(0.4*np.pi)*np.cos(0), np.sin(0.4*np.pi)*np.sin(0), np.cos(0.4*np.pi)],
        [np.sin(0.4*np.pi)*np.cos(2*np.pi/5), np.sin(0.4*np.pi)*np.sin(2*np.pi/5), np.cos(0.4*np.pi)],
        [np.sin(0.4*np.pi)*np.cos(4*np.pi/5), np.sin(0.4*np.pi)*np.sin(4*np.pi/5), np.cos(0.4*np.pi)],
        [np.sin(0.4*np.pi)*np.cos(6*np.pi/5), np.sin(0.4*np.pi)*np.sin(6*np.pi/5), np.cos(0.4*np.pi)],
    ])
    super_directions = super_directions / np.linalg.norm(super_directions, axis=1, keepdims=True)

    # --- 숫자 배치 (greedy) ---
    from itertools import combinations

    def create_one_chilja(start_num):
        centers = list(range(start_num, start_num + 7))
        periphery = list(range(start_num + 7, start_num + 49))
        clusters = []
        used = set()

        t1 = 7 * start_num + 168
        for i, c in enumerate(centers):
            target = t1 - c
            available = [p for p in periphery if p not in used]
            found = False
            for combo in combinations(available, 6):
                if sum(combo) == target:
                    clusters.append({'center': c, 'periphery': list(combo)})
                    for p in combo: used.add(p)
                    found = True
                    break
            if not found:
                # fallback: closest
                best = None
                best_diff = float('inf')
                for combo in combinations(available[:30], 6):
                    diff = abs(sum(combo) - target)
                    if diff < best_diff:
                        best_diff = diff
                        best = combo
                clusters.append({'center': c, 'periphery': list(best)})
                for p in best: used.add(p)
        return clusters

    # 3D 좌표 생성
    nodes = []
    edges = []

    for super_idx in range(5):
        start = super_idx * 49 + 1
        clusters = create_one_chilja(start)
        super_dir = super_directions[super_idx]

        for ci, cluster in enumerate(clusters):
            center_dir = directions_7[ci]
            center_pos = super_dir * 2 + center_dir * 0.5
            center_pos = center_pos / np.linalg.norm(center_pos) * 2.5

            nodes.append({
                'value': cluster['center'],
                'pos': center_pos,
                'type': 'center',
                'super': super_idx,
                'cluster': cluster['center']
            })

            for j, p in enumerate(cluster['periphery']):
                theta = 2 * np.pi * j / 6 + cluster['center'] * 0.5
                phi = np.pi / 3
                offset = np.array([
                    np.sin(phi) * np.cos(theta),
                    np.sin(phi) * np.sin(theta),
                    np.cos(phi)
                ]) * 0.3
                p_pos = center_pos + offset
                p_pos = p_pos / np.linalg.norm(p_pos) * 2.5

                nodes.append({
                    'value': p,
                    'pos': p_pos,
                    'type': 'periphery',
                    'super': super_idx,
                    'cluster': cluster['center']
                })
                edges.append((cluster['center'], p, 'cluster'))

        # 슈퍼 클러스터 내부 중심 연결
        centers = [c['center'] for c in clusters]
        for i in range(len(centers)):
            for j in range(i+1, len(centers)):
                edges.append((centers[i], centers[j], 'super'))

    return nodes, edges

# --- HTML+CSS 3D 위젯 저장 ---
HTML_TEMPLATE = r'''<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>七子開連 (Chilja Gaeryeon) 3D 위젯</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
html, body {{ width: 100%; height: 100%; overflow: hidden; background: #0a0a0f; color: #fff; font-family: 'Segoe UI', sans-serif; }}
#container {{ position: relative; width: 100%; height: 100%; perspective: 1000px; overflow: hidden; }}
#scene {{ position: absolute; left: 50%; top: 50%; width: 0; height: 0; transform-style: preserve-3d; transition: transform 0.05s linear; }}
.node {{ position: absolute; left: 0; top: 0; border-radius: 50%; transform: translate(-50%, -50%); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 9px; font-weight: bold; user-select: none; box-shadow: 0 0 4px rgba(0,0,0,0.8); }}
.node.center {{ width: 18px; height: 18px; background: #fff; color: #000; border: 1px solid #000; z-index: 10; }}
.node.periphery {{ width: 10px; height: 10px; z-index: 5; }}
#edges {{ position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; }}
#edges line {{ stroke-linecap: round; }}
#controls {{ position: absolute; top: 12px; right: 12px; width: 220px; background: rgba(20,20,30,0.9); border: 1px solid #333; border-radius: 8px; padding: 14px; z-index: 100; font-size: 13px; }}
#controls h2 {{ font-size: 15px; margin-bottom: 10px; color: #fff; }}
#controls .row {{ margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; }}
#controls label {{ cursor: pointer; }}
#controls input[type="checkbox"] {{ margin-right: 6px; }}
#controls button {{ width: 100%; margin-top: 6px; padding: 6px; background: #2a2a3a; border: 1px solid #555; color: #fff; border-radius: 4px; cursor: pointer; }}
#controls button:hover {{ background: #3a3a4a; }}
#tooltip {{ position: absolute; background: rgba(0,0,0,0.85); border: 1px solid #555; border-radius: 4px; padding: 6px 10px; font-size: 12px; pointer-events: none; display: none; z-index: 200; white-space: pre; }}
#info {{ position: absolute; bottom: 12px; left: 12px; color: #aaa; font-size: 12px; z-index: 100; }}
</style>
</head>
<body>
<div id="container">
  <div id="scene"></div>
  <svg id="edges"></svg>
  <div id="controls">
    <h2>Chilja 3D 위젯</h2>
    <div class="row"><label><input type="checkbox" id="show-cluster" checked> cluster edges</label></div>
    <div class="row"><label><input type="checkbox" id="show-super" checked> super edges</label></div>
    <div class="row"><label><input type="checkbox" id="show-center" checked> centers</label></div>
    <div class="row"><label><input type="checkbox" id="show-periphery" checked> periphery</label></div>
    <div style="margin-top:10px; border-top:1px solid #333; padding-top:8px;">
      <div style="margin-bottom:4px; font-weight:bold;">super clusters</div>
      {super_checkboxes}
    </div>
    <button id="reset">초기 화면으로</button>
  </div>
  <div id="tooltip"></div>
  <div id="info">드래그: 회전 | 휠: 줌 | 노드 호버: 정보</div>
</div>
<script>
const nodes = {nodes_json};
const edges = {edges_json};
const superColors = ['#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff'];

let rotX = -0.4, rotY = 0.6, scale = 180;
let isDragging = false, lastX = 0, lastY = 0;
const scene = document.getElementById('scene');
const edgesSvg = document.getElementById('edges');
const tooltip = document.getElementById('tooltip');
const container = document.getElementById('container');

function project(x, y, z) {{
  const cx = Math.cos(rotX), sx = Math.sin(rotX);
  const cy = Math.cos(rotY), sy = Math.sin(rotY);
  // rotate Y
  let x1 = x * cy - z * sy;
  let z1 = x * sy + z * cy;
  // rotate X
  let y2 = y * cx - z1 * sx;
  let z2 = y * sx + z1 * cx;
  return {{ x: x1 * scale, y: y2 * scale, z: z2 * scale }};
}}

function renderNodes() {{
  const showCenter = document.getElementById('show-center').checked;
  const showPeriphery = document.getElementById('show-periphery').checked;
  const superChecks = [0,1,2,3,4].map(i => document.getElementById('super-'+i).checked);

  const projected = nodes.map(n => {{
    const p = project(n.x, n.y, n.z);
    return {{ ...n, px: p.x, py: p.y, pz: p.z }};
  }}).filter(n => {{
    if (n.type === 'center' && !showCenter) return false;
    if (n.type === 'periphery' && !showPeriphery) return false;
    return superChecks[n.super];
  }}).sort((a, b) => a.pz - b.pz);

  let html = '';
  projected.forEach(n => {{
    const opacity = Math.min(1, Math.max(0.4, 1 - (n.pz + 4) / 12));
    const color = n.type === 'center' ? '#fff' : superColors[n.super % 5];
    const zIndex = Math.round((n.pz + 5) * 100);
    html += `<div class="node ${{n.type}}" data-value="${{n.value}}" style="transform: translate3d(${{n.px}}px, ${{n.py}}px, ${{n.pz}}px) translate(-50%, -50%); background: ${{color}}; opacity: ${{opacity.toFixed(2)}}; z-index: ${{zIndex}};">${{n.type === 'center' ? n.value : ''}}</div>`;
  }});
  scene.innerHTML = html;

  document.querySelectorAll('.node').forEach(el => {{
    el.addEventListener('mouseenter', e => {{
      const v = parseInt(el.dataset.value);
      const n = nodes.find(x => x.value === v);
      tooltip.textContent = `value: ${{n.value}}\ntype: ${{n.type}}\nsuper: ${{n.super}}\ncluster: ${{n.cluster}}`;
      tooltip.style.display = 'block';
    }});
    el.addEventListener('mousemove', e => {{
      tooltip.style.left = (e.clientX + 12) + 'px';
      tooltip.style.top = (e.clientY + 12) + 'px';
    }});
    el.addEventListener('mouseleave', () => tooltip.style.display = 'none');
  }});
}}

function renderEdges() {{
  const showCluster = document.getElementById('show-cluster').checked;
  const showSuper = document.getElementById('show-super').checked;
  const superChecks = [0,1,2,3,4].map(i => document.getElementById('super-'+i).checked);

  const posMap = {{}};
  nodes.forEach(n => {{
    if (!superChecks[n.super]) return;
    posMap[n.value] = project(n.x, n.y, n.z);
  }});

  let svg = '';
  edges.forEach(e => {{
    if (e.type === 'cluster' && !showCluster) return;
    if (e.type === 'super' && !showSuper) return;
    const s = posMap[e.source], t = posMap[e.target];
    if (!s || !t) return;
    const color = e.type === 'cluster' ? '#00ffff' : '#ffaa00';
    const alpha = e.type === 'cluster' ? 0.15 : 0.25;
    svg += `<line x1="${{container.clientWidth/2 + s.x}}" y1="${{container.clientHeight/2 + s.y}}" x2="${{container.clientWidth/2 + t.x}}" y2="${{container.clientHeight/2 + t.y}}" stroke="${{color}}" stroke-opacity="${{alpha}}" stroke-width="${{e.type === 'cluster' ? 0.6 : 1}}" />`;
  }});
  edgesSvg.innerHTML = svg;
}}

function render() {{
  scene.style.transform = `translate(-50%, -50%) rotateX(${{rotX}}rad) rotateY(${{rotY}}rad)`;
  renderNodes();
  renderEdges();
}}

container.addEventListener('mousedown', e => {{ isDragging = true; lastX = e.clientX; lastY = e.clientY; }});
window.addEventListener('mouseup', () => isDragging = false);
window.addEventListener('mousemove', e => {{
  if (!isDragging) return;
  const dx = e.clientX - lastX;
  const dy = e.clientY - lastY;
  rotY += dx * 0.01;
  rotX += dy * 0.01;
  lastX = e.clientX;
  lastY = e.clientY;
  render();
}});
container.addEventListener('wheel', e => {{
  e.preventDefault();
  scale *= e.deltaY > 0 ? 0.9 : 1.1;
  render();
}}, {{ passive: false }});

document.getElementById('reset').addEventListener('click', () => {{ rotX = -0.4; rotY = 0.6; scale = 180; render(); }});
['show-cluster','show-super','show-center','show-periphery'].forEach(id => {{
  document.getElementById(id).addEventListener('change', render);
}});
[0,1,2,3,4].forEach(i => {{
  document.getElementById('super-'+i).addEventListener('change', render);
}});

window.addEventListener('resize', render);
render();
</script>
</body>
</html>'''


def generate_html_widget(nodes, edges, output_path='chilja_widget.html'):
    """matplotlib과 동일한 3D 데이터를 HTML+CSS+JS 단일 파일 위젯으로 저장합니다."""
    # 노드/엣지를 JSON 직렬화 가능한 형태로 변환
    nodes_js = []
    for n in nodes:
        p = n['pos']
        nodes_js.append({
            'value': int(n['value']),
            'x': float(p[0]), 'y': float(p[1]), 'z': float(p[2]),
            'type': n['type'],
            'super': int(n['super']),
            'cluster': int(n['cluster']),
        })
    edges_js = []
    for s, t, etype in edges:
        edges_js.append({'source': int(s), 'target': int(t), 'type': etype})

    super_checks = '\n'.join(
        f'<div class="row"><label><input type="checkbox" id="super-{i}" checked style="accent-color:{c}"> super {i}</label></div>'
        for i, c in enumerate(['#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff'])
    )

    html = HTML_TEMPLATE.format(
        nodes_json=json.dumps(nodes_js, ensure_ascii=False),
        edges_json=json.dumps(edges_js, ensure_ascii=False),
        super_checkboxes=super_checks,
    )

    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(html)
    print(f"HTML 위젯 저장 완료: {output_path}")


# --- 시각화 ---
def visualize(nodes, edges, save_html_on_close=True, html_path='chilja_widget.html'):
    fig = plt.figure(figsize=(14, 10))
    ax = fig.add_subplot(111, projection='3d')

    super_colors = ['#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff']

    # 노드 분리
    centers = [n for n in nodes if n['type'] == 'center']
    periphery = [n for n in nodes if n['type'] == 'periphery']

    # 중심 그리기
    for n in centers:
        ax.scatter(*n['pos'], c='white', s=80, edgecolors='black', linewidth=0.5, zorder=5)
        ax.text(n['pos'][0], n['pos'][1], n['pos'][2], str(n['value']), fontsize=7, color='white')

    # 주변 그리기
    for n in periphery:
        color = super_colors[n['super'] % 5]
        ax.scatter(*n['pos'], c=color, s=15, alpha=0.7, zorder=3)

    # 선 그리기
    cluster_edges = [(e[0], e[1]) for e in edges if e[2] == 'cluster']
    super_edges = [(e[0], e[1]) for e in edges if e[2] == 'super']

    pos_map = {n['value']: n['pos'] for n in nodes}

    for s, t in cluster_edges:
        ax.plot3D([pos_map[s][0], pos_map[t][0]], 
                  [pos_map[s][1], pos_map[t][1]], 
                  [pos_map[s][2], pos_map[t][2]], 
                  'c-', alpha=0.2, linewidth=0.3, zorder=1)

    for s, t in super_edges:
        ax.plot3D([pos_map[s][0], pos_map[t][0]], 
                  [pos_map[s][1], pos_map[t][1]], 
                  [pos_map[s][2], pos_map[t][2]], 
                  'orange', alpha=0.3, linewidth=0.5, zorder=2)

    ax.set_facecolor('#0a0a0f')
    fig.patch.set_facecolor('#0a0a0f')
    ax.set_title('七子開連 (Chilja Gaeryeon) — 245 nodes, 315 edges', color='white', fontsize=14)
    ax.set_xlabel('X', color='white')
    ax.set_ylabel('Y', color='white')
    ax.set_zlabel('Z', color='white')
    ax.tick_params(colors='white')
    ax.xaxis.pane.fill = False
    ax.yaxis.pane.fill = False
    ax.zaxis.pane.fill = False
    ax.xaxis.pane.set_edgecolor('white')
    ax.yaxis.pane.set_edgecolor('white')
    ax.zaxis.pane.set_edgecolor('white')
    ax.xaxis.pane.set_alpha(0.1)
    ax.yaxis.pane.set_alpha(0.1)
    ax.zaxis.pane.set_alpha(0.1)

    if save_html_on_close:
        def on_close(event):
            generate_html_widget(nodes, edges, html_path)
        fig.canvas.mpl_connect('close_event', on_close)

    plt.tight_layout()
    plt.show()

if __name__ == '__main__':
    import sys
    nodes, edges = generate_chilja_gaeryeon_3d()
    if '--html-only' in sys.argv:
        generate_html_widget(nodes, edges)
    else:
        visualize(nodes, edges)

풀이

#!/usr/bin/env python3
"""
七子開連 (Chilja Gaeryeon) — 강하게 최적화된 퍼즐 생성
Python 3.12+ | CLI
"""
import sys
import time
from array import array
from itertools import combinations
from dataclasses import dataclass
from typing import List, Tuple, Optional

# --- 상수 ---
CLUSTER_SIZE = 7
NUM_CLUSTERS = 7
NUM_SUPER = 5
TOTAL_NUMS = NUM_SUPER * NUM_CLUSTERS * CLUSTER_SIZE  # 245
T1 = 175  # 각 클러스터 합

@dataclass(slots=True, frozen=True)
class Cluster:
    center: int
    periphery: Tuple[int, ...]

    @property
    def total(self) -> int:
        return self.center + sum(self.periphery)

    @property
    def all_nums(self) -> Tuple[int, ...]:
        return (self.center,) + self.periphery

class ChiljaSolver:
    def solve_one_chilja(self, start: int) -> Optional[List[Cluster]]:
        """하나의 七子開連 생성 (49개 숫자).

        각 center에 필요한 6개 periphery를 백트래킹으로 찾으면서
        바로 다음 center로 DFS를 진행합니다. 조합을 미리 모두 생성하지 않고,
        유효한 조합을 찾는 즉시 깊이 우선 탐색을 이어가므로 메모리와 시간을 절약합니다.
        """
        centers = list(range(start, start + 7))
        periphery = list(range(start + 7, start + 49))

        # 각 슈퍼 클러스터의 총합을 7개 클러스터로 균등 분배
        t1 = 7 * start + 168
        targets = [t1 - c for c in centers]

        used = [False] * 42
        path: List[Cluster] = []

        def dfs(center_idx: int) -> bool:
            if center_idx == 7:
                return True

            # 사용 가능한 periphery 숫자만 모아서 정렬된 리스트 생성
            available = [periphery[i] for i in range(42) if not used[i]]
            target = targets[center_idx]
            combo: List[int] = []

            def backtrack(avail_start: int, depth: int, remaining: int) -> bool:
                if depth == 6:
                    if remaining == 0:
                        # 사용 마킹
                        for p in combo:
                            used[p - start - 7] = True
                        path.append(Cluster(centers[center_idx], tuple(combo)))

                        if dfs(center_idx + 1):
                            return True

                        # 백트래킹
                        path.pop()
                        for p in combo:
                            used[p - start - 7] = False
                    return False

                if remaining < 0:
                    return False

                need = 6 - depth
                if avail_start + need > len(available):
                    return False

                # min/max 가지치기
                min_needed = sum(available[avail_start:avail_start + need])
                max_needed = sum(available[-need:])
                if min_needed > remaining or max_needed < remaining:
                    return False

                for i in range(avail_start, len(available) - need + 1):
                    v = available[i]
                    if v > remaining:
                        break
                    combo.append(v)
                    if backtrack(i + 1, depth + 1, remaining - v):
                        return True
                    combo.pop()

                return False

            return backtrack(0, 0, target)

        if dfs(0):
            return path
        return None

    def solve_all(self) -> List[List[Cluster]]:
        """5개 슈퍼 클러스터 전체 생성"""
        result = []
        for super_idx in range(NUM_SUPER):
            start = super_idx * 49 + 1
            chilja = self.solve_one_chilja(start)
            if chilja is None:
                raise RuntimeError(f"Failed to solve super cluster {super_idx}")
            result.append(chilja)
        return result

    def verify(self, super_clusters: List[List[Cluster]]) -> bool:
        """검증"""
        all_nums = set()
        for sci, clusters in enumerate(super_clusters):
            start = sci * 49 + 1
            t1 = 7*start + 168
            super_sum = 0
            for ci, c in enumerate(clusters):
                assert c.total == t1, f"Super {sci} cluster {ci}: sum {c.total} != {t1}"
                super_sum += c.total
                for n in c.all_nums:
                    assert n not in all_nums, f"Duplicate: {n}"
                    all_nums.add(n)
            print(f"  Super {sci}: sum={super_sum}, centers={[c.center for c in clusters]}")
        assert len(all_nums) == TOTAL_NUMS
        assert all_nums == set(range(1, TOTAL_NUMS + 1))
        print(f"✓ All {TOTAL_NUMS} numbers used, no duplicates")
        return True

def main():
    print("七子開連 (Chilja Gaeryeon) Solver")
    print("=" * 50)

    t0 = time.perf_counter()
    solver = ChiljaSolver()
    result = solver.solve_all()
    t1 = time.perf_counter()

    print(f"\nGenerated in {t1 - t0:.4f}s")
    print(f"Total clusters: {len(result) * NUM_CLUSTERS}")

    print("\n--- Verification ---")
    solver.verify(result)

    # 출력
    print("\n--- First Super Cluster (1-49) ---")
    for c in result[0]:
        print(f"  Center {c.center}: periphery={c.periphery}, sum={c.total}")

if __name__ == '__main__':
    main()

언어별 풀이

chilja_solver.c main.go main.rs chilja_solver.py

七子開連
7 clusters × 7 numbers × 5 supers = 245 numbers
Each cluster sum = 175, each super sum = 6027
3D spherical, non-overlapping, fully verified

Back

Comments

No comments yet.