칠자각득에서 확장한 4D 퍼즐

by gg582 · 2026-07-08 07:26:21 · 33 views

七子胞群(칠자포군): 칠자각득의 4차원 확장

HTML 뷰어 다운로드 chilja_pogun.png chilja.png chilja22.png

칠자각득(七子各得)은 5개 방향에 7개 숫자씩, 각 방향 합이 120이 되는 한국 산학 퍼즐이다. 七子開連(칠자개련)은 이를 3차원 구면으로 확장했다. 이 글은 한 단계 더 나아가 **4차원 정육백포체(120-cell)**를 기반으로 한 **七子胞群(칠자포군)**을 제안하고, 실제로 C + AVX2로 해를 구현한 과정을 정리한다.


1. 七子開連의 한계

七子開連(Π(7, 6, 175))는 3D 구면 위에 7개 방향을 균등 배치하고, 5개의 七子開連를 묶어 245개 숫자를 배치했다. 이 구조는 자연스럽고 우아하지만, 한 가지 질문이 남는다.

"7방향을 5개 묶었으면, 5방향을 7개 묶을 수는 없을까?"

즉, 메타-메타 구조로의 확장 가능성이다. 하지만 5와 7은 서로소이고, 5×7=35방향을 3D 구면에 균등 배치하는 것은 불가능하다. 3D에서 균등한 점 배치는 플라톤의 다면체(4, 6, 8, 12, 20)로 제한된다.

그래서 4차원으로 가야 한다.


2. 4차원으로 가는 이유

4차원에서는 **플라톤의 다포체(Platonic polychoron)**가 6개 존재한다.

다포체 정점 수 특징
5-cell 5 4차원 단체
8-cell (tesseract) 16 4차원 정육면체
16-cell 8 tesseract의 쌍대
24-cell 24 자기쌍대, 4차원만의 특수한 다포체
120-cell 120 4차원 정이십면체의 확장
600-cell 600 120-cell의 쌍대

120-cell의 120개 정점은 **"7×17+1"**과 관련이 있다. 119 = 7×17이고, 120은 119에 1을 더한 것. 이 "1"은 **중심점(origin)**이 될 수 있다.

즉, 120-cell의 120개 정점 중 1개를 중심점으로 제외하고, 나머지 **119개를 17개 그룹(各 7개)**으로 분할하면, 七子開連의 구조를 4차원에서 재현할 수 있다.


3. 七子胞群(칠자포군)의 정의

원 저작이 한자 이름을 사용했기 때문에 그에 대한 패러디로 진행한다.

3.1 이름의 의미

  • 七子(칠자): "일곱 개씩" - 각 클러스터의 크기 유지
  • 胞群(포군): "세포 군집" - 4차원에서의 유기적 집합

"開連(개련)"에서 "胞群(포군)"으로 바뀐 것은 차원의 전환을 반영한다. 3차원의 "연결"에서 4차원의 "군집"으로, 더 높은 차원에서의 집합적 존재를 강조한다.

3.2 파라미터 정의

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

파라미터 의미
p 119 클러스터(중심)의 수
q 6 각 클러스터의 주변 슬롯 수
T 2919 각 클러스터의 목표 합
상위 p 17 슈퍼 그룹의 수
상위 q 7 각 슈퍼 그룹의 중심 수

七子開連는 Π(7, 6, 175)였다. 七子胞群은 같은 (7, 6) 구조를 유지하되, 클리스터 수를 119개로 늘리고 T를 2919로 확장한 것이다.

3.3 숫자 범위와 합 조건

  • 총 숫자: 119개 클러스터 × 7개 숫자 = 833개
  • 숫자 범위: 1 ~ 833 (중첩 불허)
  • 1~833 합: 833 × 834 / 2 = 347,361
  • 각 클러스터 합: 347,361 / 119 = 2,919 (정수로 떨어짐 ✓)
  • 중심 숫자: 1, 2, 3, ..., 119 (mod 119 완전 커버)
  • 주변 6개 합: 2,919 - 중심값
중심 주변 합 주변 숫자 후보군
1 2,918 120 ~ 833 중 6개
2 2,917 120 ~ 833 중 6개
... ... ...
119 2,800 120 ~ 833 중 6개

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


4. 4차원 기하: 120-cell

4.1 120-cell 정점 생성

120-cell의 정점은 황금비 φ = (1+√5)/2를 사용하여 생성된다.

  • 타입 1: (±2, ±2, 0, 0)의 짝수치환 및 부호 조합
  • 타입 2: (±φ², ±φ, ±1, 0)의 짝수치환 및 부호 조합
  • : 이론적으로 120개

정점 좌표는 4차원 구면 상에 정규화된다. 즉, 각 정점은 4D 단위 구면 위에 위치한다.

구현 노트: 현재 코드에서는 타입 1을 48개, 타입 2를 96개 생성해 총 144개 정점을 만든다. 이 중 1개를 중심점으로 제외하고 119개를 중심으로 사용한다. 120-cell의 이론적 정점 수(120)와 구현상 정점 수(144)는 구분해서 본다.

4.2 4D -> 3D Stereographic Projection

4차원 구면 위의 점 (x, y, z, w)를 3차원으로 투영하는 방법:

(x, y, z, w) -> (x/(1.0001 - w), y/(1.0001 - w), z/(1.0001 - w))

w가 1에 아주 가까워지는 경우를 피하기 위해 분모에 작은 여유(1.0001)를 두었다. 120-cell의 복잡한 대칭 구조가 3D에서도 시각적으로 드러난다.

4.3 119개 중심의 17개 상위 그룹 분할

120-cell의 120개 정점 중 **1개를 중심점(origin)**으로 선택하고, 나머지 119개를 7개씩 묶어 17개 슈퍼 그룹을 만든다.

현재 구현에서는 단순히 정점 인덱스 순으로 7개씩 나눈다:

  • 슈퍼 그룹 1: 중심 1 ~ 7
  • 슈퍼 그룹 2: 중심 8 ~ 14
  • ...
  • 슈퍼 그룹 17: 중심 113 ~ 119

이 분할은 120-cell의 대칭성을 일부 단순화하지만, 구현과 검증을 명확하게 한다.


5. 연결 구조

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

각 중심 숫자는 자신의 6개 주변 숫자와 직접 연결된다.

  • 총 연결: 119개 중심 × 6개 주변 = 714개

5.2 슈퍼 그룹 내부 연결 (주황 선)

각 슈퍼 그룹 내부의 7개 중심은 **완전그래프(K₇)**로 연결된다.

  • 총 연결: 17개 그룹 × C(7, 2) = 17 × 21 = 357개

5.3 전체 연결

  • 총 노드: 833개 (119 중심 + 714 주변)
  • 총 엣지: 714 + 357 = 1,071개

6. 생성 알고리즘

6.1 핵심 문제

주변 714개 숫자(120 ~ 833)를 119개 중심에 각 6개씩 배분해야 한다. 각 중심의 주변 6개 합이 2,919 - 중심값이 되어야 한다.

이는 대규모 정수 분할 + exact cover 문제이다. 714개 숫자 중 6개를 고르는 조합은 C(714, 6) ≈ 1.5 × 10^15개. 순진한 접근은 불가능하다.

6.2 초기 시도와 교훈

처음에는 1 ~ 833을 17개 블록(각 49개)으로 나누고, 각 블록 안에서 처음 7개를 중심, 다음 42개를 주변으로 사용하려 했다. 하지만 이 방식은 금방 실패한다:

  • 예를 들어 첫 블록의 중심 1은 주변 합이 2,918이 필요한데,
  • 사용 가능한 주변 숫자가 8 ~ 49로 제한되면
  • 6개 주변의 최대 합은 44+45+46+47+48+49 = 279에 불과하다.

따라서 중심은 1 119, 주변은 120 833이라는 전역적 분리가 필수적이다.

6.3 최종 solver 설계

최종 구현은 다음과 같이 동작한다:

  1. 중심 고정: 슈퍼 그룹 s의 중심은 1 + 7s - 7 + 7s.
  2. 주변 풀: 120 ~ 833 전체에서 아직 사용되지 않은 숫자를 후보로 사용.
  3. 백트래킹: 각 중심에 대해 6개 주변 조합을 찾아 할당하고, 실패하면 되돌림.
  4. MRV 휴리스틱: 주변 합이 작은 중심(= 큰 중심 번호)부터 처리. 이렇게 하면 후보가 적은 단계부터 먼저 탐색하게 되어 가지치기 효과가 커진다.
  5. 슈퍼 그룹 역순 처리: 17번 슈퍼 그룹(중심 113 - 119)부터 1번(중심 1 - 7)까지 처리.

6.4 조합 탐색

6개 숫자의 합이 목표값이 되는 조합을 찾을 때는 재귀 백트래킹을 사용한다.

  • 후보 숫자 배열은 정렬되어 있으므로, 현재 숫자가 남은 목표값보다 크면 즉시 중단.
  • availableused_global 비슷한 전역 사용 테이블로 관리.
  • 6개를 모두 선택했을 때 합이 0이 되면 유효한 조합.

6.5 SIMD 가속 (AVX2)

6개 숫자의 합을 계산할 때 _mm256_hadd_epi32를 사용해 수직 합산을 수행한다. 조합 후보를 빠르게 평가할 때 유용하다.

static inline int simd_sum6(const int arr[6]) {
    __m256i v = _mm256_setr_epi32(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5], 0, 0);
    __m256i sum = _mm256_hadd_epi32(v, v);
    sum = _mm256_hadd_epi32(sum, sum);
    return _mm256_extract_epi32(sum, 0) + _mm256_extract_epi32(sum, 1);
}

6.6 실행 결과

/*
 * 七子胞群 (Chilja Pogun) — 4D 120-cell Solver
 * C + AVX2 SIMD, ~100x faster than Python
 * Compile: gcc -O3 -mavx2 -o chilja_pogun chilja_pogun.c
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <immintrin.h>

#define PHI        1.618033988749895
#define INV_PHI    0.618033988749895
#define PHI2       2.618033988749895

#define NUM_CENTERS      119
#define NUM_SUPER        17
#define CLUSTERS_PER_SUPER 7
#define NUMBERS_PER_CLUSTER 7
#define T                2919
#define TOTAL_NUMBERS    833
#define PERIPHERY_N      714  /* 833 - 119 */

/* 4D vertex */
typedef struct { double x, y, z, w; } Vec4;

/* Cluster */
typedef struct {
    int center;
    int periphery[6];
} Cluster;

/* 120-cell vertices (4D) */
static Vec4 vertices_120[144];
static int vertex_count = 0;

static inline void add_vertex(double x, double y, double z, double w) {
    double norm = sqrt(x*x + y*y + z*z + w*w);
    vertices_120[vertex_count++] = (Vec4){x/norm, y/norm, z/norm, w/norm};
}

static int perm_sign(int p[4]) {
    int inv = 0;
    for (int i = 0; i < 4; i++)
        for (int j = i+1; j < 4; j++)
            if (p[i] > p[j]) inv++;
    return inv % 2;
}

static void generate_120_cell() {
    /* Type 1: (±2, ±2, 0, 0) even permutations */
    int even_perms[12][4] = {
        {0,1,2,3},{0,2,3,1},{0,3,1,2},
        {1,0,3,2},{1,2,0,3},{1,3,2,0},
        {2,0,1,3},{2,1,3,0},{2,3,0,1},
        {3,0,2,1},{3,1,0,2},{3,2,1,0}
    };

    for (int s1 = -1; s1 <= 1; s1 += 2) {
        for (int s2 = -1; s2 <= 1; s2 += 2) {
            double base[4] = {2.0*s1, 2.0*s2, 0, 0};
            for (int pi = 0; pi < 12; pi++) {
                int *p = even_perms[pi];
                add_vertex(base[p[0]], base[p[1]], base[p[2]], base[p[3]]);
            }
        }
    }

    /* Type 2: (±φ², ±φ, ±1, 0) even permutations */
    double vals[8][4] = {
        {PHI2, PHI, 1, 0}, {PHI2, PHI, -1, 0},
        {PHI2, -PHI, 1, 0}, {PHI2, -PHI, -1, 0},
        {-PHI2, PHI, 1, 0}, {-PHI2, PHI, -1, 0},
        {-PHI2, -PHI, 1, 0}, {-PHI2, -PHI, -1, 0}
    };

    for (int vi = 0; vi < 8; vi++) {
        for (int pi = 0; pi < 12; pi++) {
            int *p = even_perms[pi];
            add_vertex(vals[vi][p[0]], vals[vi][p[1]], vals[vi][p[2]], vals[vi][p[3]]);
        }
    }
}

/* 4D -> 3D stereographic projection */
static inline Vec4 project_4d_to_3d(Vec4 v) {
    double denom = 1.0001 - v.w;
    double spread = 5.0;
    return (Vec4){spread * v.x/denom, spread * v.y/denom, spread * v.z/denom, 0};
}

/* SIMD sum of 6 ints using AVX2 */
static inline int simd_sum6(const int arr[6]) {
    __m256i v = _mm256_setr_epi32(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5], 0, 0);
    __m256i sum = _mm256_hadd_epi32(v, v);
    sum = _mm256_hadd_epi32(sum, sum);
    return _mm256_extract_epi32(sum, 0) + _mm256_extract_epi32(sum, 1);
}

/* Gosper's hack for combination enumeration */
static inline uint64_t gosper_next(uint64_t mask) {
    uint64_t c = mask & -mask;
    uint64_t r = mask + c;
    return (((r ^ mask) >> 2) / c) | r;
}

/* Recursive helper for find_combinations */
static void find_combinations_recursive(
    const int *available, int n, int target, int k,
    int combo[6], int start_idx,
    int out[][6], int *count, int max_out
) {
    if (*count >= max_out) return;
    if (k == 0) {
        if (target == 0) {
            memcpy(out[*count], combo, sizeof(int) * 6);
            (*count)++;
        }
        return;
    }
    if (start_idx > n - k) return;
    for (int i = start_idx; i < n; i++) {
        if (available[i] > target) break;
        combo[6 - k] = available[i];
        find_combinations_recursive(available, n, target - available[i], k - 1,
                                    combo, i + 1, out, count, max_out);
    }
}

/* Find 6-element combinations summing to target */
static int find_combinations(
    const int *available, int n, int target,
    int out[][6], int max_out
) {
    if (n < 6) return 0;
    int combo[6];
    int count = 0;
    find_combinations_recursive(available, n, target, 6, combo, 0, out, &count, max_out);
    return count;
}

/* Solve one super cluster (7 centers) */
static int solve_super_cluster(const int centers_in[7], Cluster *out, int *used_global) {
    int periphery[PERIPHERY_N];
    int targets[7];
    int centers[7];
    int order[7];

    for (int i = 0; i < 7; i++) {
        centers[i] = centers_in[i];
        order[i] = i;
        targets[i] = T - centers[i];
    }
    /* Process centers with smallest target (largest center) first */
    for (int i = 0; i < 6; i++) {
        for (int j = i + 1; j < 7; j++) {
            if (centers[order[i]] < centers[order[j]]) {
                int tmp = order[i];
                order[i] = order[j];
                order[j] = tmp;
            }
        }
    }

    for (int i = 0; i < PERIPHERY_N; i++) periphery[i] = 120 + i;

    int used[PERIPHERY_N] = {0};
    static int combo_buf[100000][6];

    int dfs(int idx) {
        if (idx == 7) return 1;
        int oi = order[idx];

        int available[PERIPHERY_N];
        int avail_n = 0;
        for (int i = 0; i < PERIPHERY_N; i++) {
            if (!used[i] && !used_global[periphery[i]]) {
                available[avail_n++] = periphery[i];
            }
        }

        int ncombos = find_combinations(available, avail_n, targets[oi], combo_buf, 100000);

        for (int ci = 0; ci < ncombos; ci++) {
            int indices[6];
            for (int j = 0; j < 6; j++) {
                int cval = combo_buf[ci][j];
                for (int i = 0; i < PERIPHERY_N; i++) {
                    if (periphery[i] == cval && !used[i]) {
                        indices[j] = i;
                        used[i] = 1;
                        used_global[cval] = 1;
                        break;
                    }
                }
            }

            out[oi].center = centers[oi];
            memcpy(out[oi].periphery, combo_buf[ci], sizeof(int) * 6);

            if (dfs(idx + 1)) return 1;

            for (int j = 0; j < 6; j++) {
                used[indices[j]] = 0;
                used_global[periphery[indices[j]]] = 0;
            }
        }
        return 0;
    }

    return dfs(0);
}

/* Main solver */
static int solve_all(Cluster *out[NUM_SUPER]) {
    int used_global[TOTAL_NUMBERS + 1] = {0};

    for (int si = NUM_SUPER - 1; si >= 0; si--) {
        int centers[7];
        for (int i = 0; i < 7; i++) centers[i] = 1 + si * 7 + i;
        out[si] = malloc(sizeof(Cluster) * 7);
        if (!out[si]) return 0;
        printf("    Solving super cluster %2d (centers %3d-%3d)...\n", si+1, centers[0], centers[6]);
        fflush(stdout);
        if (!solve_super_cluster(centers, out[si], used_global)) {
            fprintf(stderr, "Failed to solve super cluster %d\n", si);
            for (int j = 0; j <= si; j++) free(out[j]);
            return 0;
        }
    }
    return 1;
}

/* Verify */
static void verify(Cluster *clusters[NUM_SUPER]) {
    uint8_t seen[TOTAL_NUMBERS + 1] = {0};
    int total_sum = 0;

    for (int si = 0; si < NUM_SUPER; si++) {
        int super_sum = 0;
        int centers[7];
        for (int ci = 0; ci < 7; ci++) {
            Cluster *c = &clusters[si][ci];
            int cs = c->center + c->periphery[0] + c->periphery[1] + c->periphery[2]
                     + c->periphery[3] + c->periphery[4] + c->periphery[5];
            if (cs != T) {
                fprintf(stderr, "Sum mismatch: %d != %d\n", cs, T);
                exit(1);
            }
            super_sum += cs;
            total_sum += cs;
            centers[ci] = c->center;

            if (seen[c->center]) { fprintf(stderr, "Dup center %d\n", c->center); exit(1); }
            seen[c->center] = 1;
            for (int pi = 0; pi < 6; pi++) {
                if (seen[c->periphery[pi]]) { fprintf(stderr, "Dup peri %d\n", c->periphery[pi]); exit(1); }
                seen[c->periphery[pi]] = 1;
            }
        }
        printf("  Super %2d: sum=%d\n", si+1, super_sum);
    }

    int total_seen = 0;
    for (int i = 1; i <= TOTAL_NUMBERS; i++) if (seen[i]) total_seen++;
    printf("\nTotal sum: %d (expected: 347361)\n", total_sum);
    printf("Numbers: %d (expected: 833)\n", total_seen);
    printf("Full cover 1-833: %s\n", total_seen == TOTAL_NUMBERS ? "YES" : "NO");
}

/* Generate HTML widget */
static void generate_html(Cluster *clusters[NUM_SUPER], Vec4 origin, Vec4 *super_groups[NUM_SUPER]) {
    FILE *f = fopen("chilja_pogun_widget.html", "w");
    if (!f) { perror("fopen"); return; }

    fprintf(f, "<!DOCTYPE html>\n<html><head><meta charset=\"UTF-8\">\n");
    fprintf(f, "<title>Chilja Pogun - 4D 120-cell</title>\n");
    fprintf(f, "<style>\n");
    fprintf(f, "*{margin:0;padding:0;box-sizing:border-box}\n");
    fprintf(f, "body{background:#050508;font-family:'Courier New',monospace;overflow:hidden}\n");
    fprintf(f, "#c{width:100vw;height:100vh;position:relative}\n");
    fprintf(f, "#cv{width:100%%;height:100%%;cursor:grab;display:block}\n");
    fprintf(f, "#cv:active{cursor:grabbing}\n");
    fprintf(f, "#i{position:absolute;top:12px;left:12px;color:#aaa;font-size:11px;line-height:1.7;background:rgba(0,0,0,0.75);padding:10px 14px;border-radius:6px;pointer-events:none;border:1px solid rgba(255,255,255,0.08)}\n");
    fprintf(f, "#l{position:absolute;bottom:12px;right:12px;color:#888;font-size:10px;line-height:1.6;background:rgba(0,0,0,0.75);padding:10px 14px;border-radius:6px;pointer-events:none;border:1px solid rgba(255,255,255,0.08)}\n");
    fprintf(f, "</style></head><body>\n");
    fprintf(f, "<div id=\"c\"><canvas id=\"cv\"></canvas>\n");
    fprintf(f, "<div id=\"i\"><strong style=\"color:#fff\">Chilja Pogun</strong><br>4D 120-cell to 3D Stereographic Projection<br>119 centers | 17 super groups | 833 numbers<br>T=2919 | Drag to rotate | Scroll to zoom</div>\n");
    fprintf(f, "<div id=\"l\"><span style=\"color:#ffd700\">★</span> origin (excluded)<br><span style=\"color:#fff\">●</span> center (labeled)<br><span style=\"color:#888\">●</span> periphery<br>colors = super groups (17)</div></div>\n");

    fprintf(f, "<script>\n");
    fprintf(f, "const cv=document.getElementById('cv'),ctx=cv.getContext('2d'),c=document.getElementById('c');\n");
    fprintf(f, "function rs(){cv.width=c.clientWidth*2;cv.height=c.clientHeight*2;cv.style.width=c.clientWidth+'px';cv.style.height=c.clientHeight+'px';ctx.scale(2,2)}\n");
    fprintf(f, "rs();window.addEventListener('resize',rs);\n");
    fprintf(f, "let rx=0.4,ry=0.6,z=80;\n");
    fprintf(f, "const sc=['#ff3333','#33ff33','#3333ff','#ffff33','#ff33ff','#33ffff','#ff8833','#33ff88','#3388ff','#ff3388','#8833ff','#33ffaa','#ff6666','#66ff66','#6666ff','#ffaa66','#66ffaa'];\n");

    /* Nodes */
    fprintf(f, "const ns=[");
    Vec4 o3 = project_4d_to_3d(origin);
    fprintf(f, "{x:%.6f,y:%.6f,z:%.6f,t:'o',s:-1,v:'ORIGIN'},", o3.x, o3.y, o3.z);

    for (int si = 0; si < NUM_SUPER; si++) {
        for (int ci = 0; ci < 7; ci++) {
            Vec4 c4 = super_groups[si][ci];
            Vec4 c3 = project_4d_to_3d(c4);
            int cv = clusters[si][ci].center;
            fprintf(f, "{x:%.6f,y:%.6f,z:%.6f,t:'c',s:%d,v:'%d'},", c3.x, c3.y, c3.z, si, cv);

            for (int j = 0; j < 6; j++) {
                double theta = 2.0 * M_PI * j / 6.0 + si * 0.5 + ci * 0.3;
                double pert[4] = {
                    sin(theta) * 1.2, cos(theta) * 1.2,
                    sin(theta * 1.5) * 1.2, cos(theta * 1.5) * 1.2
                };
                Vec4 p4 = {c4.x + pert[0], c4.y + pert[1], c4.z + pert[2], c4.w + pert[3]};
                double pnorm = sqrt(p4.x*p4.x + p4.y*p4.y + p4.z*p4.z + p4.w*p4.w);
                p4.x /= pnorm; p4.y /= pnorm; p4.z /= pnorm; p4.w /= pnorm;
                Vec4 p3 = project_4d_to_3d(p4);
                int pv = clusters[si][ci].periphery[j];
                fprintf(f, "{x:%.6f,y:%.6f,z:%.6f,t:'p',s:%d,v:'%d'},", p3.x, p3.y, p3.z, si, pv);
            }
        }
    }
    fprintf(f, "];\n");

    /* Edges */
    fprintf(f, "const es=[");
    for (int si = 0; si < NUM_SUPER; si++) {
        for (int ci = 0; ci < 7; ci++) {
            Vec4 c4 = super_groups[si][ci];
            Vec4 c3 = project_4d_to_3d(c4);
            for (int j = 0; j < 6; j++) {
                double theta = 2.0 * M_PI * j / 6.0 + si * 0.5 + ci * 0.3;
                double pert[4] = {sin(theta)*1.2, cos(theta)*1.2, sin(theta*1.5)*1.2, cos(theta*1.5)*1.2};
                Vec4 p4 = {c4.x+pert[0], c4.y+pert[1], c4.z+pert[2], c4.w+pert[3]};
                double pn = sqrt(p4.x*p4.x+p4.y*p4.y+p4.z*p4.z+p4.w*p4.w);
                p4.x/=pn; p4.y/=pn; p4.z/=pn; p4.w/=pn;
                Vec4 p3 = project_4d_to_3d(p4);
                fprintf(f, "{x1:%.6f,y1:%.6f,z1:%.6f,x2:%.6f,y2:%.6f,z2:%.6f,ty:'cl'},", c3.x, c3.y, c3.z, p3.x, p3.y, p3.z);
            }
        }
        for (int i = 0; i < 7; i++) {
            for (int j = i+1; j < 7; j++) {
                Vec4 c1 = project_4d_to_3d(super_groups[si][i]);
                Vec4 c2 = project_4d_to_3d(super_groups[si][j]);
                fprintf(f, "{x1:%.6f,y1:%.6f,z1:%.6f,x2:%.6f,y2:%.6f,z2:%.6f,ty:'su'},", c1.x, c1.y, c1.z, c2.x, c2.y, c2.z);
            }
        }
    }
    fprintf(f, "];\n");

    /* Render loop */
    fprintf(f, "cv.addEventListener('mousedown',e=>{dg=true;lx=e.clientX;ly=e.clientY});\n");
    fprintf(f, "window.addEventListener('mouseup',()=>dg=false);\n");
    fprintf(f, "cv.addEventListener('mousemove',e=>{if(!dg)return;ry+=(e.clientX-lx)*0.008;rx+=(e.clientY-ly)*0.008;lx=e.clientX;ly=e.clientY});\n");
    fprintf(f, "function zw(e){e.preventDefault();e.stopPropagation();const d=Math.sign(e.deltaY||-e.detail||-e.wheelDelta);z*=d>0?0.75:1.3333;z=Math.max(10,Math.min(2000,z));return false}\n");
    fprintf(f, "cv.onwheel=zw;cv.addEventListener('wheel',zw,{passive:false});window.addEventListener('wheel',zw,{passive:false});\n");
    fprintf(f, "function p3(x,y,z){let x1=x*Math.cos(ry)-z*Math.sin(ry),z1=x*Math.sin(ry)+z*Math.cos(ry),y2=y*Math.cos(rx)-z1*Math.sin(rx),z2=y*Math.sin(rx)+z1*Math.cos(rx),f=800,s=f/(f+z2*40+300);return{x:x1*z*s+c.clientWidth/2,y:y2*z*s+c.clientHeight/2,z:z2,sc:s}}\n");
    fprintf(f, "function dr(){let w=c.clientWidth,h=c.clientHeight;ctx.clearRect(0,0,w,h);const pj=ns.map(n=>{const p=p3(n.x,n.y,n.z);return{...n,px:p.x,py:p.y,pz:p.z,sc:p.sc}});pj.sort((a,b)=>a.pz-b.pz);ctx.lineWidth=0.4;for(const e of es){const a=pj.find(n=>Math.abs(n.x-e.x1)<0.001&&Math.abs(n.y-e.y1)<0.001&&Math.abs(n.z-e.z1)<0.001),b=pj.find(n=>Math.abs(n.x-e.x2)<0.001&&Math.abs(n.y-e.y2)<0.001&&Math.abs(n.z-e.z2)<0.001);if(!a||!b)continue;let al=Math.max(0.03,0.2-(a.pz+b.pz)*0.015);ctx.strokeStyle=e.ty=='su'?`rgba(255,200,80,${al})`:`rgba(80,160,255,${al})`;ctx.beginPath();ctx.moveTo(a.px,a.py);ctx.lineTo(b.px,b.py);ctx.stroke()}for(const n of pj){let r=n.t=='o'?24*n.sc:(n.t=='c'?12*n.sc:6*n.sc);let cl=n.t=='o'?'#ffd700':(n.t=='c'?'#fff':sc[n.s%%17]);let al=Math.max(0.25,1-(n.pz+2)*0.12);ctx.globalAlpha=al;ctx.fillStyle=cl;ctx.beginPath();ctx.arc(n.px,n.py,r,0,Math.PI*2);ctx.fill();if(n.t=='c'&&n.sc>0.5){ctx.fillStyle='#fff';ctx.font=`bold ${Math.max(10,16*n.sc)}px monospace`;ctx.textAlign='center';ctx.fillText(n.v,n.px,n.py-r-5)}}ctx.globalAlpha=1;requestAnimationFrame(dr)}\n");
    fprintf(f, "dr();\n</script></body></html>\n");
    fclose(f);
}

int main() {
    printf("============================================================\n");
    printf("Chilja Pogun - 4D 120-cell (C + AVX2 SIMD)\n");
    printf("============================================================\n");

    struct timespec t0, t1;
    clock_gettime(CLOCK_MONOTONIC, &t0);

    printf("\n[1/4] Generating 120-cell vertices...\n");
    generate_120_cell();
    printf("      %d vertices\n", vertex_count);

    printf("[2/4] Creating 17 super groups...\n");
    Vec4 origin = vertices_120[0];
    Vec4 *super_groups[17];
    for (int i = 0; i < 17; i++) {
        super_groups[i] = &vertices_120[1 + i*7];
    }

    printf("[3/4] Solving 119 clusters...\n");
    Cluster *result[17];
    if (!solve_all(result)) {
        fprintf(stderr, "Failed to solve\n");
        return 1;
    }

    printf("[4/4] Verifying...\n");
    verify(result);

    clock_gettime(CLOCK_MONOTONIC, &t1);
    double elapsed = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9;
    printf("\nGenerated in %.4fs\n", elapsed);

    printf("\n[5/5] Generating HTML widget...\n");
    generate_html(result, origin, super_groups);
    printf("  HTML: chilja_pogun_widget.html\n");

    for (int i = 0; i < 17; i++) free(result[i]);

    printf("\nDone!\n");
    return 0;
}

단일 스레드, -O3 -mavx2 옵션으로 약 62초 만에 해를 찾았다.

[3/4] Solving 119 clusters...
    Solving super cluster 17 (centers 113-119)...
    ...
    Solving super cluster 1 (centers 1-7)...
[4/4] Verifying...
  Super  1: sum=20433
  ...
  Super 17: sum=20433

Total sum: 347361 (expected: 347361)
Numbers: 833 (expected: 833)
Full cover 1-833: YES

Generated in 62.5167s

7. 검증

7.1 클러스터 수준

  • 각 클러스터 합 = 2,919 ✓
  • 중심 숫자 1 ~ 119, 각각 한 번씩 ✓
  • 주변 숫자 120 ~ 833, 중복 없이 소진 ✓

7.2 슈퍼 그룹 수준

  • 각 슈퍼 그룹 합 = 7 × 2,919 = 20,433
  • 17개 슈퍼 그룹 모두 검증 통과 ✓

7.3 전체 수준

  • 1 ~ 833 중복 없이 완전 사용 ✓
  • 총 합 347,361 = 17 × 20,433 = 119 × 2,919 ✓

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

퍼즐 Π 표기 차원 특징
칠자각득 Π(5, 6, 120) 2D 원형, 5방향
七子開連 Π(7, 6, 175) 3D 확장, 7방향
七子開連×5 Π(5, 49, 6027) 3D 메타, 5 슈퍼
七子胞群 Π(119, 6, 2919) 4D 4D 120-cell, 17 슈퍼

七子胞群은 Π(119, 6, 2919)로, 이는 퍼즐 디자인 이론에서 **"대규모 파라미터 세트"**로 분류된다. 119개 클러스터가 4D 공간에 배치되고, 17개 슈퍼 그룹이 상위 구조를 형성한다.

상위 구조는 Π(17, 7, 20433)로 볼 수도 있다. 17개 슈퍼 그룹, 각 7개 중심, 합 20,433. 이는 **"퍼즐의 퍼즐의 퍼즐"**이다.


9. 시각화

4D stereographic projection을 통해 3D로 투영된 七子胞群은 마우스 드래그로 회전하고, 스크롤로 줌할 수 있다.

  • 금색 별(★): 중심점(origin) - 120-cell에서 제외된 1개 정점
  • 흰색 점: 중심 숫자 (1, 2, 3, ..., 119)
  • 색상 점: 주변 숫자 (슈퍼 그룹별 17색상 구분)
  • 파란 선: 클러스터 내부 연결 (중심-주변)
  • 주황 선: 슈퍼 그룹 내부 중심 연결 (완전그래프)

회전하면 17개 슈퍼 그룹이 4D 구면의 투영 영역에 분포한 것을 볼 수 있다. 각 슈퍼 그룹은 7개 중심으로 별 모양을 이루고, 그 주변에 6개 주변 숫자가 궤도를 돈다.

실행 후 chilja_pogun_widget.html이 생성되며, 브라우저에서 바로 확인할 수 있다.


10. 확장 가능성

10.1 600-cell로의 확장

120-cell의 쌍대인 600-cell은 600개 정점을 가진다. 600 = 7 × 85 + 5이므로, 595 = 7 × 85개 중심을 사용하고 5개를 제외하는 방식으로 더 큰 七子胞群을 만들 수 있다.

  • 595개 클러스터, 각 7개 숫자 = 4,165개 숫자
  • 1 ~ 4165 합 = 4,165 × 4,166 / 2 = 8,670,695
  • 8,670,695 / 595 = 14,573.0... -> 정수가 아님

600-cell은 120-cell보다 대칭성이 더 복잡하고, 정수 나눗셈 조건을 만족하지 않아 실용적이지 않다.

10.2 5차원으로의 확장

5차원에서는 정십육포체(16-cell), 정삼십이포체(32-cell) 등이 존재하지만, 플라톤의 다포체는 3개로 제한된다. 5차원 이상에서는 균등 점 배치가 더욱 어려워진다.

10.3 메타-메타-메타 구조

七子胞群의 17개 슈퍼 그룹을 다시 묶으려면, 17 = 소수이므로 하위 그룹 분할이 불가능하다. 이는 **"소수의 저주"**로, 더 높은 메타 구조로의 확장이 자연스럽게 막힌다.


11. 이 퍼즐이 좋은 점

11.1 차원의 도약

2D -> 3D -> 4D로의 확장은 단순한 숫자 늘리기가 아니다. 각 차원에서 새로운 기하적 제약이 등장하고, 그 제약을 해결하는 방식이 퍼즐의 구조를 형성한다.

11.2 플라톤의 다포체와의 만남

120-cell은 순수 수학의 산물이고, 칠자각득은 동아시아 산학의 산물이다. 이 둘의 만남은 **"고전과 현대의 다리"**를 넘어, **"동양과 서양의 다리"**를 만든다.

11.3 계층적 구조

119개 클러스터 -> 17개 슈퍼 그룹 -> 1개 중심점. 이 3단계 계층은 **"부분과 전체"**의 관계를 수학적으로 구현한다. 각 단계에서 7이라는 숫자가 반복되는 것은, "일곱 개씩"이라는 칠자각득의 개념이 4D에서도 살아 있음을 보여준다.


12. 이 퍼즐이 아쉬운 점

12.1 1개 정점의 제외

120-cell의 120개 정점 중 1개를 버려야 한다. 이는 **"완벽한 대칭성"**을 해치는 것이다. 119 = 7 × 17이라는 인위적 분해를 위해 120-cell의 아름다운 구조에 흠집을 내는 셈이다.

12.2 17의 소수성

17개 상위 그룹은 더 이상의 메타 구조로 확장이 불가능하다. 17이 소수라는 것은, **"이 구조가 끝이다"**라는 의미를 내포한다. 무한한 메타 타워를 쌓을 수 없다.

12.3 시각화의 한계

4D를 3D로 투영하면 정보가 손실된다. 120-cell의 복잡한 대칭 구조가 stereographic projection에서 왜곡되고, 17개 슈퍼 그룹의 4D 상대적 위치가 3D에서는 직관적으로 보이지 않는다.


13. 수학 교본으로서의 가치

13.1 고차원 기하의 입문

120-cell은 4차원 기하의 대표적인 예이다. 七子胞群을 통해 학생들은 **"4차원이 무엇인가"**를 직접 경험할 수 있다. Stereographic projection, 다포체, 황금비 등의 개념이 자연스럽게 등장한다.

13.2 대규모 조합 최적화

714개 숫자를 119개 그룹에 배분하는 문제는, 조합 폭발백트래킹의 실제 예시가 된다. 알고리즘 수업에서 훌륭한 프로젝트 주제가 될 수 있다.

13.3 SIMD 병렬 처리

AVX2를 이용한 6개 숫자의 동시 합산은, 벡터화병렬 처리의 입문 예시가 된다. 고성능 컴퓨팅 수업에서 C+SIMD 실습으로 활용할 수 있다.


14. 구현

C + AVX2 SIMD로 구현했다. Python 프로토타입은 느렸고, C 최적화 버전은 단일 스레드로 약 1분 만에 해를 찾았다.

최적화 포인트:

  • 전역 주변 풀(120 ~ 833)에서 중심별 6개 조합을 백트래킹으로 탐색
  • MRV 휴리스틱: 주변 합이 작은 중심부터 처리
  • AVX2 _mm256_hadd_epi32로 6개 숫자 합을 SIMD로 계산
  • used_global 배열로 833개 숫자의 사용 여부를 관리

향후 개선:

  • OpenMP나 pthreads로 슈퍼 그룹 탐색을 병렬화하면 수 초 대로 단축 가능
  • Dancing Links(Algorithm X) 방식으로 exact cover를 더 체계적으로 풀 수도 있음

15. 결론

七子胞群(칠자포군)은 칠자각득(七子各得)의 4차원 확장이다. 5방향 -> 7방향 -> 119방향으로, 평면 -> 3D 구면 -> 4D 다포체로 나아갔다.

이 확장은 "완벽함을 포기하고, 가능성을 얻는" 것이다. 120-cell의 1개 정점을 버리고, 17의 소수성을 받아들이는 대신, 우리는 4차원에서 퍼즐을 할 수 있게 되었다.

최석정(崔錫鼎, 1646-1715)이 구수략을 썼을 때 4차원을 상상했을까? 아마 아니었을 것이다. 하지만 그가 남긴 **"수를 그룹으로 묶어 합을 맞춘다"**는 개념은, 4차원에서도, 심지어 120-cell의 정점 위에서도, 여전히 살아 있다.


七子胞群 119 clusters × 7 numbers × 17 supers = 833 numbers Each cluster sum = 2919, each super sum = 20433 4D 120-cell, non-overlapping, fully verified C + AVX2 SIMD, ~1 minute on a single core

Back

Comments

No comments yet.