소수로만 이뤄진 마방진

요즘 사이트가 침체(?) 된거 같아 문제하나 투척하고 갑니다.^^

마방진은 각각의 숫자가 다르고 행과 열 및 대각선의 합이 같은 정사각 행렬이다.

가령 3x3 행렬의 마방진 중 대표적인 것은 다음과 같다.

4 9 2

3 5 7

8 1 6

그렇다면 3x3 마방진 중 500보다 작은 소수로만 이루어진 마방진을 몇개나 만들 수 있을까?

출력: 3x3 마방진 중 최대 500이하 소수로만 이뤄진 마방진의 갯수

단 1은 소수가 아니며, 회전하여서 같은 마방진은 1개로 생각한다.

중간값 하나의 경우에 대해서만 찾고, 90/180/270/vertical flip/horizontal flip 에 대해서는 안찾는 것으로 이해하겠습니다. - 예강효빠, 2017/06/27 10:47 M D
※ 상대에게 상처를 주기보다 서로에게 도움이 될 수 있는 댓글을 달아 주세요.

2개의 풀이가 있습니다.

홀수 마방진의 경우 center 는 무조건 값 list 의 중간값이어야 합니다. 회전했을때 (즉, 90,180,270도를 돌리거나 vertical & horizontal flip) 을 시켰을때도 하나로 친다는 얘기는 한개의 중간값에 대한 경우 하나만 찾으면 나머지는 안찾아도 된다는 얘깁니다. 조건에 맞족하는 경우 하나만 찾으면 됩니다.

a = np.array().reshape(3,3) 마방진에서

d1 = abs(a[0][0] - a[1][1]) = abs(a[2][2] - a[1][1])

d2 = abs(a[0][1] - a[1][1]) = abs(a[2][1] - a[1][1])

d3 = abs(a[0][2] - a[1][1]) = abs(a[2][0] - a[1][1])

d4 = abs(a[1][0] - a[1][1]) = abs(a[1][2] - a[1][1])

입니다.

따라서 0 < d2 < d4 인 범위를 정할 경우 중간값 이상의 소수에 대하여 combinations (x,y) 을 구하면 d2 = x-mid, d4 = y-mid 이 경우 abs(d1) = (d4+d2)/2 이며 abs(d3) = (d4-d2)/2 이고

나머지 6숫자는, r = [mid-d1, mid+d3, mid-d4, mid-d3, mid-d2, mid+d1]

all(x in 소수리스트 for x in r) 이 True 인 경우만 찾으면 됩니다.

검증코드는 1x9 array 를 받아 3x3 으로 reshape 한 후 가로줄, 세로기둥, 대각선의 각 합들을 구하고 그것들이 모두 같은지 봅니다.

알고리즘 및 판정

from math import sqrt
from itertools import combinations

# 범위내 소수찾기
def findPrimes(m):
    prime_nums = []
    for n in range(2,m):
        find_factor = list((i, n//i) for i in range(1, int(sqrt(n))+1) if n%i == 0)
        if len(find_factor) == 1:
            prime_nums.append(find_factor[0][1])
    return prime_nums

# 리스트내에서 마방진찾기    
def findMgcSqr(p):
    result = []
    for i in range(4,len(p)-4):
        mid = p[i]
        for x,y in combinations(p[i+1:],2):
            d2, d4 = x - mid, y - mid
            d1, d3 = int((d4+d2)/2), int((d4-d2)/2)
            r = [mid-d1, mid+d3, mid-d4, mid-d3, mid-d2, mid+d1]
            if all(value in p for value in r):
                r = [r[0],x,r[1],y,mid] + r[2:]
                result.append(r)
    return result

P = findPrimes(500) # 500 이하의 소수, 95개
S = findMgcSqr(P) # 가능한 마방진, 중간값기준
print(*iter(S), sep = "\n") # 하나씩 출력
print("Available MagicSquares by prime numbers: ", len(S)) # 모두 몇개?

검증코드

import numpy as np


def isMagicSquare(s):
    square = np.array(s).reshape(3,3) # 1x9 -> 3x3 matrix
    rows = iter(square) # 한개씩 반복하능하게
    columns = zip(*square) # square 의 가로줄 각각에 대하여 iter 하여 값을 순서대로 zip 함.
    diagonals = zip(*[(square[i][i], square[-i-1][i]) for i in range(len(square))]) # 대각선방향으로 각각 두개씩 tuple 로 묶어서 그걸 다시 iter 로 순서대로 zip 하면 대각선 두개 묶음나옴

    def sums(*args): # iterable 받아서 각각 sum 을 리스트에 모으기
        nums = []
        for a in args:
            nums.append(sum(a))
        return set(nums)

    if sums(*rows) == sums(*columns) == sums(*diagonals): return True # 합계 리스트 내에 sum 들을 각각 비교할때 모두 같으면 True
    else: return False # 하나라도 틀리면 마방진 아님.


if all(isMagicSquare(X) for X in S): print("All MagicSquares")
else: print("No all MagicSquares")

결과

>>>
[17, 89, 71, 113, 59, 5, 47, 29, 101]
[41, 89, 83, 113, 71, 29, 59, 53, 101]
[37, 79, 103, 139, 73, 7, 43, 67, 109]
[29, 131, 107, 167, 89, 11, 71, 47, 149]
[43, 127, 139, 199, 103, 7, 67, 79, 163]
[37, 151, 139, 211, 109, 7, 79, 67, 181]
[73, 151, 157, 211, 127, 43, 97, 103, 181]
[43, 181, 157, 241, 127, 13, 97, 73, 211]
[71, 149, 173, 233, 131, 29, 89, 113, 191]
[47, 191, 173, 263, 137, 11, 101, 83, 227]
[67, 151, 199, 271, 139, 7, 79, 127, 211]
[59, 197, 191, 281, 149, 17, 107, 101, 239]
[61, 199, 193, 283, 151, 19, 109, 103, 241]
[37, 271, 163, 283, 157, 31, 151, 43, 277]
[71, 233, 197, 293, 167, 41, 137, 101, 263]
[53, 251, 197, 311, 167, 23, 137, 83, 281]
[89, 197, 233, 317, 173, 29, 113, 149, 257]
[89, 191, 257, 347, 179, 11, 101, 167, 269]
[89, 233, 251, 353, 191, 29, 131, 149, 293]
[71, 269, 233, 353, 191, 29, 149, 113, 311]
[73, 277, 229, 349, 193, 37, 157, 109, 313]
[43, 349, 241, 409, 211, 13, 181, 73, 379]
[101, 263, 317, 443, 227, 11, 137, 191, 353]
[137, 281, 263, 353, 227, 101, 191, 173, 317]
[89, 347, 281, 431, 239, 47, 197, 131, 389]
[109, 283, 331, 463, 241, 19, 151, 199, 373]
[73, 379, 271, 439, 241, 43, 211, 103, 409]
[61, 379, 283, 463, 241, 19, 199, 103, 421]
[71, 419, 263, 443, 251, 59, 239, 83, 431]
[41, 443, 269, 479, 251, 23, 233, 59, 461]
[47, 443, 281, 491, 257, 23, 233, 71, 467]
[173, 347, 269, 359, 263, 167, 257, 179, 353]
[137, 359, 293, 419, 263, 107, 233, 167, 389]
[149, 311, 347, 467, 269, 71, 191, 227, 389]
[137, 311, 359, 491, 269, 47, 179, 227, 401]
[149, 347, 311, 431, 269, 107, 227, 191, 389]
[59, 467, 281, 491, 269, 47, 257, 71, 479]
[103, 379, 331, 499, 271, 43, 211, 163, 439]
[101, 431, 311, 491, 281, 71, 251, 131, 461]
[167, 359, 353, 479, 293, 107, 233, 227, 419]
Available MagicSquares by prime numbers:  40
All MagicSquares
※ 상대에게 상처를 주기보다 서로에게 도움이 될 수 있는 댓글을 달아 주세요.

먼저 sieve와 primes를 초기화한 다음 찾아봅니다.

#define N 500

char sieve[N] = {0,};
int primes[N] = {0,};
int len = 0;

int main(int argc, const char * argv[]) {
    // sieve
    sieve[0] = -1;
    sieve[1] = -1;
    for (int i=2; i<N; i++) {
        if (sieve[i] == -1)
            continue;
        sieve[i] = 1;
        primes[len++] = i;
        for (int p=i*2; p<N; p += i) {
            sieve[p] = -1;
        }
    }

    int count = count_magic_squares(primes, len);
    printf("%d\n", count);

    return 0;
}

마방진 채우는 건 백트래킹을 사용하면 될것 같습니다.

// assume that nums[] are sorted
int count_magic_squares(int nums[], int len) {
    int count = 0;
    int square[9] = {0,};
    bt(square, 0, nums, len, &count);
    return count;
}

bt의 기본뼈대는 금방 만들어집니다. 한글자씩 채우면서 마방진 룰을 어기면 더이상 진행하지 않고 백트랙합니다.

void bt(int square[], int i, int nums[], int len, int* count) {
    if (i == 9) {
        ++(*count);
        return;
    }

    for (int k=0; k<len; k++) {
        if (nums[k]) { // not used
            square[i] = nums[k];
            nums[k] = 0; // use

            if (magic(square, i))
                bt(square, i+1, nums, len, count);

            nums[k] = square[i]; // recover
            square[i] = 0;
        }
    }
}

magic()은 채우진 숫자들로 마방진의 룰을 어기는지 확인합니다. (가로/세로/대각선 합이 같다) 여기서 조금이라도 빨리 룰 체크가 가능하도록 채우는 순서를 조금 바꿔봤습니다.

// 0 1 2
// 3 5 6
// 4 8 7

int magic(int s[], int i) {
    if (i < 3) return 1;
    int sum = s[0] + s[1] + s[2];

    if (i < 4) return 1;
    if (s[0] + s[3] + s[4] != sum) return 0;

    if (i < 5) return 1;
    if (s[2] + s[5] + s[4] != sum) return 0;

    if (i < 6) return 1;
    if (s[3] + s[5] + s[6] != sum) return 0;

    if (i < 7) return 1;
    if (s[0] + s[5] + s[7] != sum) return 0;
    if (s[2] + s[6] + s[7] != sum) return 0;

    if (i < 8) return 1;
    if (s[4] + s[8] + s[7] != sum) return 0;
    if (s[1] + s[5] + s[8] != sum) return 0;
    return 1;
}

이러면 .. 오래 걸려도 답은 나옵니다. 그런데.. 그 답은 90도씩 4번 회전 가능하고 거기다 거울상이 있으니 총 8번 중복되어 계산됩니다. 그냥 마지막에 8로 나눠줘도 되고 쉽게 기준 점을 정해서 0번이 2번과 4번보다 커야하고(회전 방지), 2번이 4번보다 커야 하고(거울상 방지)의 규칙을 추가하면 됩니다.

오래 걸리는 문제는.. 잘 모르겠네요. 일단 넣어보는 소수가 정렬되었다는 점을 이용해서 우선은 더 큰 값이 들어오는 걸 방지하는 것도 될 것 같아요. 그래서 룰을 어겼을 때 0을 반환하는 대신 더 큰 값은 시도하지 마라는 의미로 -1을 반환한다면 .. 조금 더 단축될 것 같습니다.

int magic(int s[], int i) {
    if (i < 2) return 1;
    if (s[0] < s[2]) return -1;    // 중복제거 및 s[2] 자리에 더 큰 값은 시도하지 마라 (-1)

    if (i < 3) return 1;
    int sum = s[0] + s[1] + s[2];  // 합을 구했음

    int s4 = sum - s[0] - s[3];    // s[4] 자리에 와야 하는 값이 정해짐
    if (s4 < 2) return -1;         // 만약 2보다 작다면 더 큰 값은 시도해볼 필요도 없음
    if (s4 > N || sieve[s4] != 1) return 0;  // 그 값이 소수 아니어도 더 볼 필요없음
    ...
}

이런 식으로 룰 체크를 조금 강화하니 조금 빨라지긴 하네요.

참, -1 반환 때문에 bt()도 바뀌었습니다.

void bt(int square[], int i, int nums[], int len, int* count) {
    if (i == 9) {
        //printsquare(square);
        ++(*count);
        return;
    }

    for (int k=0; k<len; k++) {
        if (nums[k]) { // not used
            square[i] = nums[k];
            nums[k] = 0; // use

            int result = magic(square, i);
            if (result == 1)
                bt(square, i+1, nums, len, count);

            nums[k] = square[i]; // recover
            square[i] = 0;

            if (result == -1) {  // no further search
                break;
            }
        }
    }
}
※ 상대에게 상처를 주기보다 서로에게 도움이 될 수 있는 댓글을 달아 주세요.

풀이 작성

※ 풀이작성 안내
  • 본문에 코드를 삽입할 경우 에디터 우측 상단의 "코드삽입" 버튼을 이용 해 주세요.
  • 마크다운 문법으로 본문을 작성 해 주세요.
  • 풀이를 읽는 사람들을 위하여 풀이에 대한 설명도 부탁드려요. (아이디어나 사용한 알고리즘 또는 참고한 자료등)
  • 작성한 풀이는 다른 사람(빨간띠 이상)에 의해서 내용이 개선될 수 있습니다.
목록으로
코딩도장

코딩도장은 프로그래밍 문제풀이를 통해서 코딩 실력을 수련(Practice)하는 곳입니다.


언어별 풀이 현황
전 체 x 4
python x 2
기 타 x 2