*이 문제는 Programming Challenges : 알고리즘 트레이닝 북에 출제된 문제입니다.


지뢰 찾기(Minesweeper)

이 문제는 문제 자체가 어렵진 않지만 코드를 깔끔하게 작성하는 연습을 하기에는 좋은 문제이다. 코드를 깔끔하게 작성하기 좋은 팁은 다음과 같다.


1. 지뢰 찾기 맵을 char[][] map; 로 만들고, 지뢰가 아닌 곳은 아스키 문자 '0'을 넣는다.

    이렇게 하면 주위에 지뢰가 있을 때 map[i][j] ++를 호출해주면 자동으로 아스키 문자가 '1', '2', '3'.... 순서로 1씩 증가 된다.


2. 지뢰를 만나서 주변 8개 지뢰에 대한 카운트를 증가 시킬 때 해야 할 일을 함수로 빼낸다.

    "지뢰 주변"을 탐색하면서 맵을 벗어난 곳을 체크하기가 수월해진다.


3. 카운트 증가 조건에 맞으면 업데이트 하지 말고 조건에 맞지 않을 경우 리턴한다.

    리턴문(return)을 잘 활용하면 로직이 복잡해지는 것을 막을 수 있다.


아래는 직접 구현한 지뢰 찾기 소스이다.

class MineSweeper{

    Input input = new Input();

    private char[][]map;

    int n = 0;// row

    int m = 0;// column

    int iteration = 0;

    public void run(){

        while(true){

            readRowColumn();        // row(n)와 column(m) 값을 입력 받는다.

            if(isExit()) return;    // n == m == 0 이면 리턴한다.

            initMap();              // 맵을 생성한다.

            loadMap();              // 맵을 입력 받는다.

            calculateMap();         // 맵을 계산한다.

            printResult();          // 맵을 출력한다.

        }

    }

    private boolean isExit(){

        if(n == 0 && m == 0) return true;

        return false;

    }

    private void readRowColumn(){

        String line = input.readLine();

        StringTokenizer token = new StringTokenizer(line);

        n = Integer.parseInt(token.nextToken());// row

        m = Integer.parseInt(token.nextToken());// column

    }

    private void initMap(){

        map = new char[n][m];

    }

    private void loadMap(){

        for(int i = 0; i < n; i++){

        String line = input.readLine();

        for(int j = 0; j < m; j++){

        map[i][j] = line.charAt(j);

        if(map[i][j] == '.') map[i][j] = '0';

        }

        }

    }

    private void calculateMap(){

        for(int i = 0; i < n; i++){

            for(int j = 0; j < m; j++){

                if(map[i][j] == '*') addCountAround(i, j);

            }

        }

    }

    private void addCountAround(int x, int y){

        for(int i = x - 1; i <= x + 1; i++){

            for(int j = y - 1; j <= y + 1; j++){

                addCount(i, j);

            }

        }

    }

    private void addCount(int x, int y){

        if(x < 0 || x >= n) return;

        if(y < 0 || y >= m) return;

        if(map[x][y] == '*') return;

        map[x][y]++;

    }

    private void printResult(){

        iteration++;

        if(iteration != 1) System.out.println();

        System.out.println("Field #" + iteration + ":");

        for(int i = 0; i < n; i++){

            for(int j = 0; j < m; j++){

                System.out.print(map[i][j]);

            }

            System.out.println();

        }

    }

}


이 구현에서 핵심 부분은 calculateMap() 메소드와 이 메소드에 호출되는 addCountAround() 그리고 addCount() 메소드일 것이다.

calculateMap()은 가로와 세로 방향으로 for문을 돌아서 모든 맵을 방문하는 일을 담당한다. 만약 이 과정에서 지뢰(*)를 만나면 addCountAround(i, j) 메소드를 호출하는 것으로 자기 임무를 마친다.

addCountAround()는 지뢰 주변의 9개 타일(자기 자신 포함) 타일들을 방문한다. 그리고 addCount() 메소드를 호출하는 것으로 자기 임무를 마친다.

addCount() 메소드가 실질적으로 카운트를 증가시킨다. 이 때 중요한 것은 예외 처리이다. 만약 타일이 지뢰이거나 맵의 경계를 넘어갔다면 카운트를 증가시키면 안된다. 따라서 미리 예외의 경우들을 체크하고 적절한 타일이 아니라면 리턴한다. 그리고 정상적인 타일인 경우에면 카운트를 증가시킨다.


또한 run() 메소드에서는 문제에서 주어진 절차를 모두 메소드로 만들고 절차에 맞는 적절한 이름을 할당해 줌으로써 전체 동작이 어떤지를 한눈에 알 수 있도록 만들어 주는 것이 코드를 깔끔하게 만들 수 있는 비결이다.


중간에 Input 클래스를 사용하는데 이에 대해서는 [Programming Challenges]Sample 및 표준 입력 데이터 이중화 글을 참고하기 바란다.


Posted by 이세영2
,

programmingChallenges.zip


당분간 알고리즘 공부에 전념해보고자 전에 사서 킵 해 두었던 Programming Challenges 알고리즘 트레이닝 북을 꺼내 들었다.


이 책은 여러 알고리즘 문제를 난이도별, 특성 별로 나누어 제공하고 있고, 이에 대한 해답도 소스로 첨부되어 있다. 특히 온라인 페이지를 통해서 자기가 작성한 답안을 올리고 채점을 받을 수 있도록 되어 있어서 좋다. 또한 로컬에서 테스트를 해 볼 수 있도록 샘플 입력 데이터들도 제공한다. 아래는 온라인 채점 사이트이다.


http://www.programming-challenges.com/pg.php?page=index


Sample 및 표준 입력 데이터 이중화

위에서 설명한 것처럼 온라인으로도 채점이 가능하고 로컬에다가 샘플 데이터를 받아서 테스트를 해 볼 수 도 있다.

문제는 온라인에서 채점할 경우 내가 제출한 소스로 동작한 결과를 확인해 볼 수 없다는데 있다. 따라서 로컬에서 샘플 데이터를 통한 테스트가 필수이다. 하지만 샘플 데이터는 파일 입력이고, 온라인에서 소스에 데이터를 입력 받을 때는 표준 입출력(일반적으로 키보드 입력이라 부르는)이다. 따라서 온라인에 답안을 올릴 때와 샘플 데이터를 이용할 때의 입력 방식이 달라서 불편한 점이 있다.


자동으로 Sample 데이터와 표준 입력을 전환

위의 첨부 파일을 받아 보면 이미 Programming Challenges 사이트에서 제공하는 샘플 데이터가 Samples 폴더에 들어 있다.

자동으로 Sample 데이터와 표준 입력을 자동으로 전환하는 방법은 단순하다. 로컬에 샘플을 저장한 폴더에서 원하는 샘플 파일을 열어보고 예외가 발생하면 표준 입력을 사용하도록 하는 것이다.

이에 대한 소스는 아래와 같다.

class Input{

    private Scanner scan;

    private boolean isSampleSource = true;

    public Input(){ initInputSource(); }

    private void initInputSource(){

        Problem problem = new Problem();

        File file = new File("samples/" + problem.pcId +".inp");

        try {

            scan = new Scanner(file);

        } catch (FileNotFoundException e) {

            isSampleSource = false;

        }

    }

    public String readLine(){

        if(isSampleSource) return readLineFromSample();

        return Main.ReadLn(255);

    }

    private String readLineFromSample(){

        if(scan.hasNextLine()) return scan.nextLine();

        return null;

    }

}

class Problem{

    public String pcId = "110101";// PC ID

    public String uvaId = "100";// UVa ID

} 

Input 클래스는 생성시 생성자에서 initInputSource() 메소드를 호출한다. 이 메소드에서는 일단 Problem 클래스를 생성한 후 그 안에서 pcId 값을 읽어 파일명으로 변환한 후 samples 폴더에서 파일을 열어본다. 만약 파일이 열리면 파일을 입력 소스로 사용한다. 만약 파일을 여는데 실패하면 FileNotFoundException이 발생하고 isSampleSource 값을 false로 함으로써 표준 입출력을 사용하도록 되어 있다.


사용법

첨부 파일은 Eclipse 프로젝트이다. samples 폴더에는 샘플 데이터들이 들어 있다.

programmingChallenges.submitForm 패키지 아래에 있는 Main.java 파일은 위의 소스를 포함하여 알고리즘을 작성할 부분과 필요한 클래스를 작성해 넣을 수 있는 부분을 비워 둔 소스이다. 따라서 이 소스에 알고리즘을 작성해 넣으면 된다. 이 때 소스 파일 지정을 위해서 Problem 클래스 안에 pcId는 책에 나오는 PC Id를 입력해 주어야 한다.


programmingChallenges.threeNPlusOne 패키지 아래에 있는 Main.java 파일은 책에 나오는 첫번째 문제인 3n + 1 문제를 풀어 채워 넣은 파일이다. 실제로 채점 사이트에 올려 보고 해결되는 것을 확인해 본 소스이다. 물론 로컬에서 실행을 시켜 보면 로컬의 samples 폴더에 있는 테스트 데이터를 입력 받아 동작함을 알 수 있다.


혹시 Programming Challenges 알고리즘 트레이닝 북을 가지고 알고리즘을 공부하는 사람 중에서 Java를 사용할 의향이 있다면 편리하게 사용할 수 있을 것이다.

'7.알고리즘' 카테고리의 다른 글

[Programming Challenges] 지뢰 찾기(Minesweeper)  (0) 2016.10.06
Posted by 이세영2
,