코드 훔쳐보는 변태 코더
춤 좋아하는 백엔드 개발자(였으면 좋겠다)
백준 2630번 색종이 만들기 (자바) 풀이

 

시간 제한메모리 제한제출정답맞힌 사람정답 비율
1 초 128 MB 28779 19535 15790 68.832%

문제

아래 <그림 1>과 같이 여러 개의 정사각형칸들로 이루어진 정사각형 모양의 종이가 주어져 있고, 각 정사각형들은 하얀색으로 칠해져 있거나 파란색으로 칠해져 있다. 주어진 종이를 일정한 규칙에 따라 잘라서 다양한 크기를 가진 정사각형 모양의 하얀색 또는 파란색 색종이를 만들려고 한다.

전체 종이의 크기가 N×N(N=2k, k는 1 이상 7 이하의 자연수)이라면 종이를 자르는 규칙은 다음과 같다.

전체 종이가 모두 같은 색으로 칠해져 있지 않으면 가로와 세로로 중간 부분을 잘라서 <그림 2>의 I, II, III, IV와 같이 똑같은 크기의 네 개의 N/2 × N/2색 종이로 나눈다. 나누어진 종이 I, II, III, IV 각각에 대해서도 앞에서와 마찬가지로 모두 같은 색으로 칠해져 있지 않으면 같은 방법으로 똑같은 크기의 네 개의 색종이로 나눈다. 이와 같은 과정을 잘라진 종이가 모두 하얀색 또는 모두 파란색으로 칠해져 있거나, 하나의 정사각형 칸이 되어 더 이상 자를 수 없을 때까지 반복한다.

위와 같은 규칙에 따라 잘랐을 때 <그림 3>은 <그림 1>의 종이를 처음 나눈 후의 상태를, <그림 4>는 두 번째 나눈 후의 상태를, <그림 5>는 최종적으로 만들어진 다양한 크기의 9장의 하얀색 색종이와 7장의 파란색 색종이를 보여주고 있다.

입력으로 주어진 종이의 한 변의 길이 N과 각 정사각형칸의 색(하얀색 또는 파란색)이 주어질 때 잘라진 하얀색 색종이와 파란색 색종이의 개수를 구하는 프로그램을 작성하시오.

입력

첫째 줄에는 전체 종이의 한 변의 길이 N이 주어져 있다. N은 2, 4, 8, 16, 32, 64, 128 중 하나이다. 색종이의 각 가로줄의 정사각형칸들의 색이 윗줄부터 차례로 둘째 줄부터 마지막 줄까지 주어진다. 하얀색으로 칠해진 칸은 0, 파란색으로 칠해진 칸은 1로 주어지며, 각 숫자 사이에는 빈칸이 하나씩 있다.

출력

첫째 줄에는 잘라진 햐얀색 색종이의 개수를 출력하고, 둘째 줄에는 파란색 색종이의 개수를 출력한다.

예제 입력 1 복사

8
1 1 0 0 0 0 1 1
1 1 0 0 0 0 1 1
0 0 0 0 1 1 0 0
0 0 0 0 1 1 0 0
1 0 0 0 1 1 1 1
0 1 0 0 1 1 1 1
0 0 1 1 1 1 1 1
0 0 1 1 1 1 1 1

예제 출력 1 복사

9
7

 

풀이

해당 문제를 정말 알고리즘 초보자의 입장에서 보았을 때 어떻게 풀어야 할까..?

남들은 이 문제가 쉬운 편이라고는 하지만 나같이 이런 수학문제를 처음 풀어보는 사람들은 문제를 이해하는 데에도 시간이 꽤 걸릴 것이다.

 

단순히 이럴 땐 문제를 말 그대로 처음부터 순서를 매겨가며 읽어가면 된다.

 

색종이의 모든 부분이 색상이 같을 때는 나누지 않는다.

하지만 색상이 같지 않다면 4등분으로 나눈다. (상좌 상우 하좌 하우)

나눈 색종이를 다시 색상이 같은지 구별한다.

나눈 색종이의 색상이 또 다 같지 않다면, 다시 4등분으로 나눈다.

 

여기서 알 수 있는 부분은 해당 문제는 재귀용법을 사용하는구나! 를 알 수가 있다.

그리고 마찬가지로 재귀용법에서 중요한 어느 부분에서 스탑을 할 것인가 라는 물음에 색종이의 모든 부분의 색상이 같을 때라고 대답할 수 있다.

 

해당 문제가 어렵게 느껴지는 이유 첫 번째는 2차원 배열을 이용하여 좌표로 나타내야 한다는 것이다.

 

단순히 생각해 그림을 그리듯이 문제를 풀어나가면 된다.

 

일단 우리는 스탑에 대한 조건을 모든 부분의 색상이 같을 때로 정해두었다.

 

그러니 해당 부분을 메서드로 먼저 선언해 준다.

 

    public static boolean checkColor(int row, int col, int size){
        int color = paper[row][col];
        for(int i=row; i<row+size; i++){
            for(int j=col; j<col+size; j++){
                if(paper[i][j]!=color){
                    return false;
                }
            }
        }
        return true;
    }

배열과 색상에 대한 카운트를 정적 변수로 선언해 주고, 체크하는 함수의 기준을 단순히 맨 첫 데이터로 두고 순회하면 된다.

 

그리고 해당 함수를 재귀함수의 브레이크 조건으로 넣어둔다.

 

        if(checkColor(row,col,size)){
            if(paper[row][col]==0){
                white++;
            }else{
                blue++;
            }
            return;
        }

이렇게 재귀함수의 필드에 색상이 다 같다면  을 조건으로 두고 같을 때 첫 데이터의 값이 0이라면 흰색의 카운트를 더해주고, 아니라면 파란색에 카운트를 더해준다.



그리고 그 밑에는 재귀함수를 호출하면 된다.

단순히 말 그대로 문제를 풀이해나가면 되기 때문에 상하좌우로 4등 분해 재귀함수를 호출하면 된다.

이렇게 하면 각 부분별로 모든 부분의 색상이 같을 때까지 나눠서 탐색을 하게 된다.

 

해당 부분이 분할 정복의 개념을 가지고 있다. (문제를 더 이상 쪼갤 수 없을 때까지 쪼갠 후 문제를 해결해나가는 것)

단순히 색종이를 더 이상 쪼갤 수 없을 때까지 쪼갠다면, 어떠한 부분은 1x1의 크기를 가진 색종이가 될 것이다. 그렇다면 멈추고 카운트를 더하는 식으로 문제를 해결해나가는 것이다.

 

    public static void divide(int row,int col,int size) {
        if(checkColor(row,col,size)){
            if(paper[row][col]==0){
                white++;
            }else{
                blue++;
            }
            return;
        }
        int newSize = size/2;
        divide(row,col,newSize);
        divide(row,col+newSize,newSize);
        divide(row+newSize,col,newSize);
        divide(row+newSize,col+newSize,newSize);
    }

이렇게 메서드를 호출해주면 각각 알맞은 타이밍에 반복이 멈추게 되면서 결과를 가져온다.

4등분을 해야 하기 때문에 각각 좌표로 나타내었을 때를 생각하며 코드를 작성해주면 된다.

 

package baekjoon.a2630;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class a2630 {
    public static int white = 0;
    public static int blue = 0;
    public static int[][] paper;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        paper = new int[N][N];
        for (int i = 0; i < N; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            for (int j = 0; j < N; j++) {
                paper[i][j] = Integer.parseInt(st.nextToken());
            }
        }
        divide(0,0,N);
        System.out.println(white);
        System.out.println(blue);
    }

    public static void divide(int row,int col,int size) {
        if(checkColor(row,col,size)){
            if(paper[row][col]==0){
                white++;
            }else{
                blue++;
            }
            return;
        }
        int newSize = size/2;
        divide(row,col,newSize);
        divide(row,col+newSize,newSize);
        divide(row+newSize,col,newSize);
        divide(row+newSize,col+newSize,newSize);
    }
    public static boolean checkColor(int row, int col, int size){
        int color = paper[row][col];
        for(int i=row; i<row+size; i++){
            for(int j=col; j<col+size; j++){
                if(paper[i][j]!=color){
                    return false;
                }
            }
        }
        return true;
    }
}

전체 코드로 나타내면 이렇다.

재귀호출 부분의 코드가 더러워 보일 수 있는데, 이럴 때 다시 2중 반복문으로 i+newSize로 재귀함수를 각각 호출하게 선언하면 깔끔하게 표현할 수 있다.

 

 

  Comments,     Trackbacks