문제

수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오.

예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {1020, 10, 30, 20, 50} 이고, 길이는 4이다.

입력

첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다.

둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (1 ≤ Ai ≤ 1,000,000)

출력

첫째 줄에 수열 A의 가장 긴 증가하는 부분 수열의 길이를 출력한다


LIS(최장 증가 부분 수열) 알고리즘 문제 입니다.

 

LIS의 길이를 구하는 방법은 크게 두가지가 있습니다. 

(1) DP : O(N^2) 

(2) Binary Search : O(NlogN)

 

N의 크기가 최대 1,000,000이 주어지므로 (2)를 사용하여 문제를 해결할 수 있습니다.

 

Binary Search로 LIS의 길이를 찾는 방법은 다음 예시로 설명드리겠습니다.

입력
6
10 20 15 19 30 18 // 배열 A

 

 

1) LIS 배열의 첫 원소는 주어진 배열 A의 첫 원소로 초기화합니다.

  • LIS[0] = A[0]; 
    LIS배열 [ 10 ]

2) A의 두번째 원소부터 LIS 배열에 들어갈 자리를 Binary Search를 통해 찾아 삽입합니다. 

  • BinarySearchkeyLowerBound(key보다 크거나 같은 첫번째 위치)를 반환합니다.
  • 현재 LIS배열은 [ 10 ]로 20보다 크거나 같은 값이 없으므로 반환 값은 1이 됩니다.
  • 인덱스 1에 20(key)을 삽입합니다.
  • LIS 배열 [ 10, 20

3) A의 세번째 원소 ( A[2] = 15 )

  • LIS배열은 [ 10, 20 ]로 BinarySearch가 반환하는 값은 1입니다.
  • 인덱스 1의 위치에 15(key)을 삽입합니다.
  • LIS 배열 [ 10, 15

4) A의 네번째 원소 ( A[3] = 19 )

  • LIS 배열은 [ 10, 15 ]로 BinarySearch가 반환하는 값은 2입니다.
  • 인덱스 2의 위치에 19(key)을 삽입합니다.
  • LIS 배열 [ 10, 15, 19 ]

5) A의 다섯번째 원소 ( A[4] = 30 )

  • LIS 배열은 [ 10, 15, 19 ]로 BinarySearch가 반환하는 값은 3입니다.
  • 인덱스 3의 위치에 30(key)을 삽입합니다.
  • LIS 배열 [ 10, 15, 19, 30 ]

6) A의 여섯번째 원소 ( A[5] = 18 )

  • LIS 배열은 [ 10, 15, 19, 30 ]로 BinarySearch가 반환하는 값은 2입니다.
  • 인덱스 2의 위치에 18(key)을 삽입합니다.
  • LIS 배열 [ 10, 15, 18, 30

이처럼 Binary Search를 이용한 방법은 LIS의 길이를 찾을 수 있지만, 정확한 LIS 배열을 찾지는 못합니다.

 

전체코드

더보기
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

// 가장 긴 증가하는 부분 수열 2
public class Main {

    static int A[];
    static int N;
    static List<Integer> LIS;

    private int binarySearch(List<Integer> list, int key) {
        int l = 0;
        int r = list.size() - 1;

        while (l <= r) {
            int mid = (l + r) / 2;

            int midVal = list.get(mid);

            if (midVal > key) {
                r = mid - 1;
            } else if (midVal < key) {
                l = mid + 1;
            } else {
                l = mid;
                break;
            }
        }

        return l;
    }


    private void solution() throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;
        N = Integer.parseInt(br.readLine());
        st = new StringTokenizer(br.readLine());
        LIS = new ArrayList<>();
        A = new int[N];

        for (int i = 0; i < N; i++) {
            A[i] = Integer.parseInt(st.nextToken());
        }

        LIS.add(A[0]);

        for (int i = 1; i < N; i++) {
            int idx = binarySearch(LIS, A[i]);
            if (idx == LIS.size())
                LIS.add(A[i]);
            else
                LIS.set(idx, A[i]);
        }

        System.out.println(LIS.size());
    }

    public static void main(String[] args) throws Exception {
        new Main().solution();
    }


}

 

 

드래곤 앤 던전

문제

용사는 공주를 구하기 위해 무시무시한 용이 있는 던전으로 향하기로 하였습니다. 우선 용사는 용사 자신과 던전을 분석하였습니다.

용사에게는 세 종류의 능력치가 있습니다. 

  • HMaxHP : 용사의 최대 생명력입니다. 이 값은 1이상이어야 하며 던전에 들어간 이후로 변하지 않습니다.
  • HCurHP : 현재 용사의 생명력입니다. 던전에 들어가기 전 이 값은 용사의 최대 생명력 HMaxHP와 같습니다. 이 값은 HMaxHP보다 커질 수 없습니다.
  • HATK : 용사의 공격력입니다.

던전은 총 N개의 방으로 이루어져 있고 i번째 방을 통해서만 i+1번째 방으로 이동 할 수 있습니다. 방에는 포션이 있거나 몬스터가 있는데 몬스터가 있을 경우 몬스터를 쓰러트려야지 다음방으로 이동 할 수 있습니다. N번째 방에는 공주와 용이 있고, 용을 무찌르면 공주를 구할 수 있습니다.

몬스터가 있는 방에 올 경우 다음과 같이 전투가 진행됩니다.

  1. 용사의 공격력 HATK만큼 몬스터의 생명력을 깎습니다.
  2. 몬스터의 생명력이 0 이하이면 전투가 종료되고 용사는 다음 방으로 이동합니다.
  3. 몬스터의 공격력만큼 용사의 생명력 HCurHP를 깎습니다.
  4. 용사의 생명력이 0 이하이면 전투가 종료되고 용사는 사망합니다.
  5. 다시 1부터 진행합니다.

포션이 있는 방에 올 경우 포션을 마셔서 현재 용사의 생명력 HCurHP가 일정량 회복되고 공격력 HATK도 일정량만큼 증가됩니다. 회복된 생명력이 최대 생명력 HMaxHP보다 큰 경우 용사의 현재 생명력 HCurHP가 최대 생명력 HMaxHP와 같아집니다.

용사는 던전으로 향하기 전에 만반의 준비를 하고 있습니다. 용사는 수련을 하면 최대 생명력 HMaxHP를 늘릴 수 있는데 얼마나 수련해야 할지 고민입니다.

용사는 N번 방에 있는 용을 쓰러트리기 위한 최소의 HMaxHP를 여러분이 계산해주면 좋겠다고 합니다.

입력

첫 번째 줄에 방의 개수 N (1 ≤ N  ≤ 123,456) 과 용사의 초기 공격력 HATK (1 ≤ HATK  ≤ 1,000,000) 가 주어집니다.

i+1번째 줄엔 i번째 방의 정보를 나타내는 세개의 정수 ti, ai, hi (ti ∈ {1, 2}, 1 ≤ ai, hi  ≤ 1,000,000) 가 주어집니다. 

ti가 1인 경우 공격력이 ai이고 생명력이 hi인 몬스터가 있음을 나타내고, ti가 2인 경우 용사의 공격력 HATK를 ai만큼 증가시켜주고 용사의 현재 생명력 HCurHP를 hi만큼 회복시켜주는 포션이 있음을 나타냅니다.

출력

용사가 N번째 방에 있는 용을 쓰러트리기 위한 최소의 HMaxHP를 출력합니다.


HMaxHP로 완전탐색을 하는 경우 시간은 123,456 * e^6 * e^6으로 시간초과가 발생합니다. 

따라서 이분탐색을 통해 HMaxHP를 찾습니다.

 

이분탐색 코드

    private void findAnswer() {
        long l = 1;
        long r = 123456l * 1000000l * 1000000l;

        while (l <= r) {
            long mid = (l + r) / 2;
			
            // HMaxHP = mid로 탐색합니다.
            if (simulate(mid)) {
                r = mid - 1;
                answer = mid;
            } else {
                l = mid + 1;
            }
        }
    }

 

전체 코드

더보기
package BackJoon;

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

// 드래곤 앤 던전
public class BOJ16434 {

    static int N, H_atk;
    static Turn[] turns;
    static long answer;

    private boolean simulate(long maxHp) {
        long curHp = maxHp;
        long atk = H_atk;

        for (Turn turn : turns) {
            int t = turn.t;
            int a = turn.a;
            int h = turn.h;

            // 전투
            if (t == 1) {
                long i = h / atk;
                long j = h % atk;
                long cnt = ((j == 0) ? (i - 1) : (i));
                curHp -= (cnt) * a;
            } else {
                curHp = (Math.min(curHp + h, maxHp));
                atk += a;
            }

            if (curHp <= 0) {
                return false;
            }
        }
        return true;
    }

    private void findAnswer() {
        long l = 1;
        long r = 123456l * 1000000l * 1000000l;

        while (l <= r) {
            long mid = (l + r) / 2;

            if (simulate(mid)) {
                r = mid - 1;
                answer = mid;
            } else {
                l = mid + 1;
            }
        }
    }

    private void solution() throws Exception{
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer stringTokenizer = new StringTokenizer(bufferedReader.readLine());

        N = Integer.parseInt(stringTokenizer.nextToken());
        H_atk = Integer.parseInt(stringTokenizer.nextToken());

        turns = new Turn[N];

        for (int i = 0; i < N; i++) {
            stringTokenizer = new StringTokenizer(bufferedReader.readLine());

            int t = Integer.parseInt(stringTokenizer.nextToken());
            int a = Integer.parseInt(stringTokenizer.nextToken());
            int h = Integer.parseInt(stringTokenizer.nextToken());

            turns[i] = new Turn(t, a, h);
        }

        findAnswer();


        System.out.println(answer);
    }

    public static void main(String[] args) throws Exception {
        new BOJ16434().solution();
    }

    static class Turn{

        int t, a, h;

        public Turn(int t, int a, int h) {
            this.t = t;
            this.a = a;
            this.h = h;
        }
    }

}

피아노 체조 

문제

피아노를 사랑하는 시은이는 매일 아침 피아노 체조를 한다. 시은이는 N개의 악보를 가지고 있으며, 1번부터 N번까지의 번호로 부른다. 각 악보는 1 이상 109 이하의 정수로 표현되는 난이도를 가지고 있다. 난이도를 나타내는 수가 클수록 어려운 악보이다. 1 ≤ x ≤ y ≤ N 을 만족하는 두 정수 x, y를 골라 x번부터 y번까지의 악보를 번호 순서대로 연주하는 것이 피아노 체조이다.

시은이는 피아노 체조를 할 때, 지금 연주하는 악보가 바로 다음에 연주할 악보보다 어렵다면 실수를 한다. 다시 말하자면, i(x  i < y)번 악보의 난이도가 i + 1번 악보의 난이도보다 높다면 실수를 한다. 특히, 마지막으로 연주하는 y번 악보에선 절대 실수하지 않는다. 시은이는 오늘도 피아노 체조를 하기 위해 두 정수 x와 y를 골랐고, 문득 궁금한 것이 생겼다. 오늘 할 피아노 체조에서 실수하는 곡은 몇 개나 될까?

입력

첫 번째 줄에 악보의 개수 N(1 ≤ N ≤ 100,000)이 주어진다.

두 번째 줄에 1번 악보부터 N번 악보까지의 난이도가 공백을 구분으로 주어진다.

세 번째 줄에 질문의 개수 Q(1 ≤ Q ≤ 100,000)이 주어진다.

다음 Q개의 줄에 각 줄마다 두 개의 정수 x, y(1 ≤ x ≤ y ≤ N)가 주어진다.

출력

x번부터 y번까지의 악보를 순서대로 연주할 때, 몇 개의 악보에서 실수하게 될지 0 이상의 정수 하나로 출력한다. 각 출력은 개행으로 구분한다.


누적합을 이용하면 정해진 시간 내로 해결할 수 있는 문제입니다.1번 악보부터 N번 악보까지의 실수의 합을 저장합니다.ex) prefix[5] : 1번부터 5번 악보까지 연주했을 때 실수의 합

더보기
package BackJoon;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

// 피아노 체조
public class BOJ21318 {

    static int prefixSum[];
    static int N, Q;

    private void solution() throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        N = Integer.parseInt(br.readLine());

        String[] s = br.readLine().split(" ");
        prefixSum = new int[N + 1];
        prefixSum[1] = 0;

        for (int i = 1; i < N; i++) {
            int a = Integer.parseInt(s[i - 1]);
            int b = Integer.parseInt(s[i]);

            prefixSum[i + 1] = prefixSum[i] + ((a > b) ? 1 : 0);
        }

        Q = Integer.parseInt(br.readLine());

        for (int i = 0; i < Q; i++) {
            String[] input = br.readLine().split(" ");
            int from = Integer.parseInt(input[0]);
            int to = Integer.parseInt(input[1]);
            bw.append(prefixSum[to] - prefixSum[from] + "\n");
        }

        bw.flush();
    }

    public static void main(String[] args) throws Exception {
        new BOJ21318().solution();
    }

}

 

민서의 응급 수술

 
시간 제한메모리 제한제출정답맞힌 사람정답 비율
1 초 1024 MB 2751 1058 815 37.334%

문제

민서는 강원대학교 컴퓨터공학과의 신임 교수이다. 그녀가 저술한 효율적인 택배 배달을 위한 최적 경로 설계에 관한 연구 논문은 아직도 널리 인용되고 있다. 오늘도 열심히 강의를 하던 민서는 놀라 자빠질 수밖에 없었다. 한 학생이 꾸벅꾸벅 졸다가 책상에 머리를 아주 세게 박았기 때문이다. 한시라도 수술이 시급한 상황, 민서는 의사가 되어 수술을 집도하기로 결심하였다.

사람의 뇌는 수백억 개의 뉴런으로 구성되며, 각 뉴런은 시냅스를 통하여 연결된다. 민서의 진찰 결과, 학생은 뇌 속의 일부 뉴런의 연결이 끊어져 잠이 든 것으로 확인되었다. 끊어진 시냅스만 복구된다면 학생은 잠에서 깨어나겠지만, 알다시피 민서는 컴퓨터공학과 교수이다.

민서는 끊어진 시냅스를 복구하는 대신 뇌 속의 모든 뉴런을 하나의 트리 형태로 연결해보고자 한다. 여기서 트리란 사이클이 존재하지 않는 연결 그래프를 의미한다.

민서는 손기술이 뛰어나기 때문에 다음과 같은 연산을 무한히 수행할 수 있다. 연결되지 않은 두 뉴런을 연결하거나 이미 연결된 두 뉴런의 연결을 끊는다.

뉴런의 연결 정보가 주어졌을 때, 모든 뉴런을 하나의 트리 형태로 연결하기 위하여 필요한 최소 연산 횟수를 구하는 프로그램을 작성하시오.

입력

첫 번째 줄에 뉴런의 개수 N과 시냅스의 개수 M이 주어진다.

이후 M개의 줄에 걸쳐 시냅스로 연결된 두 뉴런의 번호 u, v가 주어진다.

모든 입력은 공백으로 구분되어 주어진다.

출력

첫 번째 줄에 모든 뉴런을 트리 형태로 연결하기 위하여 필요한 최소 연산 횟수를 출력한다.

제한

  • 2 ≤ N ≤ 100,000
  • 1 ≤ M ≤ min(N × (N – 1) / 2, 100,000)
  • 1 ≤ u, v ≤ N
  • u ≠ v
  • 두 뉴런 사이에는 최대 1개의 시냅스만 존재한다.

 

 

풀이 과정

DFS로 풀어보려고 했지만 실패해서 찾아보니, Union-find 알고리즘을 사용하면 쉽게 해결할 수 있는 문제였습니다.

 

풀이

  1. 연결된 뉴런끼리 집합을 만듭니다. 
  2. 집합을 만드는 과정에서 사이클이 존재하는 경우 : 시냅스 끊기 연산 횟수 + 1
    (두 뉴런이 이미 같은 집합에 존재한다면 사이클이 존재함을 알 수 있다.)
  3.  만들어진 집합들의 수를 세고, 각 집합을 연결하는 연산의 수를 셉니다. ( 총 집합의 개수 - 1 ) 

>> 코드의 주석을 확인해주세요!


전체 코드

더보기
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;

// 민서의 응급 수술
public class Main {
    
    static int N, M;
    static int parents[];
    static int count;

	// 부모 노드(속한 집합)를 찾는 메서드 
    private int find(int node) {

        if (parents[node] < 0) {
            return node;
        }

        return parents[node] = find(parents[node]);
    }
    
    // 집합 a와 b를 합치는 메서드
    private boolean union(int a, int b){

        // 각 노드의 집합을 찾습니다.
        int pA = find(a);
        int pB = find(b);

        // 이미 같은 집합인 경우
        if (pA == pB) {
            return false;
        }

        // 다른 경우에는 집합을 합칩니다.
        parents[pA] = pB;
        return true;
    }
    

    private void main() throws Exception {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(bufferedReader.readLine());

        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        parents = new int[N + 1];
        Arrays.fill(parents, -1);
        
        // 설명에서 1, 2의 해당하는 부분
        for (int i = 0; i < M; i++) {
            st = new StringTokenizer(bufferedReader.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());

            // 연결된 두 노드를 합치고, 순환이 발생한 경우 count를 하나 세줍니다.
            if (!union(a, b)) {
                count++;
            }
        }

        // 집합의 수를 저장할 변수
        int groupCnt = 0;

        // 설명에서 3에 해당하는 부분입니다. 
        // 집합의 수를 세줍니다.
        for (int i = 1; i <= N; i++) {
            if (parents[i] < 0) {
                groupCnt++;
            }
        }

        // 총 연산 수 계산
        count += (groupCnt - 1);

        System.out.println(count);
    }
}

 

 

 

+ Recent posts