이 포스팅은 Algorithm Problem Solving 시리즈 113 편 중 15 번째 글 입니다.

  • Part 1 - 백준(1003번): 피보나치 함수
  • Part 2 - 백준(1010번): 다리놓기
  • Part 3 - 백준(1012번): 유기농 배추
  • Part 4 - 백준(10159번): 저울
  • Part 5 - 백준(10164번): 격자상의 경로
  • Part 6 - 백준(1018번): 체스판 다시 칠하기
  • Part 7 - 백준(1018번): 체스판 다시 칠하기
  • Part 8 - 백준(1022번): 소용돌이 예쁘게 출력하기
  • Part 9 - 백준(10775번): 공항
  • Part 10 - 백준(10816번): 숫자 카드2
  • Part 11 - 백준(10816번): 숫자카드 2
  • Part 12 - 백준(10819번): 차이를 최대로
  • Part 13 - 백준(10830번): 행렬 곱셈
  • Part 14 - 백준(10844번): 쉬운 계단수
  • Part 15 - This Post
  • Part 16 - 백준(1092번): 배
  • Part 17 - 백준(11003번): 최솟값 찾기
  • Part 18 - 백준(1102번): 발전소
  • Part 19 - 백준(11048번): 이동하기
  • Part 20 - 백준(11053번): 가장 긴 증가하는 부분 수열
  • Part 21 - 백준(11054번): 가장 긴 바이토닉 부분 수열
  • Part 22 - 백준(11055번): 가장 긴 증가하는 부분 수열2
  • Part 23 - 백준(11057번): 오르막 수
  • Part 24 - 백준(1120번): 문자열
  • Part 25 - 백준(11403번): 경로 찾기
  • Part 26 - 백준(11404번): 플루이드
  • Part 27 - 백준(1149번): RGB 거리
  • Part 28 - 백준(11559번): puyo puyo
  • Part 29 - 백준(11655번): ROT13
  • Part 30 - 백준(1167번): 트리의 지름
  • Part 31 - 백준(11722번): 가장 감소하는 부분 수열
  • Part 32 - 백준(12015번): 가장 긴 증가하는 부분 수열(LIS) 2
  • Part 33 - 백준(12851번): 숨바꼭질2
  • Part 34 - 백준(12852번): 1로 만들기 2
  • Part 35 - 백준(12865번): 평범한 배낭
  • Part 36 - 백준(1300번): K번째 수
  • Part 37 - 백준(1325번): 효율적인 해킹
  • Part 38 - 백준(13549번): 숨바꼭질3
  • Part 39 - 백준(13913번): 숨바꼭질4
  • Part 40 - 백준(14002번): 가장 긴 증가하는 부분 수열 4
  • Part 41 - 백준(1431번): 시리얼 넘버
  • Part 42 - 백준(1436번): 영화감독 숌
  • Part 43 - 백준(14499번): 주사위 굴리기
  • Part 44 - 백준(14888번): 연산자 끼워넣기
  • Part 45 - 백준(14889번): 스타트와 링크
  • Part 46 - 백준(14891번): 톱니바퀴
  • Part 47 - 백준(15658번): 연산자 끼워넣기 2
  • Part 48 - 백준(15686번): 치킨 배달
  • Part 49 - 백준(1697번): 숨바꼭질
  • Part 50 - 백준(1697번): 숨바꼭질
  • Part 51 - 백준(1707번): 이분 그래프(Bipartite Graph)
  • Part 52 - 백준(1708번): 볼록 껍질
  • Part 53 - 백준(17136번): 색종이 붙이기
  • Part 54 - 백준(1717번): 집합의 표현
  • Part 55 - 백준(17298번): 오큰수
  • Part 56 - 백준(17626번): Four Squares
  • Part 57 - 백준(18870번): 좌표 압축
  • Part 58 - 백준(1890번): 점프
  • Part 59 - 백준(1918번): 후위 표기식
  • Part 60 - 백준(1929번): 소수 구하기
  • Part 61 - 백준(1963번): 소수 경로
  • Part 62 - 백준(1965번): 상자넣기
  • Part 63 - 백준(1976번): 여행가자
  • Part 64 - 백준(1987번): 알파벳
  • Part 65 - 백준(1992번): 쿼드트리
  • Part 66 - 백준(2003번): 수들의 합2
  • Part 67 - 백준(20040번): 사이클 게임
  • Part 68 - 백준(2042번): 구간 합 구하기
  • Part 69 - 백준(2108번): 통계학
  • Part 70 - 백준(2110번): 공유기 설치
  • Part 71 - 백준(2156번): 포도주 시식
  • Part 72 - 백준(2193번): 이친수
  • Part 73 - 백준(2231번): 분해합
  • Part 74 - 백준(2250번): 트리의 높이와 너비
  • Part 75 - 백준(2293번): 동전 1
  • Part 76 - 백준(2294번): 동전 2
  • Part 77 - 백준(2343번): 기타 레슨
  • Part 78 - 백준(2468번): 안전 영역
  • Part 79 - 백준(2512번): 예산
  • Part 80 - 백준(2529번): 부등호
  • Part 81 - 백준(2565번): 전깃줄
  • Part 82 - 백준(2581번): 소수
  • Part 83 - 백준(2583번): 영역구하기
  • Part 84 - 백준(2630번): 색종이 만들기
  • Part 85 - 백준(2644번): 촌수계산
  • Part 86 - 백준(2667번): 단지번호붙이기
  • Part 87 - 백준(2751번): 수 정렬하기 2
  • Part 88 - 백준(2798번): 블랙잭
  • Part 89 - 백준(2904번): 수학은 너무 쉬워
  • Part 90 - 백준(2986번): 파스칼
  • Part 91 - 백준(3015번): 오아시스 재결합
  • Part 92 - 백준(3190번): 뱀
  • Part 93 - 백준(3190번): 벽 부수고 이동하기
  • Part 94 - 백준(4195번): 친구 네트워크
  • Part 95 - 백준(4948번): 베르트랑 공준
  • Part 96 - 백준(4963번): 섬의 개수
  • Part 97 - 백준(4991번): 로봇 청소기
  • Part 98 - 백준(5373번): 큐빙
  • Part 99 - 백준(5437번): 불
  • Part 100 - 백준(6171번): 땅따먹기
  • Part 101 - 백준(6603번): 로또
  • Part 102 - 백준(6603번): 로또
  • Part 103 - 백준(7562번): 나이트의 이동
  • Part 104 - 백준(7568번): 덩치
  • Part 105 - 백준(7576번): 토마토
  • Part 106 - 백준(7579번): 앱
  • Part 107 - 백준(7785번): 회사에 있는 사람
  • Part 108 - 백준(9012번): 괄호
  • Part 109 - 백준(9020번): 골드바흐의 추측
  • Part 110 - 백준(9184번): 신나는 함수 실행
  • Part 111 - 백준(9251번): LCS
  • Part 112 - 백준(9421번): 소수상근수
  • Part 113 - 프로그래머스: 가장 큰 정사각형 찾기
▼ 목록 보기

플래티넘5 : 세그먼트 트리 문제이다.

생각

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

입력값이 이렇게 주어진다고 생각해보자. 이 상태에서 3~6까지의 최소값을 구하는 것은, 단순한 방법으로 구해도 $O(n^2)$으로 풀 수 있다. 하지만 이러한 확인(쿼리)가 많아진다면, 급격하게 시간 복잡도가 올라간다. 따라서 이 문제에서 제시하는 m(100000) 개에 대해서 n(100000)을 모두 확인하는 방법으로 간다면 문제를 풀 수 없다.($O(n^2)$)

문제의 원인

그렇다면 어느 부분에서 연산이 많이 걸리는 지 생각해보자. 가장 큰 부분을 판단을 중복으로 한다는 것이다. 0~3까지의 최소값을 구하는 것과, 1~4까지의 최소값을 구하는 과정에는 1~3까지의 최소값을 비교하는 과정이 중복된다. 그렇다면, 처음에 주어진 index를 나누어, 그 나눈 구간에 대해 최소값을 구해놓으면 어떨까?

어떻게 나눌까?

나누겠다는 생각이 들고 난 후에는, 어떻게 하면 잘 나눌 수 있을 지에 대해 고민했다. 이 부분은 트리가 가장 용이하다. 정보를 저장하는 자료구조 중, 탐색에 있어 시간복잡도가 log이다. 나누는 방법은 생각 보다 간단하다.

Tree 구조

트리의 구조는 모양의 변화를 가지는 것이다. 우리가 보통 가지는 선형 리스트를 조금 바꾸어 생각하면 쉽게 트리 구조를 만들 수 있다.

image

image

이렇게 2의 제곱수만큼의 개수를 아래쪽으로 옮기면서 쌓으면 트리 구조가 된다. 이 때, 중요한 것은 가장 위층으로 부터 아래로 내려오는데 그 index들의 관계를 아는 것이다. 잘 보면, 1에서 2, 3은 $2\times 1$, $2\times 1 + 1$ 과 같다. 왼쪽 트리, 오른쪽 트리로 가는 관계는 계속 일관성을 가진다.

Segment Tree

그렇다면 이번에는 이 트리 구조를 이 문제에 맞는 모양으로 바꾸어 보자. 우리는 최소값을 미리 저장하기 위해 트리구조를 사용하기로 했는데, 그 구조를 아래와 같이 만들어서 생각해보자.

image

기본적인 트리구조는 같지만 추가된 변수가 있다. 그것은 index의 시작과 끝을 나타내는 start~end 변수이다. 이렇게 나누었다고 가정하고, 2~5까지의 최소값을 얻기위해 조사해야 하는 Node의 개수를 파악해보자.

image

이렇게 3개의 값만 조사하면 최소값을 얻을 수 있다! 이 때 발생하는 시간 복잡도는 $log(N)$ 이다. depth가 내려갈 수록 반씩 조사를 덜 할 수 있기 때문이다.

tree에 필요한 node 수

위 예시에서 총 7개의 원소를 넣기 위해 필요한 node의 개수는 총 13개 이다. 이 13개를 다 넣기 위해 필요한 트리의 깊이는 총 4이다. 이 값을 얻기 위한 수식은 다음과 같다.

\[depth = ceil(log_2(n)) + 1\\ treeSize = 2^{depth}\]

보통의 tree는 input으로 들어오는 요소의 개수와 트리의 node번호가 일치하지만, 이 경우는 depth가 1개 추가되므로 1을 더해주어야 한다. 이것을 코드로 구현하면 다음과 같다.

h = ceil(log2(n))+1;
treeSize = (1 << h);

<<은 shift 연산자로, 2진 연산을 할 때, 자리수를 올려주는 역할을 한다.

init (초기화)

처음에 input을 받고서, 가장 먼저 해야하는 일은, input을 tree안에 넣는 것이다. 그런데 우리가 설계한 세그먼트 트리를 생각해보면, 이 친구는 아래 node가 결정된 뒤에 상위 노드가 결정될 수 있다. 재귀로 짜면 해결될 것이다.

void init(vector<int>& input, vector<int>& tree, int node, int start, int end){
    if (start == end) {
        tree[node] = input[start];
        return;
    }
    init(input, tree, 2*node, start, (start+end)/2);
    init(input, tree, 2*node+1, (start+end)/2+1, end);
    tree[node] = min(tree[2*node], tree[2*node+1]);
    return;
}

init 함수의 argument로는 input 벡터, tree 벡터, 내가 기록할 node의 index, 처음 node가 커버하게 될 index의 시작과 끝이 필요하다. 그 뒤로는 아까 트리 구조에서 본 왼쪽, 오른쪽 노드로 가는 함수 관계에 따라 init함수를 계속 수행하면 된다. 종료조건은, 그림에서 보았듯이 start와 end과 같아지는 지점에서 input에 start 해당하는 index의 값을 넣어준다.

find

저장된 자료구조에서 찾는 과정이다. 내가 찾으려고 하는 범위을 left, right 라 하고, 내가 처음에 탐색을 시작할 노드의 번호를 node, 그 노드가 커버하는 index의 범위를 start, end라 하자. tree의 가장 위부터 탐색을 시작할 때, 범위에 걸리는 녀석들만 비교의 대상이 되어야 한다. 이런 범위를 따져보면 총 3가지로 나눌 수 있다.

  1. left, right 사이에 현재 노드의 커버범위가 들어온다.
    • 후보로 선정한다.
  2. left, right 와 현재 노드의 커버범위가 전혀 겹치지 않는다.
    • 버린다.
  3. 애매하게 걸친다.
    • 더 깊이 들어가서 조사한다.

이 과정을 구현하면 다음과 같다.

int findMin(vector<int>& tree, int node, int start, int end, int left, int right){
    if (left > end || right < start) {
        return INF;
    } else if (left <= start && end <= right){
        return tree[node];
    }
    int a = findMin(tree, 2*node, start, (start+end)/2, left, right);
    int b = findMin(tree, 2*node+1, (start+end)/2+1, end, left, right);
    return min(a, b);
}

Code

#include <iostream>
#include <cmath>
#include <vector>
using namespace std;

const int INF = 1111111111;

void init(vector<int>& input, vector<int>& tree, int node, int start, int end){
    if (start == end) {
        tree[node] = input[start];
        return;
    }
    init(input, tree, 2*node, start, (start+end)/2);
    init(input, tree, 2*node+1, (start+end)/2+1, end);
    tree[node] = min(tree[2*node], tree[2*node+1]);
    return;
}

int findMin(vector<int>& tree, int node, int start, int end, int left, int right){
    if (left > end || right < start) {
        return INF;
    } else if (left <= start && end <= right){
        return tree[node];
    }
    int a = findMin(tree, 2*node, start, (start+end)/2, left, right);
    int b = findMin(tree, 2*node+1, (start+end)/2+1, end, left, right);
    return min(a, b);
}


int main(){
    ios_base::sync_with_stdio(false);
    cin.tie(NULL); cout.tie(NULL);

    int n, m;
    cin >> n >> m;
    int h = int(ceil(log2(n)))+1;
    int treeSize = (1 << h);
    vector<int> tree(treeSize, INF);
    vector<int> a(n);

    for (int i = 0; i < n; i++) {
       cin >> a[i];
    }
    init(a, tree, 1, 0, n-1);

    for (int i = 0; i < m; i++) {
        int left, right;
        cin >> left >> right;
        cout << findMin(tree, 1, 0, n-1, left-1, right-1) << '\n';
    }

}

Reference

백준(10868번) - 최소값