https://www.acmicpc.net/problem/9252

소요 시간: 20분
메모리: 16048KB
시간: 96ms

🔥 문제

LCS(Longest Common Subsequence, 최장 공통 부분 수열)문제는 두 수열이 주어졌을 때, 모두의 부분 수열이 되는 수열 중 가장 긴 것을 찾는 문제이다. 예를 들어, ACAYKP와 CAPCAK의 LCS는 ACAK가 된다.

 

입력

첫째 줄과 둘째 줄에 두 문자열이 주어진다. 문자열은 알파벳 대문자로만 이루어져 있으며, 최대 1000글자로 이루어져 있다.

 

출력

첫째 줄에 입력으로 주어진 두 문자열의 LCS의 길이를, 둘째 줄에 LCS를 출력한다.

LCS가 여러 가지인 경우에는 아무거나 출력하고, LCS의 길이가 0인 경우에는 둘째 줄을 출력하지 않는다.

 

 


🍀 풀이

 

BOJ.9251 LCS 문제를 풀이하면서 LCS에 대해 알아본 적이 있다.

 

저번 문제는 LCS의 길이를 출력하는 문제였다면, 이번은 LCS의 길이와 함께 LCS를 출력해야하는 문제이다.

LCS의 길이는 저번에 풀이를 했으니 LCS를 출력하는 방법에 대해서 풀이를 하려고 한다.

 

LCS를 구하는 식을 보자면..

두 문자(A[a]와 B[b])가 같은 경우, lcs[a+1][b+1] = lcs[a][b]+1이고

두 문자가 다른 경우, lcs[a+1][b+1] = Math.max(lcs[a][b+1], lcs[a+1][b])였다.

 

두 문자가 같은 경우 lcs[a][b] 값에서 1을 더하기 때문에 lcs[a+1][b+1]값이 lcs[a][b+1]과 lcs[a+1][b] 값보다 크다는 것을 알 수 있다.

 

두 문자가 다른 경우는 lcs[a+1][b+1]값이 lcs[a][b+1]과 lcs[a+1][b] 중 큰 값이 들어 있을 것이다.

 

 

LCS를 구하기 위해서 거꾸로 올라가는 것을 선택했다.

 

lcs[a][b]가 lcs[a-1][b]과 lcs[a][b-1] 중 하나와 같은 경우는 A[a-1]와 B[b-1] 문자는 같지 않다는 의미이다. 그러므로 lcs[a-1][b]과 lcs[a][b-1] 중 같은 곳으로 이동한다.("LCS가 여러 가지인 경우에는 아무거나 출력"이라는 조건이 있으므로 어디를 가도 상관없다.)

 

lcs[a][b]가 lcs[a-1][b]과 lcs[a][b-1] 모두 다른 경우는 A[a-1]와 B[b-1] 문자가 같다는 의미이다. 그러므로 A[a-1]를 StringBuilder객체 sb에 저장하고 lcs[a-1][b-1]로 이동한다.

 

그리고 이동한 곳이 0인 경우 LCS를 모두 찾았으므로 찾기를 종료하고, sb를 반대로 뒤집어서 출력한다. 

 

1. 문자열 A와 B를 입력받고, 문자 배열로 저장한다.
2. int형 2차원 배열 lcs_arr를 정의한다.
3. 문자열 B와 문자열 A를 2중 for문을 사용하여 모든 문자끼리 비교할 수 있도록 한다.
        1. A[a-1]와 B[b-1]의 문자가 같은 경우, lcs_arr[a-1][b-1]에 1을 더한 값을 lcs_arr[a][b]에 저장한다.
        2. A[a-1]와 B[b-1]의 문자가 다른 경우, lcs_arr[a-1][b]과 lcs_arr[a][b-1] 중 더 큰 값을 lcs_arr[a][b]에 저장한다.
4. StringBuilder 객체 sb를 생성하고, a에 A.length, b에 B.length를 저장한다.
5. lcs_arr[a][b]가 0보다 크다면 반복한다.
        1. lcs_arr[a][b]와 lcs_arr[a][b-1]가 같은 경우, lcs_arr[a][b-1]로 이동하기 위해 b를 1 줄인다.
        2. lcs_arr[a][b]와 lcs_arr[a-1][b]가 같은 경우, lcs_arr[a-1][b]로 이동하기 위해 a를 1 줄인다.
        3. 위 두 경우가 아니라면, A[a-1]과 B[b-1]은 같은 문자라는 의미로 sb에 A[a-1]을 저장하고, a와 b를 1씩 줄인다.
6. sb를 뒤집어서 LCS를 만든다.
7. LCS 길이인 lcs_arr[A.length][B.length]와 LCS 중 하나인 sb를 출력한다.

💻 전체 코드

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class LCS2_9252 {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        char[] A = br.readLine().trim().toCharArray();
        char[] B = br.readLine().trim().toCharArray();
        int[][] lcs_arr = new int[A.length + 1][B.length + 1];

        for (int a = 1; a <= A.length; a++) {
            for (int b = 1; b <= B.length; b++) {
                if (A[a - 1] == B[b - 1]) {
                    lcs_arr[a][b] = lcs_arr[a - 1][b - 1] + 1;
                } else if (lcs_arr[a][b - 1] < lcs_arr[a - 1][b]) {
                    lcs_arr[a][b] = lcs_arr[a - 1][b];
                } else {
                    lcs_arr[a][b] = lcs_arr[a][b - 1];
                }
            }
        }

        StringBuilder sb = new StringBuilder();
        int a = A.length;
        int b = B.length;
        while (0 < lcs_arr[a][b]) {
            if (lcs_arr[a][b] == lcs_arr[a][b - 1]) {
                b--;
            } else if (lcs_arr[a][b] == lcs_arr[a - 1][b]) {
                a--;
            } else {
                sb.append(A[--a]);
                b--;
            }
        }
        System.out.println(lcs_arr[A.length][B.length]);
        System.out.print(sb.reverse());
    }
}

💫 리뷰

lcs 까먹지말자~~

 

https://www.acmicpc.net/problem/9251

소요 시간: 17분
메모리: 15748KB
시간: 104ms

🔥 문제

LCS(Longest Common Subsequence, 최장 공통 부분 수열)문제는 두 수열이 주어졌을 때, 모두의 부분 수열이 되는 수열 중 가장 긴 것을 찾는 문제이다. 예를 들어, ACAYKP와 CAPCAK의 LCS는 ACAK가 된다.

 

입력

첫째 줄과 둘째 줄에 두 문자열이 주어진다. 문자열은 알파벳 대문자로만 이루어져 있으며, 최대 1000글자로 이루어져 있다.

 

출력

첫째 줄에 입력으로 주어진 두 문자열의 LCS의 길이를 출력한다.

 

 


🍀 풀이

LCS는 최장 공통 부분 수열(Longest Common Subsequence)을 의미한다.

Longest Common Substring과 비슷해보이지만 둘은 다르다!

 

ACAYKP와 CAPCAK의 Longest Common Substring은 CA지만 (ACAYKP, CAPCAK)

Longest Common Subsequence는 ACAK이다. (ACAYKP, CAPCAK)

 

이번 문제는 LCS를 찾는 문제이다. LCS는 DP문제이며, 푸는 방식이 있다.

먼저 문자열 A(ACAYKP), B(CAPCAK)의 길이+1에 맞는 2차원 배열 lcs를 만들고, 0번째 값은 모두 0으로 채운다.

 

이제 문자열 A와 B를 하나씩 비교한다.

A[a]와 B[b]의 문자가 같은 경우, lcs[a][b]+1 한 값을 lcs[a+1][b+1]에 저장한다.

 

lcs[a][b]는 A의 이전까지의 lcs 길이가 저장되어있을 것이므로, lcs[a][b]에 1을 더해 lcs[a+1][b+1]에 저장하는 것은 현재 문자 A[a](B[b])를 포함한 lcs 길이를 의미한다.

 

 

A[a]와 B[b]의 문자가 다른 경우, lcs[a][b+1]과 lcs[b+1][a] 중 더 큰 값을 가져와 저장한다.

문자가 다르기 때문에 lcs에 포함될 수 없으므로, A[a]와 B[b-1]까지 비교했을때 lcs 길이와 A[a-1]와 B[b]까지 비교했을때 lcs 길이를 비교해서 저장한다.

 

모든 문자들을 비교하고 나면 배열의 끝에는 lcs의 길이 값이 저장되어 있을 것이다.

1. 문자열 A와 B를 입력받고, 문자 배열로 저장한다.
2. int형 2차원 배열 lcs_arr를 정의한다.
3. 문자열 B와 문자열 A를 2중 for문을 사용하여 모든 문자끼리 비교할 수 있도록 한다.
        1. A[a-1]와 B[b-1]의 문자가 같은 경우, lcs_arr[b-1][a-1]에 1을 더한 값을 lcs_arr[b][a]에 저장한다.
        2. A[a-1]와 B[b-1]의 문자가 다른 경우, lcs_arr[b-1][a]과 lcs_arr[b][a-1] 중 더 큰 값을 lcs_arr[b][a]에 저장한다.
4. 모든 문자를 비교하였다면 맨 끝값인 lcs_arr[B.length][A.length]를 출력한다.

💻 전체 코드

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Main {

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        char[] A = br.readLine().toCharArray();
        char[] B = br.readLine().toCharArray();
        int[][] lcs_arr = new int[B.length + 1][A.length + 1];

        for (int b = 1; b <= B.length; b++) {
            for (int a = 1; a <= A.length; a++) {
                if (A[a - 1] == B[b - 1]) {
                    lcs_arr[b][a] = lcs_arr[b - 1][a - 1] + 1;
                } else {
                    lcs_arr[b][a] = Math.max(lcs_arr[b - 1][a], lcs_arr[b][a - 1]);
                }
            }
        }
        System.out.println(lcs_arr[B.length][A.length]);
    }
}

💫 리뷰

lcs처럼 풀이법이 있는 알고리즘들을 찾아서 익혀야겠다!!🔥

https://www.acmicpc.net/problem/1261

소요 시간: 20분
메모리: 19544KB
시간: 256ms

🔥 문제

알고스팟 운영진이 미로에 갇혔다. 미로는 N*M 크기이며, 총 1*1크기의 방으로 이루어져 있다. 미로는 빈 방 또는 벽으로 이루어져 있고, 빈 방은 자유롭게 다닐 수 있지만, 벽은 부수지 않으면 이동할 수 없다. 어떤 방에서 이동할 수 있는 방은 상하좌우로 인접한 빈 방이다. 알고스팟의 무기 AOJ를 이용해 벽을 부수어 버릴 수 있다. 벽을 부수면, 빈 방과 동일한 방으로 변한다.

현재 (1, 1)에 있는 알고스팟 운영진이 (N, M)으로 이동하려면 벽을 최소 몇 개 부수어야 하는지 구하는 프로그램을 작성하시오.

 

입력

첫째 줄에 미로의 크기를 나타내는 가로 크기 M, 세로 크기 N (1 ≤ N, M ≤ 100)이 주어진다.

다음 N개의 줄에는 미로의 상태를 나타내는 숫자 0과 1이 주어진다. 0은 빈 방을 의미하고, 1은 벽을 의미한다.

(1, 1)과 (N, M)은 항상 뚫려있다.

 

출력

첫째 줄에 알고스팟 운영진이 (N, M)으로 이동하기 위해 벽을 최소 몇 개 부수어야 하는지 출력한다.

 


🍀 풀이

벽을 최소로 부수면서 탈출구에 도착하는 문제로, 제일 먼저 떠오른 알고리즘인 bfs(너비 우선 탐색)로 문제를 풀었다.

 

오래 이동을 하더라도 벽을 최소로 부수면서 탈출해야한다. 벽을 최소로 부순다는 조건을 만족시키기 위해 우선순위 큐를 이용하였다.

우선순위 큐에는 new int[] {행값, 열값, 벽을 부순 횟수}를 넣을 것이다.

그러므로 아래와 같이 벽을 부순 횟수를 오름차순으로 정렬하도록 설정하였다.

PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[2]));

 

그리고 bfs를 통해서 우선순위 큐의 원소를 빼서 `node`에 저장한다.

`node`를 상하좌우로 이동시켰을 때 보드판을 벗어나지 않았고, 한번도 방문한 적 없는 위치인 경우, 

  1. 이동한 위치가 탈출구라면, 벽을 부순 횟수 `node[2]`를 출력한다.
  2. 이동한 위치가 빈 공간이라면, 방문처리 후 이동한 위치와 벽을 부순 횟수`node[2]`를 우선순위 큐에 넣는다.
  3. 이동한 위치가 벽이라면, 방문처리 후 이동한 위치와 벽을 부순 횟수`node[2]+1`를 우선순위 큐에 넣는다.
1. 보드판을 입력받아, `map`에 저장한다.
2. `bfs()`함수를 호출한다. 이 함수는 운영진이 탈출했을 때 벽을 부순 최소 횟수를 반환한다.
3. 우선순위 큐 `pq`를 정의한다. `pq`는 벽을 부순 횟수를 오름차순으로 정렬한다. new int[] {행값, 열값, 벽을 부순 횟수}
4. 방문 처리를 위해서 boolean 2차원 배열 `visited`를 정의한다.
5. 운영진들의 첫 위치인 (0,0)와 벽을 부순 횟수(0)을 `pq`에 넣고, 방문처리한다.  
6. `pq`가 빌 때까지 반복한다.
        1. `pq`에서 원소를 빼 `node`에 저장한다.
        2. 상하좌우 사방탐색을 한다. 이동한 위치가 유효범위 내에 있고, 방문한 적이 없는 곳일 때,
                1. 이동한 위치가 탈출구라면, 벽을 부순 최소 횟수 `node[2]`를 반환하고 함수를 종료한다.
                2. 이동한 위치가 빈 공간이라면, 운영진들이 이동한 위치와 벽을 부순 횟수(`node[2]`)을 `pq`에 넣고, 방문처리한다.
                3. 이동한 위치가 벽이라면, 운영진들이 이동한 위치와 벽을 부순 횟수(`node[2]+1`)을 `pq`에 넣고, 방문처리한다.
7.`bfs()`함수가 종료되면, 반환된 값을 출력한다.

💻 전체 코드

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Comparator;
import java.util.PriorityQueue;

public class Main {
    static int[] di = new int[]{-1, 0, 1, 0};
    static int[] dj = new int[]{0, 1, 0, -1};
    static int N, M;
    static char[][] map;


    public static void main(String[] args) throws Exception {
        init();
        System.out.println(bfs());
    }

    static void init() throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String input = br.readLine();
        N = Integer.parseInt(input.substring(0, input.indexOf(' ')));
        M = Integer.parseInt(input.substring(input.indexOf(' ') + 1));
        map = new char[M][N];
        for (int m = 0; m < M; m++) {
            map[m] = br.readLine().toCharArray();
        }
    }

    static int bfs() {
        PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[2]));
        boolean[][] visited = new boolean[M][N];

        pq.offer(new int[]{0, 0, 0});
        visited[0][0] = true;

        while (!pq.isEmpty()) {
            int[] node = pq.poll();

            for (int z = 0; z < 4; z++) {
                int ni = node[0] + di[z];
                int nj = node[1] + dj[z];

                if (check(ni, nj) && !visited[ni][nj]) {
                    if (ni == M - 1 && nj == N - 1) return node[2];
                    pq.offer(new int[]{ni, nj, map[ni][nj] == '1' ? node[2] + 1 : node[2]});
                    visited[ni][nj] = true;
                }
            }
        }

        return 0;
    }

    static boolean check(int i, int j) {
        return 0 <= i && i < M && 0 <= j && j < N;
    }

}

💫 리뷰

N과 M이 반대로 되어 있다! 문제를 꼼꼼하게 읽자!!

'0-1 bfs'로 풀 수 있다고 하는데, 처음보는 개념이라 먼저 공부하고 포스팅해야겠다!

https://www.acmicpc.net/problem/3078

소요 시간: 16분
메모리: 39280KB
시간: 236ms

🔥 문제

상근이네 반의 N명 학생들의 이름이 성적순으로 주어졌을 때, 좋은 친구가 몇 쌍이나 있는지 구하는 프로그램을 작성하시오.

좋은 친구는 등수의 차이가 K보다 작거나 같으면서 이름의 길이가 같은 친구이다.

 

입력

첫째 줄에 N과 K가 주어진다. (3 ≤ N ≤ 300,000, 1 ≤ K ≤ N)

다음 N개 줄에는 상근이네 반 학생의 이름이 성적순으로 주어진다. 이름은 알파벳 대문자로 이루어져 있고, 2글자 ~ 20글자이다.

 

출력

첫째 줄에 좋은 친구가 몇 쌍이 있는지 출력한다.


🍀 풀이

등수 순서대로 K명의 학생들의 이름을 카운트해서 배열에 저장하는 방식으로, 슬라이딩 윈도우를 활용해서 문제를 풀었다.

 

6 3
CYNTHIA
LLOYD
STEVIE
KEVIN
MALCOLM
DABNEY

 

위와 같은 입력이 주어졌다면, 등수 순서대로 아래와 같은 학생 이름 길이의 배열을 만들 수 있다.

 

1등 학생과 등수 차이가 K이하인 학생들의 이름 길이를 `name_len_count`배열에 카운트한다.

그리고 `name_len_count[7]`이 1등 학생을 포함하여 1등 학생과 이름 길이가 같은 학생의 수가 된다.

1등 학생과 좋은 친구 관계인 학생 수는 `name_len_count[7]-1`인 0명이 된다.

 

2등 학생과 좋은 관계인 학생 수를 찾기 위해 1등 학생의 이름 길이를 `name_len_count`배열에서 1 뺀다.

2등 학생과 등수 차이가 K이하인, 2등 이후 등수인 학생들의 이름 길이를 `name_len_count`배열에 카운트해야 한다.

2등 이전 등수는 이미 해당 등수에서 2등과 관계를 체크했으므로 2등 이후 등수인 학생들만 확인을 하면 된다.

이미 2등부터 4등(2+K-1등)까지 학생 이름 길이는 카운트가 되어 있으니, 범위의 끝 등수 학생인 5등 학생의 이름 길이만 `name_len_count`배열에 카운트하면 된다.

2등 학생과 좋은 친구 관계인 학생 수는 `name_len_count[5]-1`인 1명이 된다.

3등 학생과 좋은 관계인 학생 수를 찾기 위해 2등 학생의 이름 길이를 `name_len_count`배열에서 1 빼고, 범위의 끝 등수 학생인 6등 학생의 이름 길이를 카운트한다.

3등 학생과 좋은 친구 관계인 학생 수는 `name_len_count[6]-1`인 1명이 된다.

4등 학생과 좋은 관계인 학생 수를 찾기 위해 3등 학생의 이름 길이를 `name_len_count`배열에서 1 뺀다.

전체 학생 수는 6명으로 범위의 끝 등수 학생인 7등 학생이 없으므로 카운트하지 않는다.

3등 학생과 좋은 친구 관계인 학생 수는 `name_len_count[5]-1`인 0명이 된다.

이러한 방식으로 6등 학생까지 좋은 친구 관계를 확인하면 좋은 친구는 총 2쌍이 나온다.

 

1. 등수 순으로 학생의 이름 길이를 입력받는다. 이때 1등부터 K+1등 학생의 이름 길이를 `name_len_count` 배열에 카운트한다.
2. 좋은 친구가 몇쌍인지 저장하는 `pair`에 1등 학생과 좋은 친구 관계인 학생 수를 저장한다.
3. 이후 2등 학생부터 순차적으로 접근하며 좋은 친구 관계인 학생 수를 저장한다.
        1. i + K등 학생이 있는 경우 해당 학생의 이름 길이를 `name_len_count` 배열에 카운트한다.
        2. `pair`에 i등 학생과 이후 등수 중 좋은 친구 관계인 학생 수를 저장한다.
4. `pair`를 출력한다.

💻 전체 코드

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Main {

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String input = br.readLine();
        int N = Integer.parseInt(input.substring(0, input.indexOf(' ')));
        int K = Integer.parseInt(input.substring(input.indexOf(' ') + 1));

        int[] name_len = new int[N];
        int[] name_len_count = new int[21];

        for (int i = 0; i < N; i++) {
            name_len[i] = br.readLine().length();
            if (i <= K) {
                name_len_count[name_len[i]]++;
            }
        }
        long pair = --name_len_count[name_len[0]];

        for (int i = 1; i < N; i++) {
            if (i + K < N) name_len_count[name_len[i + K]]++;
            pair += --name_len_count[name_len[i]];
        }

        System.out.print(pair);
    }
}

💫 리뷰

풀이가 바로 생각나서 빠르게 풀 수 있었다.😀

좋은 학생 관계 수는 int형 범위를 벗어날 수 있어서 long형으로 선언해야 한다.

코테에서도 이런거 잘 계산해서 풀자!

https://www.acmicpc.net/problem/1655

소요 시간: 10분
메모리: 36252KB
시간: 380ms

🔥 문제

백준이가 정수를 하나씩 외칠때마다 동생은 지금까지 백준이가 말한 수 중에서 중간값을 말해야 한다. 만약, 그동안 백준이가 외친 수의 개수가 짝수개라면 중간에 있는 두 수 중에서 작은 수를 말해야 한다. 백준이가 외치는 수가 주어졌을 때, 동생이 말해야 하는 수를 구하는 프로그램을 작성하시오.

 

입력

첫째 줄에는 백준이가 외치는 정수의 개수 N이 주어진다. N은 1보다 크거나 같고, 100,000보다 작거나 같은 자연수이다. 그 다음 N줄에 걸쳐서 백준이가 외치는 정수가 차례대로 주어진다. 정수는 -10,000보다 크거나 같고, 10,000보다 작거나 같다.

 

출력

한 줄에 하나씩 N줄에 걸쳐 백준이의 동생이 말해야 하는 수를 순서대로 출력한다.


🍀 풀이

두 개의 우선순위 큐를 활용하여 문제를 풀었다.

 

하나는 가장 큰 값을 가져오는 우선순위 큐로, 여기에는 백준이가 지금까지 부른 숫자 중 절반에 해당하는 작은 값들을 저장한다. (`small_pq`)

지금까지 부른 숫자가 홀수개인 경우 이곳에 숫자 하나를 더 넣는다.

 

다른 하나는 가장 작은 값을 가져오는 우선순위 큐로, 여기에는 백준이가 지금까지 부른 숫자 중 절반에 해당하는 큰 값들을 저장한다. (`big_pq`)

 

항상 `small_pq`의 사이즈와 `big_pq`사이즈가 같거나, `small_pq`의 사이즈가 `big_pq`사이즈보다 1 커야한다.

입력을 받은 후 `small_pq`의 가장 큰 값이 중간값이 되도록 만들 것이다.

 

7
1
5
2
10
-99
1

 

위와 같은 예시가 주어졌다고 했을 때, 먼저 첫번째 입력은 `small_pq`에 넣고 이를 출력한다.

 

그 다음 입력은 `small_pq`의 가장 큰값과 비교한다. 예시에서는 1보다 큰 5가 입력으로 주어졌다.

`small_pq`보다 입력값이 큰 경우는 `big_pq`에 저장한다.

`small_pq`와 `big_pq`의 사이즈가 같으므로 중간에 있는 수 중 더 작은 `small_pq`의 가장 큰 값을 출력한다.

 

그 다음 입력인 2는 1보다 크므로 `big_pq`에 저장한다.

`small_pq`의 사이즈보다 `big_pq`의 사이즈가 크다. 중간값을 항상 `small_pq`의 제일 큰 값으로 가져오기 위해서 `small_pq`보다 `big_pq`의 사이즈가 큰 경우, `big_pq`의 제일 작은 값을 빼서 `small_pq`에 넣는다.

 

`small_pq`의 사이즈가 `big_pq`의 사이즈보다 1 크므로 중간값인 `small_pq`의 가장 큰 값을 출력한다.

 

그 다음 입력인 10은 2보다 크므로 `big_pq`에 저장한다.

`small_pq`와 `big_pq`의 사이즈가 같으므로 중간에 있는 수 중 더 작은 `small_pq`의 가장 큰 값을 출력한다.

 

그 다음 입력인 -99은 2보다 작으므로 `small_pq`에 저장한다.

`small_pq`의 사이즈가 `big_pq`의 사이즈보다 1 크므로 중간값인 `small_pq`의 가장 큰 값을 출력한다.

 

 

 

그 다음 입력인 1은 2보다 작으므로 `small_pq`에 저장한다.

`big_pq`의 사이즈를 +1한 것보다 `small_pq`의 사이즈가 크다. 중간값을 항상 `small_pq`의 제일 큰 값으로 가져오기 위해서 `big_pq`의 사이즈+1 보다 `small_pq`의 사이즈가 큰 경우, `small_pq`의 제일 큰 값을 빼서 `big_pq`에 넣는다.

 

`small_pq`의 사이즈가 `big_pq`의 사이즈보다 1 크므로 중간값인 `small_pq`의 가장 큰 값을 출력한다.

 

1. 우선순위 큐를 정의한다.
        1. `small_pq`: 가장 큰 값을 가져오는 우선순위 큐
        2. `big_pq`: 가장 작은 값을 가져오는 우선순위 큐
2. 제일 첫 입력을 `small_pq`에 넣고, 이를 출력한다.
3. `N-1`만큼 반복하며 입력을 받는다.
        1. 입력을 받아 `target`에 저장한다.
        2. `small_pq`의 제일 큰 값보다 `target`이 크다면, `big_pq`에 `target`을 넣는다.
                1. `target`을 넣은 후 `big_pq`의 사이즈가 `small_pq`사이즈 보다 크다면, `big_pq`의 제일 작은 원소를 빼서 `small_pq`에 넣는다.
        3. `small_pq`의 제일 큰 값보다 `target`이 작거나 같다면, `small_pq`에 `target`을 넣는다.
                1. `target`을 넣은 후 `small_pq`의 사이즈가 `big_pq`사이즈+1한 값보다 크다면, `small_pq`의 제일 큰 원소를 빼서 `big_pq`에 넣는다.
        4. `target`을 우선순위 큐에 넣었다면, `small_pq`의 제일 큰 원소를 출력한다.

💻 전체 코드

 

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Comparator;
import java.util.PriorityQueue;

public class Main {

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        int N = Integer.parseInt(br.readLine()) - 1;

        PriorityQueue<Integer> small_pq = new PriorityQueue<>(Comparator.reverseOrder());
        PriorityQueue<Integer> big_pq = new PriorityQueue<>();

        small_pq.offer(Integer.parseInt(br.readLine()));
        sb.append(small_pq.peek()).append("\n");

        while (0 < N--) {
            int target = Integer.parseInt(br.readLine());
            if (small_pq.peek() < target) {
                big_pq.offer(target);
                if (small_pq.size() < big_pq.size()) {
                    small_pq.offer(big_pq.poll());
                }
            } else {
                small_pq.offer(target);
                if (big_pq.size() + 1 < small_pq.size()) {
                    big_pq.offer(small_pq.poll());
                }
            }
            sb.append(small_pq.peek()).append("\n");
        }
        System.out.print(sb);
    }
}

💫 리뷰

우선순위 큐를 잘 활용하면 됐던 문제! 옛날에 이런 비슷한 문제를 풀었던 것 같아서 풀이법이 바로 생각났다.

이래서 꾸준히 여러 문제를 풀어봐야한다..!!! 📚🤓

https://www.acmicpc.net/problem/17244

소요 시간: 47분
메모리: 16380KB
시간: 112ms

🔥 문제

경재 씨는 한 걸음에 상하좌우에 인접한 칸으로만 움직일 수 있다.

경재 씨를 위해 집을 위에서 바라본 모습과 챙겨야 할 물건들의 위치들을 알고 있을 때, 물건을 모두 챙겨서 외출하기까지 최소 몇 걸음이 필요한지 구하는 프로그램을 작성하자.

 

입력

첫 번째 줄에는 집의 가로 길이 N과 세로 길이 M이 입력된다. (3 ≤ N, M ≤ 50)

두 번째 줄부터는 집의 구조가 예제 입력과 같이 주어진다.

비어있는 곳은 '.', 벽은 '#', 경재 씨의 현재 위치는 S, 나가는 문의 위치는 E, 챙겨야 하는 물건은 종류에 상관없이 X로 입력된다.

챙겨야 하는 물건은 최대 5개까지 있을 수 있다. 집은 언제나 벽으로 둘러싸여 있고, 나가는 문은 언제나 하나이다.

 

출력

S에서 출발하여 모든 물건을 챙겨서 E까지 도착할 수 있는 최소 시간을 출력한다. 모든 물건을 챙길 수 없는 경우는 주어지지 않는다.


🍀 풀이

아이템이 최대 5개까지 있을 수 있다. 2^5는 int형 범위 안에 있으므로 '현재 경재가 찾은 아이템'을 비트화하였다.


아이템을 `X`대신 숫자를 부여했다.

7 6
#######
#SX..X#
#..####
#..X..#
#...X.#
#####E#

 

위와 같은 입력이 들어왔다고 한다면, 아이템에게 번호를 부여하여 아래 그림과 같이 변경하였다.

아이템은 최대 5개이므로 char형 배열에 저장할 수 있다!

각 아이템 번호를 자릿수처럼 생각하며 left shift 연산(`1<<n`)을 활용했다.

e.g. 아이템 2번 
1을 왼쪽으로 2만큼 비트 이동한다.
100(bit) = 4(십진수)

 

경재가 여러 아이템을 발견한 경우 OR 비트연산(`|`)을 활용했다.

e.g. 경재가 0번 아이템만 가지고 있다가, 2번 아이템을 발견했을 때

경재가 최소 시간으로 물건을 찾고 외출해야하므로 너비 우선 탐색 BFS을 이용했다.

이때 방문처리는 현재 경재가 찾은 물건의 종류에 따라서 다르게 처리해야 한다고 생각했다.

 

아래 그림처럼 1번 아이템을 얻은 후 경재가 다시 왔던 길을 돌아가야하는 데, 방문처리를 2차원 배열로 한다면 돌아가지 못한다.

녹색이 경재의 현위치, 빨강색은 방문했던 곳

 

3차원 배열을 만들어 OR 비트연산한 결과에 따라서 다르게 방문처리할 수 있도록 구현했다.

OR 비트연산(`|`)을 통해 경재는 아이템이 하나도 없는 '0'부터 모든 아이템을 다 가진 '(1<<아이템 수) -1' 까지의 비트를 활용했다.


마지막으로 'E'에 도착했을 때, 경재가 모든 아이템을 다 찾았는지 체크하는 것은 비트 수를 확인했다.

 

1. 집의 구조를 입력받는다.
        1. 해당 칸에 아이템이 있는 경우 'X', X대신 아이템 번호(`itemCount`)로 저장하고 `itemCount`를 증가시킨다.
        2. 해당 칸이 출발 점인 경우, 변수로 위치를 저장하고 해당칸은 비어있는 곳 '.'으로 변경한다.
2. `findItem()`함수를 호출하여 외출하기까지 최소 시간을 구한다.
3. 경재의 움직임을 저장할 큐 `queue`를 선언한다. queue의 원소에는 {현재 위치의 행, 현재 위치의 열, 소요시간, 비트화한 획득한아이템}을 저장한다.
4. 방문처리할 3차원 boolean 배열 `visited`를 선언한다. [행][열][비트화한 획득한아이템]
5. 경재의 시작 위치에 맞게 `queue`에 원소를 넣고, `visited`방문처리를 한다. 경재는 아직 가진 아이템이 없으므로 비트화한 획득한아이템은 0이다.
6. `queue`가 빌 때까지 반복한다.
        1. `queue`에서 원소를 빼 `node`에 저장한다.
        2. 상하좌우 사방탐색을 하며 경재를 이동시킨다.
                1. 이동한 위치가 벽'#'이거나, 방문한적이 있는 경우 다음 반복문으로 넘어간다.
                2. 이동한 위치가 비어있는 곳'.'이라면, 이동한 위치에 방문 처리를 하고 `queue`에 원소를 넣는다.
                3. 이동한 위치가 나가는 문'E'이고 경재가 모든 아이템을 다 챙겼다면 소요시간+1을 반환하며 함수를 종료한다.
                4. 이동한 위치가 아이템이 있는 경우('0'~'4' 중 하나), OR 비트연산을 통해 아이템을 획득하고 이동한 위치에 방문 처리를 하고 `queue`에 원소를 넣는다.
7. `findItem()`함수가 종료됐다면, 반환값을 출력한다.

💻 전체 코드

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;

public class Main {
    static int N, M;
    static int itemCount, s_i, s_j;
    static char[][] map;

    public static void main(String[] args) throws Exception {
        init();
        System.out.println(findItem());
    }

    static void init() throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String input = br.readLine();
        N = Integer.parseInt(input.substring(0, input.indexOf(' ')));
        M = Integer.parseInt(input.substring(input.indexOf(' ') + 1));

        itemCount = 0;
        map = new char[M][];
        for (int m = 0; m < M; m++) {
            map[m] = br.readLine().toCharArray();
            for (int n = 0; n < N; n++) {
                if (map[m][n] == 'X') {
                    map[m][n] = (char) (itemCount++ + '0');
                } else if (map[m][n] == 'S') {
                    s_i = m;
                    s_j = n;
                    map[m][n] = '.';
                }
            }
        }
    }

    static int findItem() {
        final int[] di = new int[]{-1, 0, 1, 0};
        final int[] dj = new int[]{0, 1, 0, -1};

        Queue<int[]> queue = new LinkedList<>();
        boolean[][][] visited = new boolean[M][N][1 << itemCount];
        queue.offer(new int[]{s_i, s_j, 0, 0});
        visited[s_i][s_j][0] = true;

        while (!queue.isEmpty()) {
            int[] node = queue.poll();
            for (int z = 0; z < 4; z++) {
                int ni = node[0] + di[z];
                int nj = node[1] + dj[z];

                if (map[ni][nj] == '#' || visited[ni][nj][node[3]]) continue;

                if (map[ni][nj] == '.') {
                    visited[ni][nj][node[3]] = true;
                    queue.offer(new int[]{ni, nj, node[2] + 1, node[3]});
                } else if (map[ni][nj] == 'E') {
                    if (itemCount == Integer.bitCount(node[3])) return node[2] + 1;
                } else {
                    visited[ni][nj][node[3] | 1 << (map[ni][nj] - '0')] = true;
                    queue.offer(new int[]{ni, nj, node[2] + 1, node[3] | 1 << (map[ni][nj] - '0')});
                }
            }
        }
        return 0;
    }
}

💫 리뷰

다른 방식으로 접근하고 있다가 비슷한 문제를 풀었던 기억이 나면서 해결할 수 있었던 문제!

https://www.acmicpc.net/problem/1062

소요 시간: 46분
메모리: 11956KB
시간: 116ms

🔥 문제

김지민은 학생들에게 K개의 글자를 가르칠 수 있고, 가르치고 난 후 학생들은 그 K개의 글자로만 이루어진 단어만을 읽을 수 있다.

남극언어의 모든 단어는 "anta"로 시작되고, "tica"로 끝난다. 남극언어에 단어는 N개 밖에 없다고 가정한다. 학생들이 읽을 수 있는 단어의 최댓값을 구하는 프로그램을 작성하시오.

 

첫째 줄에 단어의 개수 N과 K가 주어진다. N은 50보다 작거나 같은 자연수이고, K는 26보다 작거나 같은 자연수 또는 0이다. 둘째 줄부터 N개의 줄에 남극 언어의 단어가 주어진다. 단어는 영어 소문자로만 이루어져 있고, 길이가 8보다 크거나 같고, 15보다 작거나 같다. 모든 단어는 중복되지 않는다.

 

출력

첫째 줄에 김지민이 K개의 글자를 가르칠 때, 학생들이 읽을 수 있는 단어 개수의 최댓값을 출력한다.


🍀 풀이

2^26은 int형 범위 안에 있기 때문에 '학생들이 배운 알파벳' 비트화하였다.


알파벳의 순서를 자릿수처럼 생각하며 left shift 연산(`1<<n`)을 활용했다.

e.g. c를 비트화
1을 왼쪽으로 2('c' - 'a')만큼 비트 이동한다.
100(bit) = 4(십진수)

 


학생들이 배우는 알파벳이 여러개인 경우 OR 비트연산(`|`)을 활용했다.

e.g. 학생들이 a와 c를 배운 경우
1(bit) | 100(bit) = 101(bit) = 5(십진수)

같은 방법으로 남극 언어의 단어마다 알파벳들을 비트화해서 저장했다.


비트 수를 체크하여 `K`개의 알파벳을 배웠는 지 확인했다.

`K`개의 알파벳을 배웠다면, 남극 언어의 단어들을 순차적으로 확인하며 배운 알파벳으로 읽을 수 있는 단어인지 확인했다.

이때는 AND 비트연산(`&`)을 활용했다.

배운 언어와 남극 언어의 단어를 AND 비트연산하면 공통된 부분이 추출된다.

공통된 부분이 남극 언어의 단어와 같은 경우, 배운 알파벳으로 해당 단어를 읽을 수 있다는 뜻이다.

e.g. 'antancttica'
배운 언어와 남극 언어의 단어를 AND 비트연산한 결과가 남극 언어의 단어와 일치한다. 학생들은 해당 단어를 읽을 수 있다.



e.g. `antabndtica'
배운 언어와 남극 언어의 단어를 AND 비트연산한 결과가 남극 언어의 단어와 불일치한다. 학생들은 해당 단어를 읽을 수 없다.

 

 

1. `fixedBit`: [a, n, t, i, c]은 필수로 배워야하는 알파벳이므로 미리 비트로 만들어 저장한다.
2. 남극 언어의 단어들을 순차적으로 입력받는다. 단어별로 포함된 알파벳을 모두 OR 비트연산한 후 `wordsBit`에 저장한다.
3. 부분집합을 활용하여 알파벳을 배우는 함수 `checkedAlphabet`을 호출한다.
        1. `index`: 현재 배울 예정인 알파벳의 순서를 저장한다. (`a`= 0, `b`= 1, ... )
        2. `currentBit`: 학생들이 배운 알파벳들을 OR 비트연산한 결과를 저장한다.
4. 학생들이 배운 알파벳 개수가 `K`개인 경우
        1. 남극 언어의 단어들을 순차적으로 확인하며 학생들이 배운 알파벳으로 만들 수 있는 단어인지 확인한다. 맞다면 `wordCount`를 1 더한다.
        2. `wordCount`가 `max`보다 크다면 `max`값을 갱신한다.
        3. 함수를 종료한다.
5.  모든 알파벳을 확인했다면 함수를 종료한다.
6. `currentBit`에 포함되지 않은 알파벳이라면 포함시킨 후 `checkedAlphabet`를 재귀호출한다. ([a, n, t, i, c] 때문에 조건이 붙는다)
7.  현재 알파벳을 포함시키지 않고, `checkedAlphabet`를 재귀호출한다.
8. `checkedAlphabet`가 모두 종료되면 `max`를 출력한다.

💻 전체 코드

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Main {
    static int K, max, fixedBit;
    static int[] wordsBit;

    public static void main(String[] args) throws Exception {
        init();
        checkedAlphabet(0, fixedBit);
        System.out.println(max);
    }

    static void init() throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String input = br.readLine();
        int N = Integer.parseInt(input.substring(0, input.indexOf(' ')));
        K = Integer.parseInt(input.substring(input.indexOf(' ') + 1));
        max = 0;
        fixedBit = (1 << ('a' - 'a')) | (1 << ('n' - 'a')) | (1 << ('t' - 'a')) | (1 << ('i' - 'a')) | (1 << ('c' - 'a'));
        wordsBit = new int[N];
        for (int n = 0; n < N; n++) {
            input = br.readLine();
            int bit = fixedBit;
            for (int i = 4; i < input.length() - 4; i++) {
                bit |= (1 << (input.charAt(i) - 'a'));
            }
            wordsBit[n] = bit;
        }
    }

    static void checkedAlphabet(int index, int currentBit) {
        if (Integer.bitCount(currentBit) == K) {
            int wordCount = 0;
            for (int wordBit : wordsBit) {
                if ((currentBit & wordBit) == wordBit) {
                    wordCount++;
                }
            }
            if (max < wordCount) max = wordCount;
            return;
        }
        if (index == 26) return;

        if ((currentBit & (1 << index)) == 0) checkedAlphabet(index + 1, currentBit | (1 << index));
        checkedAlphabet(index + 1, currentBit);

    }
}

💫 리뷰

10개월 전에 도전했다가 실패했던 문제! 나.. 성장했다!!! ๑•̀∀•́ฅ

비트마스킹은 문제를 보고 비트마스킹으로 풀어야 겠다는 생각이 바로 안나서 어렵지만, 비트마스킹 문제라는것을 알고 풀이를 고민하는 과정은 재밌다.ㅎㅎ

 

11578번: 팀원 모집

3번 학생과 4번 학생을 선택하면 1번부터 5번까지 모든 문제를 풀 수 있는 팀을 만들 수 있다. 1번, 2번, 4번 학생을 선택해도 모든 문제를 다 풀 수 있지만 팀원의 수가 3명이라 답이 될 수 없다.

www.acmicpc.net

소요 시간: 15분
메모리: 18096KB
시간: 220ms

🔥 문제

강호는 모든 문제를 풀 수 있는 팀을 만들고 싶어 한다. 강호가 선택할 수 있는 팀원의 목록과 각각의 팀원들이 해결할 수 있는 문제의 번호들이 주어졌을 때 강호가 IUPC에서 최소한의 팀원으로 모든 문제를 다 풀어 우승할 수 있도록 팀을 만들어보자.

 

입력

첫 번째 줄에 문제의 수 N과 강호가 팀원으로 고를 수 있는 학생들의 수 M이 공백을 구분으로 차례대로 주어진다. N과 M은 1이상 10이하의 자연수이다.

두 번째 줄부터 M개의 줄에 차례대로 i(1 ≤ i ≤ M)번 학생들이 풀 수 있는 문제의 개수 Oi와 i번 학생이 풀 수 있는 문제의 번호 Pij(1 ≤ j ≤ Oi, 1 ≤ Pij ≤ N)가 Oi개 주어진다.

 

출력

모든 문제를 풀 수 있으면서 팀원의 수가 가장 적은 팀을 구해 팀원의 수를 출력한다. 만약 모든 문제를 풀 수 있는 팀을 만들 수 없다면 -1을 출력한다.


🍀 풀이

문제 수가 최대 10개로 주어졌다. 2^10은 int형 범위 안에 있기 때문에 '현재 팀에서 풀 수 있는 문제'를 비트화하였다.


문제 번호를 자릿수처럼 활용하여 `n`번 문제를 비트화하는 경우 left shift 연산(`1<<n`)을 활용했다.

e.g. 3번 문제
1을 왼쪽으로 3만큼 비트 이동한다.
1000(bit) = 8(십진수)

한 학생이 여러 문제를 풀 수 있는 경우 OR 비트연산(`|`)을 활용했다.

e.g. 한 학생이 1, 4번 문제를 풀 수 있는 경우
10(bit) | 10000(bit) = 10010(bit) = 18(십진수)

팀을 구성할 때는 비트 수를 확인했다.

 

1. 영입하려는 학생이 현재 구성된 팀원들의 풀 수 없는 문제를 풀 수 있는지 확인한다.

  • 현재 구성된 팀원들이 풀 수 있는 문제를 모두 OR 비트연산 한 결과(A)
  • A에 영입하려는 학생이 풀 수 있는 문제를 OR 비트연산 한 결과(B)

  • (1) A와 B의 비트 수가 같다면, 해당 학생이 풀 수 있는 문제는 이미 팀원들이 풀 수 있는 문제라는 의미이다.
  • (2) A의 비트 수보다 B의 비트 수가 크다면, 해당 학생이 풀 수 있는 문제 중 현재 구성된 팀원들의 풀 수 없는 문제가 있다는 의미이다.

 

2. 현재 구성된 팀원들의 모든 문제를 풀 수 있는 순간 팀원 영입은 끝난다.

  • 현재 구성된 팀원들의 비트 수가 N개라면, 모든 문제를 풀 수 있다는 의미이다.

1. userInfos에 해당 학생이 풀 수 있는 문제 수와 문제들을 OR 비트연산한 결과를 저장한다.
2. 입력 값을 다 저장했다면, userInfos를 풀 수 있는 문제 수의 내림차순으로 정렬한다.
3. makeTeam함수에서는 비트연산과 재귀를 통해 강호의 팀을 구성한다.
        1. currentBit: 현재까지 영입한 팀원들이 풀 수 있는 문제들을 모두 OR 비트연산한 결과
        2. startIndex: 현시점에서 학생 배열에서 확인할 시작 인덱스값
        3. memberCount: 팀원 수
4. answer보다 memberCount가 크거나 같다면 함수를 종료한다. (현재 팀은 answer보다 최소값이 나올 수 없다.)
5. 현재 구성된 팀으로 모든 문제를 풀 수 있다는 의미이므로 answer를 memberCount로 갱신하고 함수를 종료한다.
6. startIndex번째 학생부터 순차적으로 정보를 확인한다.
        1. m번째 학생이 현재 팀원들이 풀 수 없는 문제를 풀 수 있다면, 영입 후 makeTeam함수를 재귀로 호출한다.
7. makeTeam함수가 모두 종료되면 answer를 출력한다.

💻 전체 코드

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

public class Main {
    static int N, M, answer;
    static int[][] userInfos;

    public static void main(String[] args) throws Exception {
        init();
        makeTeam(0, 0, 0);
        System.out.println(answer == M + 1 ? -1 : answer);
    }

    static void init() throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine(), " ");
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        answer = M + 1;

        userInfos = new int[M][2];
        for (int m = 0; m < M; m++) {
            st = new StringTokenizer(br.readLine(), " ");
            int count = Integer.parseInt(st.nextToken());
            int bit = 0;
            for (int i = 0; i < count; i++) {
                bit |= (1 << Integer.parseInt(st.nextToken()));
            }
            userInfos[m] = new int[]{count, bit};
        }
        Arrays.sort(userInfos, (o1, o2) -> o2[0] - o1[0]);
    }

    static void makeTeam(int currentBit, int startIndex, int memberCount) {
        if (answer <= memberCount) return;
        if (Integer.bitCount(currentBit) == N) {
            answer = memberCount;
            return;
        }

        for (int m = startIndex; m < M; m++) {
            if (Integer.bitCount(currentBit) < Integer.bitCount(currentBit | userInfos[m][1])) {
                makeTeam(currentBit | userInfos[m][1], m + 1, memberCount + 1);
            }
        }
    }
}

💫 리뷰

많이 어렵지 않았던 비트마스킹 문제! 

+ Recent posts