[BOJ 12899] - 데이터 구조 (Platinum 4)

KongUm·2022년 11월 30일
1

문제

자연수를 저장하는 데이터베이스 S에 대해 다음의 쿼리를 처리합시다.

유형 1 : SS에 자연수 XX를 추가한다.

유형 2 : SS에 포함된 숫자 중 XX번째로 작은 수를 응답하고 그 수를 삭제한다.

입력:

첫째 줄에 사전에 있는 쿼리의 수 NN 이 주어집니다. (1N2,000,000)(1 ≤ N ≤ 2,000,000)

둘째 줄부터 N개의 줄에 걸쳐 각 쿼리를 나타내는 2개의 정수 TT XX가 주어집니다.

TT가 1이라면 SS에 추가할 XX가 주어지는 것입니다. (1X2,000,000)(1 ≤ X ≤ 2,000,000)

TT가 2라면 XXSS에서 삭제해야 할 몇 번째로 작은 수인지를 나타냅니다. SS에 최소 XX개의 원소가 있음이 보장됩니다.

출력:

유형 2의 쿼리 개수만큼의 줄에 각 쿼리에 대한 답을 출력합니다.

접근 방법

QueryQuery 11은 기본적으로 O(1)O(1)의 시간복잡도를 가진다.
하지만 QueryQuery 22를 S의 오름차순을 따라 탐색을 하는 매우 단순한 방법으로 처리를 하면 최악의 경우 O(N)O(N)의 시간복잡도를 가지게 된다.
즉 문제 전체의 시간복잡도는 O(N2)O(N^2)이 되어 주어진 시간을 초과하게 된다.

처음 아이디어를 얻게된 곳은 추가되는 수인 X의 범위가 (1X2,000,000)(1 ≤ X ≤ 2,000,000) 로 꽤 좁다는 것이였다.
사실 잘 생각을 해보면 S에 들어있는 수 중 X번째로 작은 수를 찾는 것은 S에 들어있는 각 수의 개수를 그 수의 인덱스에 저장해 둔 배열의 1 ~ ?까지의 구간합을 이용해서 찾을 수 있다.
즉 배열의 값이 변화할 때 구간합을 효율적으로 구할 수 있게 해주는 자료구조인 "세그먼트 트리"를 이용하면
QueryQuery 1,21, 2 모두 O(logN)O(log N)의 시간 복잡도로 처리해줄 수 있게 된다.

구현

a = 2000000 # S에 추가될 수 있는 가장 큰 수
seg_tree = [0] * (a * 4) # 세그먼트 트리 배열

우선 배열의 정의를 arr[i]=arr[i] = SS에 들어있는 ii의 개수로 정의했다.
처음에는 어떠한 수도 SS에 들어있지 않으므로 세그먼트 트리 배열의 모든 값들을 0으로 초기화 해주었다.

def update(start, end, node, target, diff):
   if target < start or target > end:
       return

   seg_tree[node] += diff

   if start == end:
       return
   mid = (start + end) // 2
   update(start, mid, node * 2, target, diff)
   update(mid + 1, end, node * 2 + 1, target, diff)

QueryQuery 11을 처리해줄 update 함수이다.

def find(start, end, node, target): 
    seg_tree[node] -= 1

    if start == end:
        return start

    mid = (start + end) // 2
    if seg_tree[node * 2] >= target:
        return find(start, mid, node * 2, target)
    else:
        return find(mid + 1, end, node * 2 + 1, target - seg_tree[node * 2])

이 문제에서 가장 핵심이라고 할 수 있는 QueryQuery 22를 처리해주는 find 함수이다.
평범한 방법으로는 O(N)O(N)이 걸리는 쿼리를 완전 이진 트리의 특성을 이분탐색처럼 이용하여 O(logN)O(logN)에 처리할 수 있게 해준다.

우선 QueryQuery에서 주어진 XX를 target에 넣어준다. 만약 현재 노드의 왼쪽 자식 노드가 가지는 구간 합이 target 보다 같거나 크다면 XX번째로 작은 수의 인덱스가 왼쪽 자식 노드에 있는 것임으로 왼쪽 노드를 탐색해준다.

그게아니라 만약, 왼쪽 자식 노드가 가지는 구간합이 target보다 작다면 XX번째로 작은 수의 인덱스가 오른쪽 자식 노드에 있는 것임으로 오른쪽 노드를 탐색해준다.
이때 우리가 찾아야하는건 오른쪽 노드에 target에 맞는 노드를 찾는 것임으로
target - seg_tree[node * 2]으로 찾아야 하는 target 값을 갱신해준다.

계속 탐색을 진행해다 start==endstart == end 인 리프노드에 도달하면 배열의 정의에 따라 리프노드가 가지는 범위를 반환해준다.

근데 그럼 seg_tree[node] -= 1 이건 뭘까?

처음에는 XX번째로 작은 수를 지우는 작업을 update 함수로 따로 처리해줄려고 했다.
하지만 시간초과가 나서 좀 더 효율적으로 처리하는 방법을 고민해 보았는데,
잘생각해보면, find 함수가 방문하는 트리의 노드의 구간은 XX가 포함되어 있다.
또한 방문했던 노드를 한 쿼리에서 다시 방문할 필요도 없기 때문에 방문하자 마자 각 노드의 값을 -1하면 되겠다고 생각했다.

후기

배열의 정의를 arr[i]=arr[i] = SS에 들어있는 ii의 개수로 두는 것은 매우 빠르게 생각을 해냈는데
find 함수를 응용하는 것이 생각보다 어려웠다.
트리의 구조와 원리를 잘 이용한 좋은 문제라고 생각된다.

profile
Problem Solving & Competitive Programming

0개의 댓글