분할 정복 알고리즘을 이용해 L - 트로미노 도형으로 정사각형을 채우는 문제를 풀 수 있다.
위 사이트에 들어가면 L - 트로미노 도형으로 정사각형을 채우는 과정을 보여주는 이미지를 gif형식으로 보여준다.
이 페이지에서는 L - 트로미노 도형을 이용해 정사각형을 채우는 과정을 자세히 살펴본다.
예제로는 [백준 14061: 샤워실 바닥 깔기], [백준 22359: 흔한 타일 색칠 문제]가 있다.
1. 트로미노란?
아래의 그림처럼 정사각형 3개를 이어붙여 만든 도형을 말한다.
트로미노 도형은 일자로 만든 도형과 L자로 만들어진 도형 두 가지가 존재한다.
2. L-트로미노 알고리즘
아이디어
[수학적 귀납법으로 모든 자연수에 대하여 참임을 확인]
L - 트트로미노 도형에 대하여 다음 명제가 성립한다.
한 변의 길이가 2의 n승인 정사각형은 한 칸을 제외하고 모두 L-트로미노 도형으로 채울 수 있다
따라서 위의 명제만 증명하면 분할 정복 알고리즘으로 한 변의 길이가 2ⁿ인 정사각형을 한 칸을 제외하고 트로미노 도형을 채울 수 있다.
<증명>
1. n = 1일 때
위의 그림에서도 알 수 있듯이 총 4가지의 방법으로 채울 수 있다.
2. n = k일 때
한 변의 길이가 2의 k승인 정사각형을 한 칸을 제외하고 모두 트로미노 도형으로 채울 수 있다고 가정하자.
3. n = k + 1일 때
바로 앞에서 한 변의 길이가 2의 k승인 정사각형들은 한 칸을 제외하고 모두 트로미노 도형으로 채울 수 있다고 가정했다.
n = k+1일 때에는 한 변의 길이가 2의 k승인 정사각형이 총 4개로 확장된다.
그러므로 n = k+1일 때에는 위의 그림과 같이 크기가 1 x 1인 정사각형이 총 4개 생긴다.
따라서 1 x 1의 4칸에 L - 트로미노 도형을 채우게 되면 1 x 1의 빈 공간이 하나 남는다.
4. 결론
n = 1일 때에 참이고, n = k일 때 참이라고 가정했을 때에 n = k+1일 때에 참이므로 수학적 귀납법에 따라 모든 자연수 n에 대하여 참이다.
따라서 한 변의 길이가 2ⁿ인 정사각형은 한 칸을 제외하고 모두 L - 트로미노 도형으로 채울 수 있다.
[분할 정복 알고리즘]
하나의 문제는 두 개 이상의 부분 문제로 쪼갤 수 있다. 쪼개진 부분 문제는 다음 특징을 가진다.
- 분할된 작은 문제는 원래 문제와 같은 해를 도출한다.
- 분할된 문제는 최적 부분 구조를 갖는다.(서로 독립적이다)
따라서 재귀를 이용한 분할 정복 알고리즘으로 구현할 수 있다.
이를 바탕으로 한 변의 길이가 2의 n승인 정사각형을 4등분하고 비어있는 3곳에 하나씩 공간을 채우고 난 다음 4등분된 영역에도 앞의 두 작업을 반복한다. 계속 진행되다 보면 한 변의 길이가 2인 정사각형까지 쪼개지고 마지막엔 한 칸을 제외한 모든 칸이 채워지게 된다.
알고리즘
[알고리즘의 순서]
- 정사각형을 1, 2, 3, 4구역으로 4등분한다.
- 1, 2, 3, 4구역을 탐색한 뒤 한 칸이라도 채워져 있는 영역을 찾는다.(한 구혁만 채워져 있고 나머지 세 영역은 비어이는 영역이 나온다)
- 한 칸이라도 채워져 있는 영역을 제외하고 나머지 세 영역에 L - 트로미노 도형의 모양대로 채운다.
- 1 ~ 3의 과정을 반복한다. 기저 사례는 한 변의 길이가 1일 때이다.
[시뮬레이션]
한 변의 길이가 2의 n승인 정사각형을 4등분하고 비어있는 3곳에 하나씩 공간을 채우고 난 다음 4등분된 영역에도 앞의 두 작업을 반복한다. 계속 진행되다 보면 한 변의 길이가 2인 정사각형까지 쪼개지고 마지막엔 한 칸을 제외한 모든 칸이 채워지게 된다.
n = 2일 때 진행 부분 문제들이 반복되는 과정을 간단히 살펴보자.
위와 같이 1구역, 2구역, 3구역, 4구역으로 나눌 수 있다.(X는 채우지 못하는 한 칸이다)
그 다음 비어있는 세 곳에 L - 트로미노 도형을 채운다.
1구역을 다시 1, 2, 3, 4구역으로 나누고 비어있는 세 부분에 L - 트로미노 도형을 채운다.
같은 과정을 귀납적으로 반복한다.
2구역을 다시 1, 2, 3, 4구역으로 나누고 비어있는 세 부분에 L - 트로미노 도형을 채운다.
3구역을 다시 1, 2, 3, 4구역으로 나누고 비어있는 세 부분에 L - 트로미노 도형을 채운다.
4구역을 다시 1, 2, 3, 4구역으로 나누고 비어있는 세 부분에 L - 트로미노 도형을 채운다.
이렇게 한 변의 길이가 1일 때까지 위의 과정을 반복하면 총 5개의 L - 트로미노 도형이 채워지게 된다.
위에서 귀납적으로 증명했듯이,이 과정은 한 변의 길이가 2ⁿ인 모든 정사각형에 대해 똑같이 반복된다.
구현하기
크게 tromino함수와 check함수 두 가지가 쓰인다.
[tromino함수]
void tromino(int** map, int& num, int size, int x, int y) {
if (size == 1) return;
num++;
size /= 2;
x -= size;
y -= size;
pair<int, int> dir[4] = { {x, y}, {x+1, y}, {x, y+1}, {x+1, y+1} };
bool pos1 = check(map, x - (size - 1), y - (size - 1), x, y); // 1구역 탐색
bool pos2 = check(map, x + 1, y - (size - 1), x + size, y); // 2구역 탐색
bool pos3 = check(map, x - (size - 1), y + 1, x, y + size); // 3구역 탐색
bool pos4 = check(map, x + 1, y + 1, x + size, y + size); // 4구역 탐색
//빈곳 num표시
bool pos[4] = { pos1, pos2, pos3, pos4 };
for (int i = 0; i < 4; i++) {
if (pos[i] == true) map[dir[i].X-1][dir[i].Y-1] = num;
}
//이후 재귀 호출(분할 정복)
toumino(map, num, size, x, y);
tromino(map, num, size, x + size, y);
tromino(map, num, size, x, y + size);
tromino(map, num, size, x + size, y + size);
}
- 정사각형의 새로운 중심을 찾고, 4등분하여 구역을 나눈다.
- 4등분한 구역을 탐색한다.(check함수로 탐색)
- 숫자가 차있는 구역을 제외한 나머지 3구역(모두 비어있는 구역)에 숫자를 채운다.
- 이후 처음 나누어진 4구역에 대해 재귀호출을 실행한다.
- 기저 사례는 크기가 1일 때이다.
[check함수]
bool check(int** arr, int x1, int y1, int x2, int y2) {
for (int i = x1; i <= x2; i++) {
for (int j = y1; j <= y2; j++) {
if (arr[i-1][j-1] != 0) return false;
}
}
return true;
}
- 시작좌표와 끝좌표를 입력받고 탐색한다.
- 하나라도 차있을 경우 false를 return받고 종료한다.
- 모두 비어있을 경우 true를 return받고 종료한다.
[기타]
- 2차원 배열의 동적할당을 사용하여 좌표를 구현하였다.
- 4등분된 구역의 탐색 결과는 pos의 배열을 만들어 비어있는지 여부를 확인하였다.
- 정사각형의 중심을 기준으로 숫자를 채울 4구역은 pair의 형태로 배열을 만들어 저장하였다.
- trumino함수가 종료할 때마다 num이 제대로 업데이트가 되지 않아서 num매개변수는 참조를 활용하였다.
[전체 코드]
다음의 코드는 k를 입력받고, 빈칸의 좌표(m, n)을 입력받으면 그 칸을 제외하고 나머지 칸을 트로미노 알고리즘으로 탐색하고 숫자를 채워서 결과를 2차원배열로 출력해주는 코드이다.
/*trumino algorithm*************************************
1. 중심을 기준으로 1,2, 3, 4구역을 돌며 비어있는지 체크
2. 차있는 곳을 제외한 빈 구역 3곳을 하나씩 체크하기
3. 다시 재귀 호출 -> DFS의 탐색구조를 가짐
*******************************************************/
#define X first
#define Y second
#include<iostream>
#include<cmath>
#include<cstdlib>
#include<string>
using namespace std;
int siz;
int num;
//주어진 범위 (x1, y1) ~ (y1, y2)를 탐색하며 모두 비어있으면 true, 하나라도 차있으면 false
bool check(int** arr, int x1, int y1, int x2, int y2) {
for (int i = x1; i <= x2; i++) {
for (int j = y1; j <= y2; j++) {
if (arr[i-1][j-1] != 0) return false;
}
}
return true;
}
void tromino(int** map, int& num, int size, int x, int y) {
if (size == 1) return;
num++;
size /= 2;
x -= size;
y -= size;
pair<int, int> dir[4] = { {x, y}, {x+1, y}, {x, y+1}, {x+1, y+1} };
bool pos1 = check(map, x - (size - 1), y - (size - 1), x, y); // 1구역 탐색
bool pos2 = check(map, x + 1, y - (size - 1), x + size, y); // 2구역 탐색
bool pos3 = check(map, x - (size - 1), y + 1, x, y + size); // 3구역 탐색
bool pos4 = check(map, x + 1, y + 1, x + size, y + size); // 4구역 탐색
//빈곳 num표시
bool pos[4] = { pos1, pos2, pos3, pos4 };
for (int i = 0; i < 4; i++) {
if (pos[i] == true) map[dir[i].X-1][dir[i].Y-1] = num;
}
//이후 재귀 호출(분할 정복)
tromino(map, num, size, x, y);
tromino(map, num, size, x + size, y);
tromino(map, num, size, x, y + size);
tromino(map, num, size, x + size, y + size);
}
int main(void) {
int k, m, n;
cin >> k;
siz = pow(2, k);
int** map = new int* [siz];
for (int i = 0; i < siz; i++) {
map[i] = new int[siz];
memset(map[i], 0, siz*4);
}
cin >> m >> n; //(채워지지 않는 한 곳 입력)
map[m-1][n-1] = -1;
tromino(map, num, siz, siz, siz);
for (int i = 0; i < siz; i++) {
for (int j = 0; j < siz; j++) {
cout << map[i][j] << " ";
}
cout << "\n";
}
return 0;
}
[출력]
첫 번째 입력: k = 4, 채워지지 않는 칸: 5행 6열
두 번째 입력: k = 2, 채워지지 않는 칸: 1행 2열
3. 예제
기본 문제와 트로미노 알고리즘을 응용한 예제가 있다.
https://www.acmicpc.net/problem/14601
다음은 조금 응요해서 이웃한 도형은 색이 다르게 칠하는 문제이다.
https://www.acmicpc.net/problem/22359
4. 느낀점
분할 정복 알고리즘 중에선 난이도가 상당히 높은 예제인 것 같다. 이 알고리즘만 제대로 이해해도 거의 모든 분할 정복 문제들을 쉽게 풀 수 있을 것 같다.
'자료구조 | 알고리즘 > 분할 정복(Devide Conquer)' 카테고리의 다른 글
재귀 함수와 반복문의 비교(recursion function) (2) | 2023.10.17 |
---|---|
분할 정복 대표 예시: 하노이탑(hanoi top) (0) | 2023.10.16 |
경이로운 BE 개발자가 되기 위한 프로그래밍 공부 기록장
도움이 되었다면 "❤️" 또는 "👍🏻" 해주세요! 문의는 아래 이메일로 보내주세요.