** 여기서 다루는 알고리즘 문제는 저의 창작 문제가 아닐 경우, 저작권 관련 문제를 방지하고자, 문제의 대략적인 설명 및 요구사항만 소개하고 있습니다.
** 이 공간에서 공유되는 풀이 및 Source code는 제 개인의 학습 목적이며, 당연히 최선의 정답이 아닙니다.
** 혹시 더 나은 방법이나 아이디어가 있으신 경우에는, 댓글에 의견을 공유해주시면 감사하겠습니다.
** Source code는 C 언어로 작성되었습니다.
알고리즘 분류 및 개요
- Backtracking / Branch and pruning
Input / output
- 입력
N-Queen 문제는 크기가 N × N인 체스판 위에 퀸 N개를 서로 공격할 수 없게 놓는 문제이다. N이 주어졌을 때, 퀸을 놓는 방법의 수를 구하는 프로그램을 작성하시오. 입력으로 N이 주어진다. (1 ≤ N < 15)
- 출력
첫째 줄에 퀸 N개를 서로 공격할 수 없게 놓는 경우의 수를 출력한다.
N Queen 문제?
N Queen 문제는 backtracking 알고리즘 설명 시에 항상 함께 언급되는 대표적인 문제입니다. 그만큼 중요한 문제이고, backtracking을 정확하게 이해하지 못할 경우 문제 풀이에 어려움을 겪을 수 있는 문제입니다.
먼저, 문제의 요구사항을 살펴 보겠습니다. 체스의 queen은 대각선과 직선으로 자유롭게 움직일 수 있습니다. 두 개의 queen이 서로를 공격하지 않도록 하려면, 대각선과 직선상에 queen을 함께 배치해서는 안됩니다. 아래 두가지 케이스가 함께 배치해서는 안되는 대표적인 사례를 보여줍니다.
풀이를 위한 가장 단순한 방법은, 모든 node에 queen을 배치하거나 배치하지 않고, 모든 case가 요구사항에 부합하는지 일일이 체크하는 것입니다. 이렇게 구현할 경우, N의 크기가 작을 경우에는 충분히 결과를 도출할 수 있지만, N의 크기가 커질 수록 연산량이 기하급수적으로 증가하게 됩니다. 고려해야할 케이스가 2 (True or False) ^ (N * N) 가지가 되기 때문입니다.
따라서 backtracking과 적절한 pruning을 활용하여 연산량을 줄여야만 빠른 시간내에 연산을 종료할 수 있습니다.
지금부터는, 기본적인 backtracking 방법의 적용부터 시작해서, 최적화를 통해서 한단계씩 연산량을 줄여나가 보도록 하겠습니다.
Backtracking과 Pruning - 최적화 1 단계
N이 4라고 가정할 때, 모든 case를 tree 형태로 나열할 경우, 다음과 같은 tree 구조로 표현될 수 있습니다.
즉, 총 4 x 4 x 4 x 4 = 256 가지의 case가 check되어야 합니다. 하지만, 모든 branch를 최대 깊이까지 검색하지 않고, 요구사항에 맞게 pruning(가지치기)을 할 경우, 다음과 같이 case를 정리할 수 있습니다.
요구 사항으로 활용할 수 있는 사항은 다음과 같습니다.
- Queen은 동일 선상에 위치할 수 없다. (동일 X 선상에서 Queen 하나가 할당되었다면, 더 이상 할당될 수 없음)
- Queen은 대각 선상에 위치할 수 없다.
(좌측 대각 방향 (↖) 과, 우측 대각 방향 (↗) 에 이미 Queen이 할당되어 있다면, 더 이상 할당 될 수 없음)
#include <stdio.h> #include <stdlib.h> typedef unsigned int (u32); typedef unsigned short (u16); typedef unsigned char (u8); typedef signed int (s32); typedef signed short (s16); typedef signed char (s8); u8 g_max_size; u32 g_total_num; static __inline u8 IsComplete( u8 depth ) { if (depth == (g_max_size - 1)) return 1; return 0; } static void FindNextTargets( u8 *arr, u8 depth, u8 *next_targets, u8 *num_of_next_targets ) { int i, j; u8 valid_target_num = 0; for (i = 0; i < g_max_size; i++) { u8 valid_flag = 1; for (j = 1; j <= depth; j++) { if (arr[(depth - j) * g_max_size + i] != 0) { valid_flag = 0; break; } if (((i - j) >= 0) && (arr[(depth - j) * g_max_size + (i - j)] != 0)) { valid_flag = 0; break; } if (((i + j) < g_max_size) && (arr[(depth - j) * g_max_size + (i + j)] != 0)) { valid_flag = 0; break; } } if (valid_flag == 1) next_targets[valid_target_num++] = i; } *num_of_next_targets = valid_target_num; } void Backtracking( u8 *arr, u8 depth ) { int i; // Check whether this combination is the solution or not. if (IsComplete( depth )) { g_total_num++; } else { u8 num_of_next_targets = 0; u8 *next_targets = (u8 *)calloc(g_max_size, sizeof(u8)); // Increase the count of depth. depth++; // Update the list of next targets and the number. FindNextTargets(arr, depth, next_targets, &num_of_next_targets); // Check every feasible target. for (i = 0; i < num_of_next_targets; i++) { arr[depth * g_max_size + next_targets[i]] = 1; Backtracking(arr, depth); arr[depth * g_max_size + next_targets[i]] = 0; } free(next_targets); } } int main(void) { int i; u8 *arr_ptr; scanf("%d", &g_max_size); arr_ptr = (u8 *)calloc(g_max_size*g_max_size, sizeof(u8)); for (i = 0; i < g_max_size; i++) { arr_ptr[i] = 1; Backtracking(arr_ptr, 0); arr_ptr[i] = 0; } printf("%d\n", g_total_num); free(arr_ptr); return 0; }
#include <time.h> int main(void) { int tc; int i; for (tc = 1; tc < 15; tc++) { u8 *arr_ptr; double start = clock(); g_max_size = tc; arr_ptr = (u8 *)calloc(g_max_size*g_max_size, sizeof(u8)); for (i = 0; i < g_max_size; i++) { arr_ptr[i] = 1; Backtracking(arr_ptr, 0); arr_ptr[i] = 0; } printf("Size : %2d -> Total case : %6d // Time complexity : %.6lf(msec)\n", tc, g_total_num, (double)(clock()-start)); g_total_num = 0; free(arr_ptr); } return 0; }
추가 최적화 아이디어 찾기 (1) - 최적화 2 단계
N queen 문제를 도식화한 그림을 살펴보면, 중심부를 기준으로 대칭인 결과가 도출됨을 발견할 수 있습니다. 만약 아래 4 queen case에서 x 축으로 2만큼만 검색을 한다면, 왼쪽의 case만을 결과로 취득할 수 있을 것입니다. 그렇다면, 최종 결과는 좌, 우측이 대칭임을 감안할 때, '최종 결과 = 왼쪽 결과 * 2'로 예상할 수 있습니다.
여기서 주의할 점은 N이 홀수인 케이스입니다. 홀수인 케이스는 중심 부분에 대한 검색을 추가해 주어야만 합니다.
#include <time.h> int main(void) { int tc; int i; for (tc = 1; tc < 15; tc++) { u8 *arr_ptr; double start = clock(); g_max_size = tc; arr_ptr = (u8 *)calloc(g_max_size*g_max_size, sizeof(u8)); { u8 test_size = g_max_size / 2; u32 total_num = 0; for (i = 0; i < test_size; i++) { arr_ptr[i] = 1; Backtracking(arr_ptr, 0); arr_ptr[i] = 0; } g_total_num *= 2; // 홀수 케이스에 대한 추가 처리 if (g_max_size % 2) { arr_ptr[i] = 1; Backtracking(arr_ptr, 0); arr_ptr[i] = 0; } } printf("Size : %2d -> Total case : %6d // Time complexity : %.6lf(msec)\n", tc, g_total_num, (double)(clock()-start)); g_total_num = 0; free(arr_ptr); } return 0; }
출력되는 결과는 다음과 같습니다. 여전히, 연산량이 크긴 하지만 앞선 결과와 비교했을 때, 절반 가까이 연산량이 줄어든 것을 확인 할 수 있습니다.
추가 최적화 아이디어 찾기 (2) - 최적화 3 단계
추가로 더 해볼 것은, 가지치기 방식에 관한 것입니다. 위 코드들은 가지치기를 위해서, 좌측 대각 방향 (↖) 과, 우측 대각 방향 (↗), 수직 방향으로 먼저 놓여진 queen이 있는지에 대해서 현재 depth만큼의 loop를 돌면서 검색을 수행하고 있습니다. (FindNextTarget 함수 참조) 속도 개선을 위해서 메모리를 좀 더 활용해서, for loop를 제거해보고자 합니다.
아래 그림을 보시면, 왼쪽 대각선 방향과 오른쪽 대각선 방향을 하나의 묶음으로 처리할 수 있음을 확인할 수 있습니다.
특정 위치에 queen이 놓여진다면, 이 위치에 해당하는 visit buffer에 표시를 하게 됩니다. 표시해야할 visit buffer는 수직, 좌측 대각, 우측 대각 총 세 가지입니다. 만약에 (0, 0) 위치에 queen이 놓여지게 된다면, 위 그림을 기준으로,
- visit_y[0] = 1; // vertical
- visit_l[3] = 1; // left upper
- visit_r[0] = 1; // right upper
(3, 3) 위치라면 다음과 같이 표시됩니다.
- visit_y[3] = 1; // vertical
- visit_l[4] = 1; // left upper
- visit_r[7] = 1; // right upper
#include <stdio.h> #include <stdlib.h> typedef unsigned int (u32); typedef unsigned short (u16); typedef unsigned char (u8); typedef signed int (s32); typedef signed short (s16); typedef signed char (s8); #define MAX_BUF_SIZE (14) u8 g_max_size; u32 g_total_num; u8 visit_y[MAX_BUF_SIZE]; u8 visit_l[MAX_BUF_SIZE + MAX_BUF_SIZE - 1]; u8 visit_r[MAX_BUF_SIZE + MAX_BUF_SIZE - 1]; #define SET_VERT(x) do { (visit_y[x] = 1); } while(0) #define SET_LEFT(x, y) do { (visit_l[x + y] = 1); } while(0) #define SET_RIGHT(x, y) do { (visit_r[(g_max_size - x - 1) + y] = 1); } while(0) #define CLEAR_VERT(x) do { (visit_y[x] = 0); } while(0) #define CLEAR_LEFT(x, y) do { (visit_l[x + y] = 0); } while(0) #define CLEAR_RIGHT(x, y) do { (visit_r[(g_max_size - x - 1) + y] = 0); } while(0) #define CHECK_VERT(x) (!(visit_y[x])) #define CHECK_LEFT(x, y) (!(visit_l[x + y])) #define CHECK_RIGHT(x, y) (!(visit_r[(g_max_size - x - 1) + y])) static __inline u8 IsComplete( u8 depth ) { if (depth == (g_max_size - 1)) return 1; return 0; } static void FindNextTargets( u8 *arr, u8 depth, u8 *next_targets, u8 *num_of_next_targets ) { int i, j; u8 valid_target_num = 0; for (i = 0; i < g_max_size; i++) { u8 valid_flag = 1; if ((CHECK_VERT(i)) && (CHECK_LEFT(i, depth)) && (CHECK_RIGHT(i, depth))) { next_targets[valid_target_num++] = i; } //for (j = 1; j <= depth; j++) //{ // if (arr[(depth - j) * g_max_size + i] != 0) // { // valid_flag = 0; // break; // } // if (((i - j) >= 0) && (arr[(depth - j) * g_max_size + (i - j)] != 0)) // { // valid_flag = 0; // break; // } // if (((i + j) < g_max_size) && (arr[(depth - j) * g_max_size + (i + j)] != 0)) // { // valid_flag = 0; // break; // } //} //if (valid_flag == 1) next_targets[valid_target_num++] = i; } *num_of_next_targets = valid_target_num; } static void __inline SetVisitFlag( int x, int y ) { SET_VERT(x); SET_LEFT(x, y); SET_RIGHT(x, y); } static void __inline ClearVisitFlag( int x, int y ) { CLEAR_VERT(x); CLEAR_LEFT(x, y); CLEAR_RIGHT(x, y); } void Backtracking( u8 *arr, u8 depth ) { int i; // Check whether this combination is the solution or not. if (IsComplete( depth )) { g_total_num++; } else { u8 num_of_next_targets = 0; u8 *next_targets = (u8 *)calloc(g_max_size, sizeof(u8)); // Increase the count of depth. depth++; // Update the list of next targets and the number. FindNextTargets(arr, depth, next_targets, &num_of_next_targets); // Check every feasible target. for (i = 0; i < num_of_next_targets; i++) { int x_pos = next_targets[i]; arr[depth * g_max_size + x_pos] = 1; SetVisitFlag(x_pos, depth); Backtracking(arr, depth); ClearVisitFlag(x_pos, depth); arr[depth * g_max_size + x_pos] = 0; } free(next_targets); } } int main(void) { int i; u8 *arr_ptr; scanf("%d", &g_max_size); arr_ptr = (u8 *)calloc(g_max_size*g_max_size, sizeof(u8)); { u8 test_size = g_max_size / 2; u32 total_num = 0; for (i = 0; i < test_size; i++) { arr_ptr[i] = 1; SetVisitFlag(i, 0); Backtracking(arr_ptr, 0); ClearVisitFlag(i, 0); arr_ptr[i] = 0; } g_total_num *= 2; if (g_max_size % 2) { arr_ptr[i] = 1; SetVisitFlag(i, 0); Backtracking(arr_ptr, 0); ClearVisitFlag(i, 0); arr_ptr[i] = 0; } } printf("%d\n", g_total_num); return 0; }
NOTE :
정적 메모리의 적절한 활용 - 최적화 4 단계
위 코드에서 제가 간과한 부분이 있습니다. 바로 'calloc()' 함수를 활용하여 memory를 동적 할당하는 부분이 두 군데 있었습니다. 동적 할당은 불필요한 data memory를 미리 선언해서 차지하지 않고, 입력되는 요구 사항에 맞게 heap 영역에 memory를 할당하여 사용하기 때문에 메모리 최적화 측면에서 효율적입니다.
하지만 속도가 중요한 지금 문제의 경우에서는 동적 할당과 메모리 해제에 들어가는 시간이 꽤 많이 소모됩니다. 따라서, 반복적인 memory할당 보다는 memory를 정적으로 선언하는 것이 시간 단축에 보다 효율적입니다. 다음 결과는 'calloc()' 함수를 정적 메모리로 변경한 후 측정한 결과입니다. 시간이 대폭 감소한 것을 확인할 수 있습니다. (Baekjoon online judge를 기준으로는 800ms 내외로 결과가 확인됩니다.)
최종 Source code
최적화를 거친 최종 Source code는 다음과 같습니다. 혹시 추가 아이디어가 있으신 분은 꼭 댓글로 의견 남겨주시면 감사하겠습니다.
/////////////////////////////////////////////// // - Backjoon 9663 : N-Queen (3) // - https://www.acmicpc.net/problem/9663 /////////////////////////////////////////////// #include <stdio.h> #include <stdlib.h> typedef unsigned int (u32); typedef unsigned short (u16); typedef unsigned char (u8); typedef signed int (s32); typedef signed short (s16); typedef signed char (s8); #define MAX_BUF_SIZE (14) u8 g_max_size; u32 g_total_num; u8 visit_y[MAX_BUF_SIZE]; u8 visit_l[MAX_BUF_SIZE + MAX_BUF_SIZE - 1]; u8 visit_r[MAX_BUF_SIZE + MAX_BUF_SIZE - 1]; #define SET_VERT(x) do { (visit_y[x] = 1); } while(0) #define SET_LEFT(x, y) do { (visit_l[x + y] = 1); } while(0) #define SET_RIGHT(x, y) do { (visit_r[(g_max_size - x - 1) + y] = 1); } while(0) #define CLEAR_VERT(x) do { (visit_y[x] = 0); } while(0) #define CLEAR_LEFT(x, y) do { (visit_l[x + y] = 0); } while(0) #define CLEAR_RIGHT(x, y) do { (visit_r[(g_max_size - x - 1) + y] = 0); } while(0) #define CHECK_VERT(x) (!(visit_y[x])) #define CHECK_LEFT(x, y) (!(visit_l[x + y])) #define CHECK_RIGHT(x, y) (!(visit_r[(g_max_size - x - 1) + y])) static __inline u8 IsComplete( u8 depth ) { if (depth == (g_max_size - 1)) return 1; return 0; } static void FindNextTargets( u8 *arr, u8 depth, u8 *next_targets, u8 *num_of_next_targets ) { int i, j; u8 valid_target_num = 0; for (i = 0; i < g_max_size; i++) { u8 valid_flag = 1; if ((CHECK_VERT(i)) && (CHECK_LEFT(i, depth)) && (CHECK_RIGHT(i, depth))) { next_targets[valid_target_num++] = i; } //for (j = 1; j <= depth; j++) //{ // if (arr[(depth - j) * g_max_size + i] != 0) // { // valid_flag = 0; // break; // } // if (((i - j) >= 0) && (arr[(depth - j) * g_max_size + (i - j)] != 0)) // { // valid_flag = 0; // break; // } // if (((i + j) < g_max_size) && (arr[(depth - j) * g_max_size + (i + j)] != 0)) // { // valid_flag = 0; // break; // } //} //if (valid_flag == 1) next_targets[valid_target_num++] = i; } *num_of_next_targets = valid_target_num; } static void __inline SetVisitFlag( int x, int y ) { SET_VERT(x); SET_LEFT(x, y); SET_RIGHT(x, y); } static void __inline ClearVisitFlag( int x, int y ) { CLEAR_VERT(x); CLEAR_LEFT(x, y); CLEAR_RIGHT(x, y); } void Backtracking( u8 *arr, u8 depth ) { int i; // Check whether this combination is the solution or not. if (IsComplete( depth )) { g_total_num++; } else { u8 num_of_next_targets = 0; u8 next_targets[MAX_BUF_SIZE] = {0, }; //u8 *next_targets = (u8 *)calloc(g_max_size, sizeof(u8)); // Increase the count of depth. depth++; // Update the list of next targets and the number. FindNextTargets(arr, depth, next_targets, &num_of_next_targets); // Check every feasible target. for (i = 0; i < num_of_next_targets; i++) { int x_pos = next_targets[i]; arr[depth * g_max_size + x_pos] = 1; SetVisitFlag(x_pos, depth); Backtracking(arr, depth); ClearVisitFlag(x_pos, depth); arr[depth * g_max_size + x_pos] = 0; } //free(next_targets); } } u8 arr_ptr[MAX_BUF_SIZE * MAX_BUF_SIZE]; int main(void) { int i; //u8 *arr_ptr; scanf("%d", &g_max_size); //arr_ptr = (u8 *)calloc(g_max_size*g_max_size, sizeof(u8)); { u8 test_size = g_max_size / 2; u32 total_num = 0; for (i = 0; i < test_size; i++) { arr_ptr[i] = 1; SetVisitFlag(i, 0); Backtracking(arr_ptr, 0); ClearVisitFlag(i, 0); arr_ptr[i] = 0; } g_total_num *= 2; if (g_max_size % 2) { arr_ptr[i] = 1; SetVisitFlag(i, 0); Backtracking(arr_ptr, 0); ClearVisitFlag(i, 0); arr_ptr[i] = 0; } } printf("%d\n", g_total_num); //free(arr_ptr); return 0; }
#include <time.h> u8 arr_ptr[MAX_BUF_SIZE * MAX_BUF_SIZE]; int main(void) { int tc; int i; for (tc = 1; tc < 15; tc++) { //u8 *arr_ptr; double start = clock(); g_max_size = tc; //arr_ptr = (u8 *)calloc(g_max_size*g_max_size, sizeof(u8)); { u8 test_size = g_max_size / 2; u32 total_num = 0; for (i = 0; i < test_size; i++) { arr_ptr[i] = 1; SetVisitFlag(i, 0); Backtracking(arr_ptr, 0); ClearVisitFlag(i, 0); arr_ptr[i] = 0; } g_total_num *= 2; if (g_max_size % 2) { arr_ptr[i] = 1; SetVisitFlag(i, 0); Backtracking(arr_ptr, 0); ClearVisitFlag(i, 0); arr_ptr[i] = 0; } } printf("Size : %2d -> Total case : %6d // Time complexity : %.6lf(msec)\n", tc, g_total_num, (double)(clock()-start)); g_total_num = 0; //free(arr_ptr); } return 0; }
난이도 / 중요도 / 재시도
★★★★☆ / ★★★★★ / x 0
출처 / 관련 링크
- Wiki : https://en.wikipedia.org/wiki/Eight_queens_puzzle
- Baekjoon online judge : https://www.acmicpc.net/problem/9663
- Backtracking 알고리즘 소개 : http://jayce-with.tistory.com/16
- 알고리즘 계산시간 측정 : http://jayce-with.tistory.com/8
Test input file
-
'# Algorithm > 알고리즘 일반' 카테고리의 다른 글
백트래킹 (Backtracking) (0) | 2018.02.10 |
---|---|
너비 우선 탐색 (BFS, Breadth First Search) (0) | 2018.02.01 |
알고리즘 계산 시간 측정 (2) | 2018.01.31 |