www.acmicpc.net/problem/2580

 

2580번: 스도쿠

스도쿠는 18세기 스위스 수학자가 만든 '라틴 사각형'이랑 퍼즐에서 유래한 것으로 현재 많은 인기를 누리고 있다. 이 게임은 아래 그림과 같이 가로, 세로 각각 9개씩 총 81개의 작은 칸으로 이루

www.acmicpc.net

문제

스도쿠는 18세기 스위스 수학자가 만든 '라틴 사각형'이랑 퍼즐에서 유래한 것으로 현재 많은 인기를 누리고 있다. 이 게임은 아래 그림과 같이 가로, 세로 각각 9개씩 총 81개의 작은 칸으로 이루어진 정사각형 판 위에서 이뤄지는데, 게임 시작 전 일부 칸에는 1부터 9까지의 숫자 중 하나가 쓰여 있다.

나머지 빈 칸을 채우는 방식은 다음과 같다.

  1. 각각의 가로줄과 세로줄에는 1부터 9까지의 숫자가 한 번씩만 나타나야 한다.
  2. 굵은 선으로 구분되어 있는 3x3 정사각형 안에도 1부터 9까지의 숫자가 한 번씩만 나타나야 한다.

위의 예의 경우, 첫째 줄에는 1을 제외한 나머지 2부터 9까지의 숫자들이 이미 나타나 있으므로 첫째 줄 빈칸에는 1이 들어가야 한다.

또한 위쪽 가운데 위치한 3x3 정사각형의 경우에는 3을 제외한 나머지 숫자들이 이미 쓰여있으므로 가운데 빈 칸에는 3이 들어가야 한다.

이와 같이 빈 칸을 차례로 채워 가면 다음과 같은 최종 결과를 얻을 수 있다.

게임 시작 전 스도쿠 판에 쓰여 있는 숫자들의 정보가 주어질 때 모든 빈 칸이 채워진 최종 모습을 출력하는 프로그램을 작성하시오.

입력

아홉 줄에 걸쳐 한 줄에 9개씩 게임 시작 전 스도쿠판 각 줄에 쓰여 있는 숫자가 한 칸씩 띄워서 차례로 주어진다. 스도쿠 판의 빈 칸의 경우에는 0이 주어진다. 스도쿠 판을 규칙대로 채울 수 없는 경우의 입력은 주어지지 않는다.

출력

모든 빈 칸이 채워진 스도쿠 판의 최종 모습을 아홉 줄에 걸쳐 한 줄에 9개씩 한 칸씩 띄워서 출력한다.

스도쿠 판을 채우는 방법이 여럿인 경우는 그 중 하나만을 출력한다.

제한

  • baekjoon의 백트래킹 알고리즘으로 풀 수 있는 입력만 주어진다. 다음은 그 알고리즘의 수행 시간이다.
    • C++14: 80ms
    • Java: 292ms
    • PyPy3: 1172ms

예제 입력 1 복사

0 3 5 4 6 9 2 7 8

7 8 2 1 0 5 6 0 9

0 6 0 2 7 8 1 3 5

3 2 1 0 4 6 8 9 7

8 0 4 9 1 3 5 0 6

5 9 6 8 2 0 4 1 3

9 1 7 6 5 2 0 8 0

6 0 3 7 0 1 9 5 2

2 5 8 3 9 4 7 6 0

예제 출력 1 복사

1 3 5 4 6 9 2 7 8

7 8 2 1 3 5 6 4 9

4 6 9 2 7 8 1 3 5

3 2 1 5 4 6 8 9 7

8 7 4 9 1 3 5 2 6

5 9 6 8 2 7 4 1 3

9 1 7 6 5 2 3 8 4

6 4 3 7 8 1 9 5 2

2 5 8 3 9 4 7 6 1

 

 

 

 

 

 

 

풀이 .

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

public class Main {
    static BufferedReader br = null;
    static StringTokenizer st = null;
    static int[][] map = null;
    static int[][] zero = null;
    static boolean[][] check = null;
    static int zeroCnt;

    public static boolean isPossible(int r, int c) {
        // 행 검사
        for(int i = 0; i < 9; i++) {
            if(i == c) continue;
            if(map[r][i] == map[r][c]) {
                return false;
            }
        }

        // 열 검사
        for(int i = 0; i < 9; i++) {
            if(i == r) continue;
            if(map[i][c] == map[r][c]) {
                return false;
            }
        }

        // 3x3 사각형 검사
        int row = r / 3 * 3;
        int col = c / 3 * 3;
        for(int i = row; i < row + 3; i++) {
            for(int j = col; j < col + 3; j++) {
                if(i == r && j == c) continue;
                if(map[i][j] == map[r][c]) {
                    return false;
                }
            }
        }

        return true;
    }


    public static void dfs(int deptNow, int dept) {
        if(deptNow == dept) {  // 모든 0 처리 완료
            for(int i = 0; i < 9; i++) {
                for(int j = 0; j < 9; j++) {
                    System.out.print(map[i][j] + " ");
                }
                System.out.println();
            }
            System.exit(0);
        }

        int r = zero[deptNow][0];
        int c = zero[deptNow][1];
        for(int i = 1; i <= 9; i++) {
            map[r][c] = i;
            if(isPossible(r, c)) {  // 새로운 칸 채울 때마다 검사해서 불필요한 재귀를 막는다.
                dfs(deptNow + 1, dept);
            }
            map[r][c] = 0;
        }
    }

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        map = new int[9][9];
        zero = new int[81][2];
        check = new boolean[9][9];

        for(int i = 0; i < 9; i++) {
            st = new StringTokenizer(br.readLine());
            for(int j = 0; j < 9; j++) {
                map[i][j] = Integer.parseInt(st.nextToken());
                if(map[i][j] == 0) {
                    zero[zeroCnt][0] = i;
                    zero[zeroCnt][1] = j;
                    zeroCnt += 1;
                }
            }
        }

        int r = zero[0][0];
        int c = zero[0][1];
        for(int i = 1; i <= 9; i++) {
            map[r][c] = i;
            if(isPossible(r, c)) {
                dfs(1, zeroCnt);
            }
            map[r][c] = 0;
        }
    }
}

 

정답이 여러 개인 경우엔 하나만 출력하고 재귀를 멈춰야 한다.

 

이걸 못 봐서 한동안 맞왜틀을 시전하고 있었다.

System.exit(0) 을 사용해서 프로그램 자체를 종료시켜 버릴 수 있다.

 

근데 설명이 조금 애매하다 싶기도 하다. 여러 개의 정답 중 어떤 걸 출력하라는 얘기?

그냥 눈치껏 가장 먼저 등장하는 정답을 출력하도록 했더니 정답 처리 되었다.

 

 

 

 

'알고리즘 문제 > 백준 온라인 저지' 카테고리의 다른 글

[BOJ] 6603 - 로또 JAVA  (0) 2021.02.15
[BOJ] 1987 - 알파벳 JAVA  (0) 2021.02.15
[BOJ] 1759 - 암호 만들기 JAVA  (0) 2021.02.14
[BOJ] 5014 - 스타트링크 JAVA  (0) 2021.02.14
[BOJ] 3108 - 로고 JAVA  (0) 2021.02.14

www.acmicpc.net/problem/1759

 

1759번: 암호 만들기

첫째 줄에 두 정수 L, C가 주어진다. (3 ≤ L ≤ C ≤ 15) 다음 줄에는 C개의 문자들이 공백으로 구분되어 주어진다. 주어지는 문자들은 알파벳 소문자이며, 중복되는 것은 없다.

www.acmicpc.net

문제

바로 어제 최백준 조교가 방 열쇠를 주머니에 넣은 채 깜빡하고 서울로 가 버리는 황당한 상황에 직면한 조교들은, 702호에 새로운 보안 시스템을 설치하기로 하였다. 이 보안 시스템은 열쇠가 아닌 암호로 동작하게 되어 있는 시스템이다.

암호는 서로 다른 L개의 알파벳 소문자들로 구성되며 최소 한 개의 모음(a, e, i, o, u)과 최소 두 개의 자음으로 구성되어 있다고 알려져 있다. 또한 정렬된 문자열을 선호하는 조교들의 성향으로 미루어 보아 암호를 이루는 알파벳이 암호에서 증가하는 순서로 배열되었을 것이라고 추측된다. 즉, abc는 가능성이 있는 암호이지만 bac는 그렇지 않다.

새 보안 시스템에서 조교들이 암호로 사용했을 법한 문자의 종류는 C가지가 있다고 한다. 이 알파벳을 입수한 민식, 영식 형제는 조교들의 방에 침투하기 위해 암호를 추측해 보려고 한다. C개의 문자들이 모두 주어졌을 때, 가능성 있는 암호들을 모두 구하는 프로그램을 작성하시오.

입력

첫째 줄에 두 정수 L, C가 주어진다. (3 ≤ L ≤ C ≤ 15) 다음 줄에는 C개의 문자들이 공백으로 구분되어 주어진다. 주어지는 문자들은 알파벳 소문자이며, 중복되는 것은 없다.

출력

각 줄에 하나씩, 사전식으로 가능성 있는 암호를 모두 출력한다.

예제 입력 1

4 6

a t c i s w

예제 출력 1

acis

acit

aciw

acst

acsw

actw

aist

aisw

aitw

astw

cist

cisw

citw

istw

 

 

 

 

 

 

 

풀이 .

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

public class Main {
    static BufferedReader br = null;
    static StringTokenizer st = null;
    static StringBuilder sb = null;
    static ArrayList<String> ans = null;
    static char[] arr = null;
    static boolean[] check = null;
    static int L, C;

    public static boolean isPossible() {
        int consonant = 0, vowel = 0;
        for(int i = 0; i < sb.length(); i++) {
            char c = sb.charAt(i);
            if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
                vowel += 1;
            }else {
                consonant += 1;
            }
        }
        // 사실 입력받은 문자들은 중복되지 않는 것이 보장되기 때문에 contains()는 검사할 필요 없음
        if(consonant >= 2 && vowel >= 1 && !ans.contains(sb.toString())) {
            return true;
        }else {
            return false;
        }
    }

    public static void dfs(int deptNow, int dept) {
        if(deptNow == dept) {
            if(isPossible()) {
                ans.add(sb.toString());
            }
            return;
        }

        for(int i = 0; i < C; i++) {
            if(!check[i] && sb.charAt(sb.length() - 1) < arr[i]) {
                check[i] = true;
                sb.append(arr[i]);
                dfs(deptNow + 1, dept);
                check[i] = false;
                sb.deleteCharAt(sb.length() - 1);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        st = new StringTokenizer(br.readLine());
        sb = new StringBuilder();
        L = Integer.parseInt(st.nextToken());
        C = Integer.parseInt(st.nextToken());

        ans = new ArrayList<>();
        arr = new char[C];
        check = new boolean[C];
        st = new StringTokenizer(br.readLine());
        for(int i = 0; i < C; i++) {
            arr[i] = st.nextToken().charAt(0);
        }
        Arrays.sort(arr);

        for(int i = 0; i < C; i++) {
            check[i] = true;
            sb.append(arr[i]);
            dfs(1, L);
            check[i] = false;
            sb.deleteCharAt(sb.length() - 1);
        }

        Collections.sort(ans);
        for(String str : ans) {
            System.out.println(str);
        }
    }
}

 

백트래킹을 사용해 모든 조합을 구한다.

 

단, 조합을 만드는 과정에서 문제의 제한사항을 잘 적용시켜야 한다.

(오름차순 정렬, 자음과 모음 개수 등)

 

char[]를 쓸까 String을 쓸까 StringBuilder를 쓸까 쓸 데 없는 고민으로 코드를 이리저리 바꿔가며 시간을 잡아먹었다.

 

어려운 문제는 아니니 실전에서는 아무거나 하나 잡고 빠르게 풀자.

 

 

 

 

'알고리즘 문제 > 백준 온라인 저지' 카테고리의 다른 글

[BOJ] 1987 - 알파벳 JAVA  (0) 2021.02.15
[BOJ] 2580 - 스도쿠 JAVA  (0) 2021.02.15
[BOJ] 5014 - 스타트링크 JAVA  (0) 2021.02.14
[BOJ] 3108 - 로고 JAVA  (0) 2021.02.14
[BOJ] 2186 - 문자판 JAVA  (0) 2021.02.13

www.acmicpc.net/problem/5014

 

5014번: 스타트링크

첫째 줄에 F, S, G, U, D가 주어진다. (1 ≤ S, G ≤ F ≤ 1000000, 0 ≤ U, D ≤ 1000000) 건물은 1층부터 시작하고, 가장 높은 층은 F층이다.

www.acmicpc.net

문제

강호는 코딩 교육을 하는 스타트업 스타트링크에 지원했다. 오늘은 강호의 면접날이다. 하지만, 늦잠을 잔 강호는 스타트링크가 있는 건물에 늦게 도착하고 말았다.

스타트링크는 총 F층으로 이루어진 고층 건물에 사무실이 있고, 스타트링크가 있는 곳의 위치는 G층이다. 강호가 지금 있는 곳은 S층이고, 이제 엘리베이터를 타고 G층으로 이동하려고 한다.

보통 엘리베이터에는 어떤 층으로 이동할 수 있는 버튼이 있지만, 강호가 탄 엘리베이터는 버튼이 2개밖에 없다. U버튼은 위로 U층을 가는 버튼, D버튼은 아래로 D층을 가는 버튼이다. (만약, U층 위, 또는 D층 아래에 해당하는 층이 없을 때는, 엘리베이터는 움직이지 않는다)

강호가 G층에 도착하려면, 버튼을 적어도 몇 번 눌러야 하는지 구하는 프로그램을 작성하시오. 만약, 엘리베이터를 이용해서 G층에 갈 수 없다면, "use the stairs"를 출력한다.

입력

첫째 줄에 F, S, G, U, D가 주어진다. (1 ≤ S, G ≤ F ≤ 1000000, 0 ≤ U, D ≤ 1000000) 건물은 1층부터 시작하고, 가장 높은 층은 F층이다.

출력

첫째 줄에 강호가 S층에서 G층으로 가기 위해 눌러야 하는 버튼의 수의 최솟값을 출력한다. 만약, 엘리베이터로 이동할 수 없을 때는 "use the stairs"를 출력한다.

예제 입력 1

10 1 10 2 1

예제 출력 1

6

예제 입력 2

100 2 1 1 0

예제 출력 2

use the stairs

 

 

 

 

 

 

 

 

풀이 .

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

public class Main {
    static BufferedReader br = null;
    static StringTokenizer st = null;
    static int F, S, G, U, D, ans = -1;
    static int[] map = null;
    static boolean[] check = null;

    public static void bfs() {
        Queue<Integer> que = new ArrayDeque<>();
        que.add(S);
        map[S] = 0;
        check[S] = true;

        while(!que.isEmpty()) {
            int now = que.poll();
            int up = now + U;
            int down = now - D;

            if(up == G || down == G) {  // 최단경로 찾으면 굳이 탐색 이어갈 필요 없음
                ans = map[now] + 1;
                return;
            }

            if(up <= F && !check[up]) {
                que.add(up);
                map[up] = map[now] + 1;
                check[up] = true;
            }
            if(1 <= down && !check[down]) {
                que.add(down);
                map[down] = map[now] + 1;
                check[down] = true;
            }
        }
    }

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        st = new StringTokenizer(br.readLine());

        // F, S, G, U, D
        F = Integer.parseInt(st.nextToken());
        S = Integer.parseInt(st.nextToken());
        G = Integer.parseInt(st.nextToken());
        U = Integer.parseInt(st.nextToken());
        D = Integer.parseInt(st.nextToken());

        if(S == G) {  // 예외처리
            System.out.println(0);
            return;
        }

        map = new int[F + 1];  // map[idx] : idx층에 가기 위한 최소 버튼 수
        check = new boolean[F + 1];

        bfs();
        System.out.println((ans == -1) ? "use the stairs" : ans);
    }
}

 

전형적인 BFS를 사용한 최단경로 문제.

 

if(S == G) 의 예외처리에서 return을 안 넣어서 틀린 걸 멍청하게 한참 찾았다.

 

 

 

'알고리즘 문제 > 백준 온라인 저지' 카테고리의 다른 글

[BOJ] 2580 - 스도쿠 JAVA  (0) 2021.02.15
[BOJ] 1759 - 암호 만들기 JAVA  (0) 2021.02.14
[BOJ] 3108 - 로고 JAVA  (0) 2021.02.14
[BOJ] 2186 - 문자판 JAVA  (0) 2021.02.13
[BOJ] 1525 - 퍼즐 JAVA  (0) 2021.02.12

www.acmicpc.net/problem/3108

 

3108번: 로고

로고는 주로 교육용에 쓰이는 프로그래밍 언어이다. 로고의 가장 큰 특징은 거북이 로봇인데, 사용자는 이 거북이 로봇을 움직이는 명령을 입력해 화면에 도형을 그릴 수 있다. 거북이는 위치와

www.acmicpc.net

문제

로고는 주로 교육용에 쓰이는 프로그래밍 언어이다. 로고의 가장 큰 특징은 거북이 로봇인데, 사용자는 이 거북이 로봇을 움직이는 명령을 입력해 화면에 도형을 그릴 수 있다.

거북이는 위치와 각도로 표현할 수 있다. 거북이는 입에 연필을 물고 있는데, 연필을 내리면 움직일 때 화면에 선을 그리고, 올리면 선을 그리지 않고 그냥 지나가기만 한다.

제일 처음에 거북이는 (0,0)에 있고, 거북이가 보고 있는 방향은 y축이 증가하는 방향이다. 또한 연필은 내리고 있다.

사용자는 다음과 같은 다섯가지 명령으로 거북이를 조정할 수 있다.

  1. FD x: 거북이를 x만큼 앞으로 전진
  2. LT a: 거북이를 반시계 방향으로 a도 만큼 회전
  3. RT a: 거북이를 시계 방향으로 a도 만큼 회전
  4. PU: 연필을 올린다
  5. PD: 연필을 내린다.

축에 평행한 직사각형 N개가 주어졌을 때, 이 직사각형을 그리는데 필요한 PU 명령의 최솟값을 구하는 프로그램을 작성하시오.

거북이는 같은 선을 여러 번 그릴 수 있지만, 문제에 주어진 직사각형 N개를 제외한 어떤 것도 그릴 수 없다. 거북이의 크기는 아주 작아서 좌표 평면의 한 점이라고 생각하면 된다. 직사각형의 변은 축에 평행하다.

입력

첫째 줄에 직사각형의 개수 N이 주어진다. (1 ≤ N ≤ 1000)

다음 N개의 줄에는 직사각형의 좌표 x1, y1, x2, y2가 주어진다. (−500 ≤ x1 < x2 ≤ 500), (−500 ≤ y1 < y2 ≤ 500) (x1, y1)는 직사각형의 한 꼭짓점 좌표이고, (x2, y2)는 그 점의 대각선 방향의 반대 꼭짓점의 좌표이다.

출력

N개의 직사각형을 그리는 필요한 PU명령의 최솟값을 출력한다.

예제 입력 1

5

1 1 4 4

3 3 6 6

4 4 5 5

5 0 8 3

6 1 7 2

예제 출력 1

2

 

 

 

 

 

 

 

풀이 1. (틀린 코드, 접근법 자체가 틀렸다)

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

class Pair implements Comparable<Pair>{
    int r;
    int c;
    @Override
    public int compareTo(Pair o) {
        return this.r < o.r ? -1 : 1;  // 대각선 방향에 있으므로 row만 검사해도 충분하다
    }
}

public class Main {
    static BufferedReader br = null;
    static StringTokenizer st = null;
    static int[][] map = null;
    static Pair[][] input = null;
    static boolean[][] check = null;

    static int n, maxRow, maxCol;
    static int[] rArr = {-1, 1, 0, 0};
    static int[] cArr = {0, 0, -1, 1};

    public static void dfs(int r, int c) {
        for(int i = 0; i < 4; i++) {
            int nr = r + rArr[i];
            int nc = c + cArr[i];
            if(-1 < nr && nr <= maxRow && -1 < nc && nc <= maxCol) {
                if(!check[nr][nc] && map[nr][nc] == 1) {
                    check[nr][nc] = true;
                    dfs(nr, nc);
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        n = Integer.parseInt(br.readLine());
        input = new Pair[n][2];  // input[] = 직사각형의 꼭지점 Pair 두개를 저장

        // x, y 입력은 그냥 row, col 입력으로 본다. row, col 순서가 바뀌게 되지만 무방하다.
        for(int i = 0; i < n; i++) {
            st = new StringTokenizer(br.readLine());
            input[i][0] = new Pair();
            input[i][1] = new Pair();
            for(int j = 0; j < 4; j++) {
                // map을 배열로 표현하기 위해 좌표평면 범위를 -500~500에서 0~1000으로 변경
                // 입력을 받으면서 max row, max col을 찾는다.
                int temp = Integer.parseInt(st.nextToken());
                if(j == 0) input[i][0].r = temp;
                else if(j == 1) input[i][0].c = temp;
                else if(j == 2) input[i][1].r = temp;
                else if(j == 3) input[i][1].c = temp;

                // map 크기를 정하기 위한 find max row, max col
                if(j % 2 == 0) maxRow = Math.max(maxRow, temp);
                else maxCol = Math.max(maxCol, temp);
            }
            Arrays.sort(input[i]);  // 더 큰 좌표의 꼭지점부터 입력된다는 보장 없음. 정렬 필요.
        }

        // 입력받은 꼭지점을 사용해 직사각형의 모든 좌표를 찾아 표시
        map = new int[maxRow + 1][maxCol + 1];
        check = new boolean[maxRow + 1][maxCol + 1];
        for(int i = 0; i < n; i++) {
            int r1 = input[i][0].r, c1 = input[i][0].c;
            int r2 = input[i][1].r, c2 = input[i][1].c;
            while(r1 <= r2) map[r1++][c1] = 1; r1--;
            while(c1 <= c2) map[r1][c1++] = 1; c1--;

            r1 = input[i][0].r;
            c1 = input[i][0].c;
            while(c1 <= c2) map[r1][c1++] = 1; c1--;
            while(r1 <= r2) map[r1++][c1] = 1; r1--;
        }

        int ans = 0;
        for(int i = 0; i <= maxRow; i++) {
            for(int j = 0; j <= maxCol; j++) {
                if(!check[i][j] && map[i][j] == 1) {
                    check[i][j] = true;
                    dfs(i, j);
                    ans += 1;
                }
            }
        }
        if(map[500][500] == 1) ans -= 1;  // 시작 위치가 사각형의 변에 들어가는 경우엔 1을 빼준다.
        System.out.println(ans);
    }
}

 

구현은 헷갈리는 부분이 많지만 개념 자체는 쉬운 문제라고 생각했다.

 

완벽한 착각.

 

사각형의 변이 지나는 모든 좌표에 점을 찍은 후 DFS를 돌려서 Component의 개수를 세는 문제라고 생각했다.

하지만 예제 입출력부터 틀려버린다.

 

내 생각대로라면 예제에서 주어진 사각형의 좌표에 점을 찍으면 아래 사진같은 형태가 나오는데, 

이런 형태이기 때문에 PU를 한 번만 수행하면 그 이후론 모든 사각형을 한 번에 그려낼 수 있을 거라 생각했다.

 

하지만 사각형의 변은 '선'이다. 저렇게 1씩 떨어져있는 점들로만 이뤄지지 않는다는 것이다.

그냥 단순하게 DFS 탐색 형태를 생각하다보니 스스로 함정에 빠져버렸다.

 

점이 아닌 선을 찍어봐야 제대로 된 모양을 알 수 있다.

선을 이으면 아래와 같다.

우측 하단의 동심사각형에서 PU를 한 번 더 실행해야 한다는 걸 알 수 있다.

 

결국 접근법 자체를 처음부터 다시 생각해야 한다.

 

 

 

 

 

풀이 2. (틀린 코드)

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

class Pair implements Comparable<Pair>{
    int r;
    int c;
    @Override
    public int compareTo(Pair o) {
        return this.r < o.r ? -1 : 1;  // 대각선 방향에 있으므로 row만 검사해도 충분하다
    }
}

public class Main {
    static BufferedReader br = null;
    static StringTokenizer st = null;
    static int[][] map = null;
    static Pair[][] input = null;
    static int n, ans, maxRow, maxCol;

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        n = Integer.parseInt(br.readLine());
        input = new Pair[n][2];  // input[] = 직사각형의 꼭지점 Pair 두개를 저장

        // x, y 입력은 그냥 row, col 입력으로 본다. row, col 순서가 바뀌게 되지만 무방하다.
        for(int i = 0; i < n; i++) {
            st = new StringTokenizer(br.readLine());
            input[i][0] = new Pair();
            input[i][1] = new Pair();
            for(int j = 0; j < 4; j++) {
                // map을 배열로 표현하기 위해 좌표평면 범위를 -500~500에서 0~1000으로 변경
                // 입력을 받으면서 max row, max col을 찾는다.
                int temp = Integer.parseInt(st.nextToken()) + 500;
                if(j == 0) input[i][0].r = temp;
                else if(j == 1) input[i][0].c = temp;
                else if(j == 2) input[i][1].r = temp;
                else if(j == 3) input[i][1].c = temp;

                // map 크기를 정하기 위한 find max row, max col
                if(j % 2 == 0) maxRow = Math.max(maxRow, temp);
                else maxCol = Math.max(maxCol, temp);
            }
            Arrays.sort(input[i]);  // 더 큰 좌표의 꼭지점부터 입력된다는 보장 없음. 정렬 필요.
        }

        // 입력받은 꼭지점을 사용해 직사각형의 모든 좌표를 찾아 표시
        map = new int[maxRow + 1][maxCol + 1];
        for(int i = 0; i < n; i++) {
            int r1 = input[i][0].r, c1 = input[i][0].c;
            int r2 = input[i][1].r, c2 = input[i][1].c;

            boolean isOverlap = false;
            while(r1 <= r2) {
                if(map[r1][c1] == 0) map[r1][c1] = 1;
                else isOverlap = true;
                r1 += 1;
            }
            r1--;

            c1++;
            while(c1 <= c2) {
                if(map[r1][c1] == 0) map[r1][c1] = 1;
                else isOverlap = true;
                c1 += 1;
            }
            c1--;

            r1 = input[i][0].r;
            c1 = input[i][0].c + 1;
            while(c1 <= c2) {
                if(map[r1][c1] == 0) map[r1][c1] = 1;
                else isOverlap = true;
                c1 += 1;
            }
            c1--;

            r1++;
            while(r1 != r2) {
                if(map[r1][c1] == 0) map[r1][c1]= 1;
                else isOverlap = true;
                r1 += 1;
            }
            r1--;

            if(!isOverlap) ans += 1;
        }

        if(map[500][500] == 1) ans -= 1;  // 시작 위치가 사각형의 변에 들어가는 경우엔 1을 빼준다.
        System.out.println(ans);
    }
}

 

사각형 경로에 있는 점을 하나씩 찍어나갈 때, 이미 찍혀있는 점에 또 찍어야 하는 경우는 이전에 그린 사각형과 겹친다는 뜻.

 

즉, 겹칠 때는 ans += 1을 하지 않고 겹치지 않을 때만 ans += 1을 하는 식으로 고쳤다.

 

솔직히 아직도 왜 틀린지 모르겠다.

 

아무 도움도 받지 않고 처음부터 내가 짠 코드를 포기하지 못 해서 이런식으로 시간을 날리게 되는 경우가 너무 많다.

안 되겠다 싶을 때는 과감하게 전부 엎어버리고 남의 코드를 보는 게 맞는 걸 알면서도 그러기가 쉽지 않다.

 

 

 

 

 

 

풀이 3. (정답 코드)

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

class Rectangle {
    int x1, y1, x2, y2;
    Rectangle(int x1, int y1, int x2, int y2) {
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
    }
}

public class Main {
    static BufferedReader br = null;
    static StringTokenizer st = null;
    static Rectangle[] map = null;
    static boolean[] check = null;
    static int n, ans;

    public static boolean isOverlap(Rectangle a, Rectangle b) {  // 사각형 겹침 여부 판단
        int aLeft, aRight, aTop, aBottom;
        int bLeft, bRight, bTop, bBottom;

        // 아래 조건 중 하나라도 TRUE라면 두 사각형은 절대 겹치지 않는다.
        // 1. A의 오른쪽이 B의 왼쪽보다 더 왼쪽에 있는 경우
        // 2. A의 왼쪽이 B의 오른쪽보다 더 오른쪽에 있는 경우
        // 3. A의 위쪽이 B의 아래쪽보다 더 아래쪽에 있는 경우
        // 4. A의 아래쪽이 B의 위쪽보다 더 위쪽에 있는 경우
        // 여기에 추가로 한 사각형이 다른 사각형 안에 내포된 경우까지 고려해야 함

        // 입력받은 두 점이 정렬 되어있다는 보장이 없으므로 왼, 오, 위, 아래 전부 직접 찾아줘야 한다.
        
        // find a left, right
        if(a.x1 < a.x2) { aLeft = a.x1; aRight = a.x2; }
        else { aLeft = a.x2;aRight = a.x1; }

        // find a top, bottom
        if(a.y1 < a.y2) { aBottom = a.y1; aTop = a.y2; }
        else { aBottom = a.y2; aTop = a.y1; }

        // find b left, right
        if(b.x1 < b.x2) { bLeft = b.x1; bRight = b.x2; }
        else { bLeft = b.x2; bRight = b.x1; }

        // find b top, bottom
        if(b.y1 < b.y2) { bBottom = b.y1; bTop = b.y2; }
        else { bBottom = b.y2; bTop = b.y1; }

        if(aLeft > bRight || aRight < bLeft || aTop < bBottom || aBottom > bTop ||
                (aLeft < bLeft && aRight > bRight && aTop > bTop && aBottom < bBottom) ||  // a가 b를 내포
                (bLeft < aLeft && bRight > aRight && bTop > aTop && bBottom < aBottom)) {  // b가 a를 내포
            return false;
        }else {
            return true;
        }
    }

    public static void dfs(int idx) {  // 점 단위가 아니라 하나의 사각형을 최소단위로 하여 탐색한다.
        for(int i = 0; i < n; i++) {
            if(!check[i] && isOverlap(map[i], map[idx])) {  // 사각형이 겹치면 이동 가능
                check[i] = true;
                dfs(i);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        n = Integer.parseInt(br.readLine());
        map = new Rectangle[n];
        check = new boolean[n];

        for(int i = 0; i < n; i++) {
            st = new StringTokenizer(br.readLine());
            int x1 = Integer.parseInt(st.nextToken());
            int y1 = Integer.parseInt(st.nextToken());
            int x2 = Integer.parseInt(st.nextToken());
            int y2 = Integer.parseInt(st.nextToken());
            map[i] = new Rectangle(x1, y1, x2, y2);

            // 시작점에 사각형이 있는 경우
            if((x1 == 0 && (y1 <= 0 && y2 >= 0)) || (x2 == 0 && (y1 <= 0 && y2 >= 0)))
                ans = -1;
        }

        for(int i = 0; i < n; i++) {
            if(!check[i]){
                check[i] = true;
                dfs(i);
                ans += 1;
            }
        }
        System.out.println(ans);
    }
}

 

훨씬 깔끔한 코드.

 

굳이 하나의 점을 최소단위로 잡아야만 DFS, BFS가 가능한 게 아니란 것을 배웠다.

하나의 사각형을 단위로 잡아 DFS를 전개했다.

 

사각형이 겹치면 인접한 노드로 판단하여 탐색을 전개할 수 있다.

 

이 때, 겹침 여부를 판단할 때 한 번 더 함정에 빠졌다.

 

일반적으로 사용되는 사각형 겹침 조건으로 겹침 여부를 판단하면 한 사각형이 다른 사각형을 완전히 내포하고있는 경우에서 오류가 발생한다.

 

사각형의 면적은 겹치는 게 맞지만 문제에서는 사각형의 변이 겹치는지를 확인해야하기 때문에 조건을 추가하여 검사해야 한다.

 

 

 

 

'알고리즘 문제 > 백준 온라인 저지' 카테고리의 다른 글

[BOJ] 1759 - 암호 만들기 JAVA  (0) 2021.02.14
[BOJ] 5014 - 스타트링크 JAVA  (0) 2021.02.14
[BOJ] 2186 - 문자판 JAVA  (0) 2021.02.13
[BOJ] 1525 - 퍼즐 JAVA  (0) 2021.02.12
[BOJ] 2251 - 물통 JAVA  (0) 2021.02.12

www.acmicpc.net/problem/2186

 

2186번: 문자판

첫째 줄에 N(1 ≤ N ≤ 100), M(1 ≤ M ≤ 100), K(1 ≤ K ≤ 5)가 주어진다. 다음 N개의 줄에는 M개의 알파벳 대문자가 주어지는데, 이는 N×M 크기의 문자판을 나타낸다. 다음 줄에는 1자 이상 80자 이하의

www.acmicpc.net

문제

알파벳 대문자가 한 칸에 한 개씩 적혀있는 N×M 크기의 문자판이 있다. 편의상 모든 문자는 대문자라 생각하자. 예를 들어 아래와 같은 문자판을 보자.

K A K T
X E A S
Y R W U
Z B Q P

이 문자판의 한 칸(아무 칸이나 상관없음)에서 시작하여 움직이면서, 그 칸에 적혀 있는 문자들을 차례대로 모으면 하나의 단어를 만들 수 있다. 움직일 때는 상하좌우로 K개의 칸까지만 이동할 수 있다. 예를 들어 K=2일 때 아래의 그림의 가운데에서는 'X' 표시된 곳으로 이동할 수 있다.

    X    
    X    
X X   X X
    X    
    X    

반드시 한 칸 이상 이동을 해야 하고, 같은 자리에 머물러 있을 수 없다. 또, 같은 칸을 여러 번 방문할 수 있다.

이와 같은 문자판과 K, 그리고 하나의 영단어가 주어졌을 때, 이와 같은 영단어를 만들 수 있는 경로가 총 몇 개 존재하는지 알아내는 프로그램을 작성하시오.

위의 예에서 영단어가 BREAK인 경우에는 다음과 같이 3개의 경로가 존재한다. 앞의 수는 행 번호, 뒤의 수는 열 번호를 나타낸다.

  • (4, 2) (3, 2) (2, 2) (1, 2) (1, 1)
  • (4, 2) (3, 2) (2, 2) (1, 2) (1, 3)
  • (4, 2) (3, 2) (2, 2) (2, 3) (1, 3)

입력

첫째 줄에 N(1 ≤ N ≤ 100), M(1 ≤ M ≤ 100), K(1 ≤ K ≤ 5)가 주어진다. 다음 N개의 줄에는 M개의 알파벳 대문자가 주어지는데, 이는 N×M 크기의 문자판을 나타낸다. 다음 줄에는 1자 이상 80자 이하의 영단어가 주어진다. 모든 문자들은 알파벳 대문자이며, 공백 없이 주어진다.

출력

첫째 줄에 경로의 개수를 출력한다. 이 값은 int 범위이다.

예제 입력 1

4 4 1

KAKT

XEAS

YRWU

ZBQP

BREAK

예제 출력 1

3

 

 

 

 

 

 

 

 

풀이 .

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

public class Main {
    static BufferedReader br = null;
    static StringTokenizer st = null;
    static char[][] map = null;
    static int[][][] dp = null;
    static int[] rArr = {-1, 1, 0, 0};
    static int[] cArr = {0, 0, -1, 1};
    static String target = null;
    static int n, m, k, ans;

    public static void dfs(int r, int c, int idx, int length) {
        if(idx == length - 1) {  // 마지막 idx까지 도착. 문자열 완성.
            dp[r][c][idx] = 1;
            return;
        }
        if(dp[r][c][idx] != -1) {  // 이미 방분한 곳이면 리턴하고 그 값 더함
            return;
        }
        dp[r][c][idx] = 0;  // 방문한 노드는 0으로 해줘야 함. -1값을 그대로 유지하면 나중에 값을 더할 때 문제가 생길 수 있다.

        for(int i = 1; i <= k; i++) {
            for(int j = 0; j < 4; j++) {  // k가 1씩 증가할 때마다 현재 위치에서 상하좌우 1칸씩 더 검사
                int nr = r + rArr[j] * i;
                int nc = c + cArr[j] * i;
                if(-1 < nr && nr < n && -1 < nc && nc < m) {  // n행 m열 범위 체크
                    if(map[nr][nc] == target.charAt(idx + 1)) {  // 다음 문자 일치하면 재귀
                        dfs(nr, nc, idx + 1, length);
                        dp[r][c][idx] += dp[nr][nc][idx + 1];
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        st = new StringTokenizer(br.readLine());
        n = Integer.parseInt(st.nextToken());
        m = Integer.parseInt(st.nextToken());
        k = Integer.parseInt(st.nextToken());

        map = new char[n][m];
        for(int i = 0; i < n; i++) {
            String input = br.readLine();
            for(int j = 0; j < m; j++) {
                map[i][j] = input.charAt(j);
            }
        }
        target = br.readLine();

        dp = new int[n][m][target.length()];  // 방문하지 않은 상를 -1로 초기화해야함
        for(int i = 0; i < n; i++) {  // 0으로 초기화하면
            for(int j = 0; j < m; j++) {
                Arrays.fill(dp[i][j], -1);
            }
        }

        for(int i = 0; i < n; i++) {
            for(int j = 0; j < m; j++) {
                if(map[i][j] == target.charAt(0)) {
                    dfs(i, j, 0, target.length());
                    ans += dp[i][j][0];
                }
            }
        }
        System.out.println(ans);
    }
}

 

너무너무너무너무너무 어려웠고 시간도 오래 걸렸다. 결국 구글링과 오픈카톡의 도움을 받았다.

 

일단 처음엔 문제를 잘못 이해했다. 같은 칸을 여러 번 방문할 수 "없다"인 줄 알았는데, "있다" 였다.

 

이는 무작정 중복조회가 가능하다는 의미가 아니다.

예를들어, ABABA 라는 문자열을 찾는다고 할 때, 하나의 좌표 (r, c)에 있는 A를 [0], [2], [4]에 모두 사용할 수 있다는 뜻이다.

즉, 다른 인덱스의 문자를 찾는 데에 한해서 중복조회를 허용한다는 것이다.

-> (근데 결국 그게 무작정 중복조회를 허용한다는 얘긴가..? 음..?)

-> (생각해보니 같은 인덱스에 대한 중복 조회를 허용하면 경우의 수가 무한하게 나오게 될 테니 이건 애초에 말이 안 되는 소리였다.)

 

 

비록 다른 인덱스에 한해서이긴 하지만 중복조회를 허용한다는 대목에서 브루트포스로는 절대 해결할 수 없다는 것을 눈치채야한다.

map의 크기는 최대 100*100으로 10,000이고 타겟 문자열의 최대 길이는 80. 대충 생각해봐도 브루트포스는 불가능하다.

 

다음 인덱스의 문자와 일치하는 좌표에 대해서만 재귀를 수행한다면 그래도 시간 안에 들어올 수 있지 않을까 하는 헛된 희망을 가지고 코드를 짰으나 어림도 없었다.

 

 

시간초과에서 빠져나오기 위해 DFS에 DP를 함께 사용해야한다.

DP문제에선 항상 그렇듯이 dp 배열에 무엇을 담을 것인지가 중요하다.

-> dp[r][c][idx] == (r, c)를 idx번째 인덱스로 사용할 수 있는 정답 경우의 수

 

이렇게하면 문제의 요구사항대로 같은 인덱스에 대해서는 중복조회를 막으면서 다른 인덱스에 대해서만 중복조회를 허용할 수 있게 된다.

 

DP의 초기값을 0으로 했다가 또 문제가 발생했다.

처음에는, 어차피 if(map[nr][nc] == target.charAt(idx + 1)) 을 사용해서 다음 인덱스의 문자가 일치하는 것에 대해서만 재귀호출을 실행하니까 초기화를 0으로 하더라도 별 문제가 없을 것이라고 생각했다.

 

그런데 별 문제가 있다.

 

예를 들어, 길이가 5인 문자열을 찾아야 할 때 길이 3까지만 일치하고 그 이후로는 일치하는 문자를 찾을 수 없다면? 

 

결국 dp[] = 0이 의미하는 바가 아래의 두 가지 경우를 모두 포함하게 되어 제대로 된 구분을 할 수 없게 된다.

1. 아직 방문하지 않음(= 초기값으로서의 의미)
2. 해당 경우에서 정답을 완성시킬 수 있는 경우의 수가 0개.

 

방문하지 않았으면 탐색을 진행하도록 하고, 해당 경우에서 정답을 완성시킬 수 없으면 더이상 탐색을 진행하지 않아야하는데, 초기값이 이렇게 중복적인 의미를 띄게 되면 메모이제이션이 제대로 작동하지 않게 되고.. 결과적으로 안 해도 될 탐색을 더 수행하게 돼서 시간초과가 나는 것이다.

 

모호성을 피하기 위해서 DP 초기값은 반드시 0이 아닌, "방문하지 않음" 만을 의미하도록 잡아야한다.

즉, 탐색을 진행하며 "절대로 발생할 수 없는 경우"로 초기값을 잡아줘야한다.

 

 

 

 

 

 

아, 어렵다...

 

 

 

'알고리즘 문제 > 백준 온라인 저지' 카테고리의 다른 글

[BOJ] 5014 - 스타트링크 JAVA  (0) 2021.02.14
[BOJ] 3108 - 로고 JAVA  (0) 2021.02.14
[BOJ] 1525 - 퍼즐 JAVA  (0) 2021.02.12
[BOJ] 2251 - 물통 JAVA  (0) 2021.02.12
[BOJ] 9019 - DSLR JAVA  (0) 2021.02.11

www.acmicpc.net/problem/1525

 

1525번: 퍼즐

세 줄에 걸쳐서 표에 채워져 있는 아홉 개의 수가 주어진다. 한 줄에 세 개의 수가 주어지며, 빈 칸은 0으로 나타낸다.

www.acmicpc.net

문제

3×3 표에 다음과 같이 수가 채워져 있다. 오른쪽 아래 가장 끝 칸은 비어 있는 칸이다.

1 2 3
4 5 6
7 8  

어떤 수와 인접해 있는 네 개의 칸 중에 하나가 비어 있으면, 수를 그 칸으로 이동시킬 수가 있다. 물론 표 바깥으로 나가는 경우는 불가능하다. 우리의 목표는 초기 상태가 주어졌을 때, 최소의 이동으로 위와 같은 정리된 상태를 만드는 것이다. 다음의 예를 보자.

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

가장 윗 상태에서 세 번의 이동을 통해 정리된 상태를 만들 수 있다. 이와 같이 최소 이동 횟수를 구하는 프로그램을 작성하시오.

입력

세 줄에 걸쳐서 표에 채워져 있는 아홉 개의 수가 주어진다. 한 줄에 세 개의 수가 주어지며, 빈 칸은 0으로 나타낸다.

출력

첫째 줄에 최소의 이동 횟수를 출력한다. 이동이 불가능한 경우 -1을 출력한다.

예제 입력 1

1 0 3

4 2 5

7 8 6

예제 출력 1

3

예제 입력 2

3 6 0

8 1 2

7 4 5

예제 출력 2

-1

 

 

 

 

 

 

 

풀이 1. (틀린 코드)

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

class Pair {
    int[][] arr;
    int dept;
    int zeroRow;
    int zeroCol;
    Pair(int[][] arr, int dept, int zeroRow, int zeroCol) {
        this.arr = arr;
        this.dept = dept;
        this.zeroRow = zeroRow;
        this.zeroCol = zeroCol;
    }
}

public class Main {
    static BufferedReader br = null;
    static StringTokenizer st = null;
    static int[][] map = null;
    static Map<String, Integer> check = null;

    static int[] rArr = {-1, 1, 0, 0};
    static int[] cArr = {0, 0, -1, 1};

    static int ans = -1;

    public static void swap(int[][] arr, int row1, int col1, int row2, int col2) {
        int temp = arr[row1][col1];
        arr[row1][col1] = arr[row2][col2];
        arr[row2][col2] = temp;
    }

    public static String getStr(int[][] arr) {  // check를 위한 2차원 배열의 String 받기
        String str = "";
        for(int i = 0; i < arr.length; i++) {
            for(int j = 0; j < arr[i].length; j++) {
                str += String.valueOf(arr[i][j]);
            }
        }
        return str;
    }

    public static void putToCheck(int[][] arr) {  // 2차원 배열 형태에 대한 check를 String으로 기억
        String input = "";
        for(int i = 0; i < arr.length; i++) {
            for(int j = 0; j < arr[i].length; j++) {
                input += String.valueOf(arr[i][j]);
            }
        }
        check.put(input, 1);
    }

    public static void bfs(int zeroRow, int zeroCol) {
        Queue<Pair> que = new ArrayDeque<>();
        que.add(new Pair(map.clone(), 0, zeroRow, zeroCol));
        putToCheck(map);

        while(!que.isEmpty()) {
            Pair p = que.poll();
            int[][] temp = null;
            
            for(int i = 0; i < 4; i++) {
                temp = p.arr.clone();
                int nr = p.zeroRow + rArr[i];
                int nc = p.zeroCol + cArr[i];

                if(nr == 2 && nc == 2) {  // 다음 0의 위치가 [2][2]이면 완료
                    ans = p.dept + 1;
                    return;
                }

                if(-1 < nr && nr < 3 && -1 < nc && nc < 3) {  // 범위 체크
                    swap(temp, p.zeroRow, p.zeroCol, nr, nc);  // 0 위치 스왑
                    String str = getStr(temp);  // 바뀐 배열의 String
                    if(check.get(str) == null) {  // if(!check[temp])
                        que.add(new Pair(temp, p.dept + 1, nr, nc));
                        putToCheck(temp);  // check[temp] = true
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        map = new int[3][3];
        check = new HashMap<>();

        int zeroRow = 0, zeroCol = 0;
        for(int i = 0; i < 3; i++) {
            st = new StringTokenizer(br.readLine());
            for(int j = 0; j < 3; j++) {
                map[i][j] = Integer.parseInt(st.nextToken());
                if(map[i][j] == 0) {
                    zeroRow = i;
                    zeroCol = j;
                }
            }
        }

        if(map[2][2] == 0) {
            System.out.println(0);
            return;
        }
        bfs(zeroRow, zeroCol);
        System.out.println(ans);
    }
}

 

삽질을 꽤나 오랫동안 했다.

 

[2][2] == 0 으로 만드는 게 목적인 줄 알았는데 그게 아니었다. (심지어 이걸 구현하는 데 엄청 오래걸렸다)

퍼즐의 순서가 1, 2, 3, 4, 5, 6, 7, 8, 0 이 되도록 만들어야 한다.

 

하...

 

 

 

 

 

 

풀이 2. (정답 코드)

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

public class Main {
    static BufferedReader br = null;
    static StringTokenizer st = null;
    static Map<Integer, Integer> check = null;  // <검사 대상, dept> 방문여부와 dept를 한 번에 검사할 수 있다.

    static int[] rArr = {-1, 1, 0, 0};
    static int[] cArr = {0, 0, -1, 1};

    public static void bfs(int init) {
        Queue<Integer> que = new ArrayDeque<>();
        que.add(init);
        check.put(init, 0);

        while (!que.isEmpty()) {
            int now = que.poll();
            String nowStr = String.valueOf(now);

            int indexOfNine = nowStr.indexOf("9");  // 일차원 배열 상에서 9의 인덱스
            int r = indexOfNine / 3;  // (핵심)
            int c = indexOfNine % 3;  // (핵심)

            for(int i = 0; i < 4; i++) {
                int nr = r + rArr[i];
                int nc = c + cArr[i];
                int move = nr * 3 + nc;  // (핵심) 일차원 배열 상에서 9가 이동할 인덱스

                if(-1 < nr && nr < 3 && -1 < nc && nc < 3) {
                    StringBuilder sb = new StringBuilder(nowStr);
                    char temp = sb.charAt(move);
                    sb.setCharAt(move, '9');
                    sb.setCharAt(indexOfNine, temp);

                    int next = Integer.parseInt(sb.toString());
                    if(check.get(next) == null) {
                        check.put(next, check.get(now) + 1);
                        que.add(next);
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        check = new HashMap<>();

        int init = 0;
        for(int i = 0; i < 3; i++) {
            st = new StringTokenizer(br.readLine());
            for(int j = 0; j < 3; j++) {
                int input = Integer.parseInt(st.nextToken());
                if(input == 0) input = 9;  // 이차원 배열을 하나의 숫자로 표현하기 위해 0을 9로 바꾼다.
                init = (init * 10) + input;  // 0을 그대로 사용해버리면 나중에 인덱스가 꼬일 수 있음.
            }
        }

        bfs(init);
        if(check.get(123456789) == null) {
            System.out.println(-1);
        }else {
            int ans = check.get(123456789);
            System.out.println(ans);
        }
    }
}

 

삽질 1.

나도 처음에는 배열을 int로 표현하려고 했었지만 0이 들어가면 제대로 된 값을 구할 수가 없어져서 시간이 좀 느려지더라도 String으로 처리한 거였는데.. 그냥 0을 9로 바꿔버리면 아주 간단히 해결되는 문제였다.

(그리고 아마 String 그대로 사용했으면 그건 시간초과로 틀렸을 듯?)

 

 

삽질 2.

swap을 할 때 원본 배열값을 유지하기 위해 나름대로 Deep Copy를 하겠다고 clone()을 사용했다.

그런데, 분명 clone()은 딥카피로 배열을 복사해주는 함수인데.. 자꾸 원본 배열까지 수정이 되는 문제가 발생했다.

 

알고보니 2차원 배열을 온전히 딥카피 해주려면 2차원 배열의 모든 행을 하나하나 전부 clone() 해줘야 하는 것이었다.

2차원 배열의 원소인 1차원 배열은 Object이기 때문에 원래 값이 유지되었던 것이다. 그러니 당연히 원본 값도 같이 수정이 되지..

 

 

삽질 3.

딥카피도 문제였지만 사실 2차원 배열 자체를 쓸 필요가 없었다.

 

2차원 배열을 1차원 배열 형태로 표현했을 때, 2차원 배열의 인덱스에 대응되는 1차원 배열이 인덱스를 뽑아낼 수 있는 방법이 있었다.

간단한 수식으로 해결할 수 있었는데 역시 수학멍청이답게 전혀 생각하지 못하고 있다가 구글링을 통해 알게 됐다.

 

 

삽질 4.

그리고 굳이 Pair를 사용할 필요도 없었다. 

(배열, dept, zeroRow, zeroCol 다 때려박은 순간부터 Pair라는 이름도 모순이 돼버렸지만)

 

Map을 사용해서 checking과 dept를 한 번에 다룰 수 있었다.

(해당 key의 value가 없으면 방문하지 않은 것. 있다면 그 value가 해당 key의 dept())

 

 

 

 

 

 

아.. 너무 힘들다...

 

 

 

 

'알고리즘 문제 > 백준 온라인 저지' 카테고리의 다른 글

[BOJ] 3108 - 로고 JAVA  (0) 2021.02.14
[BOJ] 2186 - 문자판 JAVA  (0) 2021.02.13
[BOJ] 2251 - 물통 JAVA  (0) 2021.02.12
[BOJ] 9019 - DSLR JAVA  (0) 2021.02.11
[BOJ] 1963 - 소수 경로 JAVA  (0) 2021.02.11

www.acmicpc.net/problem/2251

 

2251번: 물통

각각 부피가 A, B, C(1≤A, B, C≤200) 리터인 세 개의 물통이 있다. 처음에는 앞의 두 물통은 비어 있고, 세 번째 물통은 가득(C 리터) 차 있다. 이제 어떤 물통에 들어있는 물을 다른 물통으로 쏟아 부

www.acmicpc.net

문제

각각 부피가 A, B, C(1≤A, B, C≤200) 리터인 세 개의 물통이 있다. 처음에는 앞의 두 물통은 비어 있고, 세 번째 물통은 가득(C 리터) 차 있다. 이제 어떤 물통에 들어있는 물을 다른 물통으로 쏟아 부을 수 있는데, 이때에는 한 물통이 비거나, 다른 한 물통이 가득 찰 때까지 물을 부을 수 있다. 이 과정에서 손실되는 물은 없다고 가정한다.

이와 같은 과정을 거치다보면 세 번째 물통(용량이 C인)에 담겨있는 물의 양이 변할 수도 있다. 첫 번째 물통(용량이 A인)이 비어 있을 때, 세 번째 물통(용량이 C인)에 담겨있을 수 있는 물의 양을 모두 구해내는 프로그램을 작성하시오.

입력

첫째 줄에 세 정수 A, B, C가 주어진다.

출력

첫째 줄에 공백으로 구분하여 답을 출력한다. 각 용량은 오름차순으로 정렬한다.

예제 입력 1

8 9 10

예제 출력 1

1 2 8 9 10

 

 

 

 

 

 

 

풀이 .

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

class Bottle {
    int a;
    int b;
    int c;
    Bottle(int a, int b, int c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }
}

public class Main {
    static BufferedReader br = null;
    static StringTokenizer st = null;

    static int[] mBottle;  // ([0] = A, [1] = B, [2] = C) 의 용량(= 크기)
    static boolean[][][] check = null;  // [A][B][C]
    static ArrayList<Integer> ans = null;
    static Queue<Bottle> que = null;

    public static void pourWater(int from, int to, int[] pBottle) {  // from은 반드시 비어있지 않아야한다.
        // from, to는 pBottle의 인덱스
        // pBottle = {a, b, c}
        int[] temp = {pBottle[0], pBottle[1], pBottle[2]};

        if(temp[from] + temp[to] <= mBottle[to]) {  // from의 물을 다 붓는 경우
            temp[to] += temp[from];
            temp[from] = 0;
        }else {  // to가 꽉 찰때까지만 붓는 경우
            temp[from] -= mBottle[to] - temp[to];  // to의 남은 공간만큼만 붓는다.
            temp[to] = mBottle[to];  // to는 꽉 차게 된다.
        }

        int nowA = temp[0], nowB = temp[1], nowC = temp[2];
        if(!check[nowA][nowB][nowC]) {
            que.add(new Bottle(nowA, nowB, nowC));
            check[nowA][nowB][nowC] = true;
            if(nowA == 0) ans.add(nowC);  // A == 0 일 때, C값 저장
        }
    }

    public static void bfs(int a, int b, int c) {
        que = new ArrayDeque<>();
        que.add(new Bottle(a, b, c));
        check[a][b][c] = true;
        ans.add(c);

        while(!que.isEmpty()) {
            Bottle bottle = que.poll();
            int[] pBottle = {bottle.a, bottle.b, bottle.c};

            if(pBottle[0] != 0) {  // from A
                if(pBottle[1] != mBottle[1]) pourWater(0, 1, pBottle);
                if(pBottle[2] != mBottle[2]) pourWater(0, 2, pBottle);
            }
            if(pBottle[1] != 0) {  // from B
                if(pBottle[0] != mBottle[0]) pourWater(1, 0, pBottle);
                if(pBottle[2] != mBottle[2]) pourWater(1, 2, pBottle);
            }
            if(pBottle[2] != 0) {  // from C
                if(pBottle[0] != mBottle[0]) pourWater(2, 0, pBottle);
                if(pBottle[1] != mBottle[1]) pourWater(2, 1, pBottle);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        st = new StringTokenizer(br.readLine());

        mBottle = new int[3];
        mBottle[0] = Integer.parseInt(st.nextToken());
        mBottle[1] = Integer.parseInt(st.nextToken());
        mBottle[2] = Integer.parseInt(st.nextToken());
        check = new boolean[mBottle[0] + 1][mBottle[1] + 1][mBottle[2] + 1];
        ans = new ArrayList<>();

        bfs(0, 0, mBottle[2]);
        Collections.sort(ans);
        for(int c : ans) {
            System.out.print(c + " ");
        }
    }
}

 

어려운 문제는 아닌데 구현으로 시간을 꽤 잡아먹었다.

 

DFS, BFS 뭘 사용하든 상관없어보인다.

 

노드의 개수 = 숫자 3개의 합으로 200을 만드는 모든 경우의 수

에지의 개수 = 6개 (한 물통에서 한 물통으로 물을 옮기는 경우의 수)

 

check[][][] 으로 8MB를 사용한다. 메모리 제한에 충분히 여유롭긴 하지만 뭔가 낭비되는 게 많은 느낌.

 

200*200*200으로 하지 않고 A+B+C<=200 으로 딱 가고 싶으면 

map<Bottle, boolean> 으로 처리할 수도 있을 듯.

 

 

 

'알고리즘 문제 > 백준 온라인 저지' 카테고리의 다른 글

[BOJ] 2186 - 문자판 JAVA  (0) 2021.02.13
[BOJ] 1525 - 퍼즐 JAVA  (0) 2021.02.12
[BOJ] 9019 - DSLR JAVA  (0) 2021.02.11
[BOJ] 1963 - 소수 경로 JAVA  (0) 2021.02.11
[BOJ] 1697 - 숨바꼭질 JAVA  (0) 2021.02.11

www.acmicpc.net/problem/9019

 

9019번: DSLR

네 개의 명령어 D, S, L, R 을 이용하는 간단한 계산기가 있다. 이 계산기에는 레지스터가 하나 있는데, 이 레지스터에는 0 이상 10,000 미만의 십진수를 저장할 수 있다. 각 명령어는 이 레지스터에

www.acmicpc.net

문제

네 개의 명령어 D, S, L, R 을 이용하는 간단한 계산기가 있다. 이 계산기에는 레지스터가 하나 있는데, 이 레지스터에는 0 이상 10,000 미만의 십진수를 저장할 수 있다. 각 명령어는 이 레지스터에 저장된 n을 다음과 같이 변환한다. n의 네 자릿수를 d1, d2, d3, d4라고 하자(즉 n = ((d1 × 10 + d2) × 10 + d3) × 10 + d4라고 하자)

  1. D: D 는 n을 두 배로 바꾼다. 결과 값이 9999 보다 큰 경우에는 10000 으로 나눈 나머지를 취한다. 그 결과 값(2n mod 10000)을 레지스터에 저장한다.
  2. S: S 는 n에서 1 을 뺀 결과 n-1을 레지스터에 저장한다. n이 0 이라면 9999 가 대신 레지스터에 저장된다.
  3. L: L 은 n의 각 자릿수를 왼편으로 회전시켜 그 결과를 레지스터에 저장한다. 이 연산이 끝나면 레지스터에 저장된 네 자릿수는 왼편부터 d2, d3, d4, d1이 된다.
  4. R: R 은 n의 각 자릿수를 오른편으로 회전시켜 그 결과를 레지스터에 저장한다. 이 연산이 끝나면 레지스터에 저장된 네 자릿수는 왼편부터 d4, d1, d2, d3이 된다.

위에서 언급한 것처럼, L 과 R 명령어는 십진 자릿수를 가정하고 연산을 수행한다. 예를 들어서 n = 1234 라면 여기에 L 을 적용하면 2341 이 되고 R 을 적용하면 4123 이 된다.

여러분이 작성할 프로그램은 주어진 서로 다른 두 정수 A와 B(A ≠ B)에 대하여 A를 B로 바꾸는 최소한의 명령어를 생성하는 프로그램이다. 예를 들어서 A = 1234, B = 3412 라면 다음과 같이 두 개의 명령어를 적용하면 A를 B로 변환할 수 있다.

1234 →L 2341 →L 3412
1234 →R 4123 →R 3412

따라서 여러분의 프로그램은 이 경우에 LL 이나 RR 을 출력해야 한다.

n의 자릿수로 0 이 포함된 경우에 주의해야 한다. 예를 들어서 1000 에 L 을 적용하면 0001 이 되므로 결과는 1 이 된다. 그러나 R 을 적용하면 0100 이 되므로 결과는 100 이 된다.

입력

프로그램 입력은 T 개의 테스트 케이스로 구성된다. 테스트 케이스 개수 T 는 입력의 첫 줄에 주어진다. 각 테스트 케이스로는 두 개의 정수 A와 B(A ≠ B)가 공백으로 분리되어 차례로 주어지는데 A는 레지스터의 초기 값을 나타내고 B는 최종 값을 나타낸다. A 와 B는 모두 0 이상 10,000 미만이다.

출력

A에서 B로 변환하기 위해 필요한 최소한의 명령어 나열을 출력한다. 가능한 명령어 나열이 여러가지면, 아무거나 출력한다.

예제 입력 1

3

1234 3412

1000 1

1 16

예제 출력 1

LL

L

DDDD

 

 

 

 

 

 

 

풀이 1. (틀린코드 - 시간초과)

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

class Pair {
    String instruction;
    String number;
    Pair(String intstruction, String number) {
        this.instruction = intstruction;
        this.number = number;
    }
}

public class Main {
    static BufferedReader br = null;
    static StringTokenizer st = null;

    static boolean[] check = null;
    static String ans;

    public static String makeLengthFour(String str) {
        int len = str.length();
        String result = str;
        for(int i = 0; i < len; i++) {
            result = "0" + result;
        }
        return result;
    }

    public static String d(String strNum) {
        int num = Integer.parseInt(strNum);
        num = 2 * num % 10000;

        String result = makeLengthFour(String.valueOf(num));
        return result;
    }

    public static String s(String strNum) {
        int num = Integer.parseInt(strNum);
        if(num == 0) num = 9999;
        else num -= 1;

        String result = makeLengthFour(String.valueOf(num));
        return result;
    }

    public static String l(String strNum) {
        String temp = makeLengthFour(strNum);

        String result = String.valueOf(temp.charAt(1));
        result += String.valueOf(temp.charAt(2));
        result += String.valueOf(temp.charAt(3));
        result += String.valueOf(temp.charAt(0));

        return result;
    }

    public static String r(String strNum) {
        String temp = makeLengthFour(strNum);

        String result = String.valueOf(temp.charAt(3));
        result += String.valueOf(temp.charAt(0));
        result += String.valueOf(temp.charAt(1));
        result += String.valueOf(temp.charAt(2));

        return result;
    }

    public static void bfs(int start, int destination) {
        String strStart = String.valueOf(start);

        Queue<Pair> que = new ArrayDeque<>();
        que.add(new Pair("", makeLengthFour(strStart)));
        check[start] = true;

        while(!que.isEmpty()) {
            Pair p = que.poll();
            String inst = p.instruction;
            String now = p.number;

            String dStr = d(now), sStr = s(now), lStr = l(now), rStr = r(now);
            int dNum = Integer.parseInt(dStr), sNum = Integer.parseInt(sStr), lNum = Integer.parseInt(lStr), rNum = Integer.parseInt(rStr);

            if(dNum == destination) {ans = inst + "D"; return;}
            if(sNum == destination) {ans = inst + "S"; return;}
            if(lNum == destination) {ans = inst + "L"; return;}
            if(rNum == destination) {ans = inst + "R"; return;}

            if(!check[dNum]) {que.add(new Pair(inst + "D", dStr)); check[dNum] = true;}
            if(!check[sNum]) {que.add(new Pair(inst + "S", sStr)); check[sNum] = true;}
            if(!check[lNum]) {que.add(new Pair(inst + "L", lStr)); check[lNum] = true;}
            if(!check[rNum]) {que.add(new Pair(inst + "R", rStr)); check[rNum] = true;}
        }
    }

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        int T = Integer.parseInt(br.readLine());

        while(T-- > 0) {
            st = new StringTokenizer(br.readLine());
            int from = Integer.parseInt(st.nextToken());
            int to = Integer.parseInt(st.nextToken());

            check = new boolean[10000];
            bfs(from, to);
            System.out.println(ans);
        }
    }
}

 

String 연산을 쓸데없이 많이 사용해서 시간초과가 난 거 같다.

 

숫자의 길이에 대한 주의사항 때문에 일부러 makeLengthFour() 같은 것도 만들고 이것저것 신경을 썼는데.. 오히려 그게 시간초과로 발목을 잡았다.

 

 

 

 

 

 

풀이 2. (정답코드)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Queue;
import java.util.StringTokenizer;

public class Main {
    static BufferedReader br = null;
    static StringTokenizer st = null;

    static boolean[] check = null;
    static String[] instructions = null;
    static String ans;

    public static void bfs(int start, int destination) {
        Queue<Integer> que = new ArrayDeque<>();
        que.add(start);
        check[start] = true;

        while(!que.isEmpty()) {
            int now = que.poll();

            int D = 2 * now % 10000;
            int S = (now-1 == -1) ? 9999 : now-1;
            int L = (now % 1000) * 10 + (now / 1000);
            int R = (now / 10) + (now % 10) * 1000;

            if(D == destination) { ans = instructions[now] + "D"; return; }
            if(S == destination) { ans = instructions[now] + "S"; return; }
            if(L == destination) { ans = instructions[now] + "L"; return; }
            if(R == destination) { ans = instructions[now] + "R"; return; }

            if(!check[D]) { que.add(D); check[D] = true; instructions[D] = instructions[now] + "D"; }
            if(!check[S]) { que.add(S); check[S] = true; instructions[S] = instructions[now] + "S"; }
            if(!check[L]) { que.add(L); check[L] = true; instructions[L] = instructions[now] + "L"; }
            if(!check[R]) { que.add(R); check[R] = true; instructions[R] = instructions[now] + "R"; }
        }
    }

    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        int T = Integer.parseInt(br.readLine());

        while(T-- > 0) {
            st = new StringTokenizer(br.readLine());
            int from = Integer.parseInt(st.nextToken());
            int to = Integer.parseInt(st.nextToken());

            check = new boolean[10000];
            instructions = new String[10000];
            Arrays.fill(instructions, "");

            bfs(from, to);
            System.out.println(ans);
        }
    }
}

 

애초부터 String을 그렇게 남발할 필요도 없었고 D, S, L, R 각 명령을 함수로 만들 필요도 없었다.

'알고리즘 문제 > 백준 온라인 저지' 카테고리의 다른 글

[BOJ] 1525 - 퍼즐 JAVA  (0) 2021.02.12
[BOJ] 2251 - 물통 JAVA  (0) 2021.02.12
[BOJ] 1963 - 소수 경로 JAVA  (0) 2021.02.11
[BOJ] 1697 - 숨바꼭질 JAVA  (0) 2021.02.11
[BOJ] 10971 - 외판원 순회 2 JAVA  (0) 2021.02.11

+ Recent posts