https://www.acmicpc.net/problem/2243
2243번: 사탕상자
첫째 줄에 수정이가 사탕상자에 손을 댄 횟수 n(1 ≤ n ≤ 100,000)이 주어진다. 다음 n개의 줄에는 두 정수 A, B, 혹은 세 정수 A, B, C가 주어진다. A가 1인 경우는 사탕상자에서 사탕을 꺼내는 경우이
www.acmicpc.net
학교 동아리 스터디에서 세그트리 연습문제로 나와 풀게 되었다.
스터디 수업때 다뤘던 연습문제와 세그트리를 적용하는 결이 달라 한참 고민을 하고 풀었다.
보통 '세그먼트 트리' 하면 유명한 연습문제가 '구간 합 구하기' 이다.
https://www.acmicpc.net/problem/2042
2042번: 구간 합 구하기
첫째 줄에 수의 개수 N(1 ≤ N ≤ 1,000,000)과 M(1 ≤ M ≤ 10,000), K(1 ≤ K ≤ 10,000) 가 주어진다. M은 수의 변경이 일어나는 횟수이고, K는 구간의 합을 구하는 횟수이다. 그리고 둘째 줄부터 N+1번째 줄
www.acmicpc.net
이 문제의 경우 주어진 수열에 대해 '특정 구간' 을 잡고 그 구간의 합을 구하면 된다.
따라서 주어진 수열에 따라 미리 세그트리를 설정해두고, 숫자가 바뀌면 업데이트, 구간 조회를 요청하면 요청 결과를 세그 트리 내 각 구간 별 데이터를 잘 조합해 돌려주면 되었다.
그러나 사탕상자 문제는 구간 합 구하기 문제와 다른 점이 있었다.
우선 구간 합 구하기 문제의 경우 '구하는 것' 이 문제 제목에도 명시되어 있듯이 '구간 합' 이므로 세그 트리에 '구간 합' 을 저장하면 되었다.
리프 노드에는 각 수열의 구성 숫자가 들어가는데, 이 역시 재귀적으로는 크기가 1인 '구간의 합' 과 같으니 세그 트리 구성시에 이해하는 난이도가 높지 않았다.
하지만 사탕상자 문제는 생각할 요소가 많아서 무엇을 저장해야 하는지 결정하기가 힘들었다.
우선 '우선 순위가 높은' 사탕을 돌려주어야 하니, 각 사탕의 우선순위를 고려하여 저장해야 할 것 같은 느낌이 들었다.
세그먼트 트리가 BST와 정말 헷갈리는 게, 세그트리는 각 노드 위치의 의미가 '구간' 이고, BST는 각 노드의 위치 의미가 '순서' 라서 '우선 순위' 를 생각 하면서 아이디어를 생각하니 자연스럽게 세그먼트 트리가 아니라 BST로 구현을 하려고 하게 되었다.
또한 각 우선 순위 (맛) 별로 사탕을 추가하고 빼는 과정이 있다는 점이 더 혼란스럽게 했다.
사탕의 갯수가 증가하고 감소하는 입력을 보면서 마치 힙에다 데이터를 추가하고 빼는 과정을 연상하게 되니 머릿속에서 세그먼트 트리, BST, 힙이 왔다갔다 하면서 더 혼란스러웠다.
그러다가 세그먼트 트리의 특징인 '구간 쿼리' 에 좀 더 집중해서 어떻게 해야 '구간' 을 설정할 수 있을지 고민해보기 시작했다.
그러자 한가지 아이디어가 떠올랐는데, 바로 '사탕 맛' 의 구간을 잡는 것이다.
루트 노드를 1번째 맛부터 100만번쨰 맛까지 사탕의 개수의 총 합으로 생각하자.
그리고 루트 노드의 왼쪽 노드를 1번째 맛부터 50만번째 맛까지 사탕의 개수의 총합으로
루트 노드의 오른쪽 노드를 50만 + 1번째 맛부터 100만번째 맛까지 사탕의 개수의 총 합으로 생각하자.
이런 식으로 반씩 구간을 나눠 사탕 갯수의 총 합을 쭉 저장하는 구조로 생각하고 세그트리를 짜면 된다.
이 문제는 '구간 합 구하기' 문제와 달리 전체 수열이 주어지지 않고, 나중에 사탕이 추가되는 방식이라 헷갈린다.
이는 간단하게 초기에 각 맛 별 사탕의 개수가 0개인 것으로 생각하면 된다.
그리고 사탕이 추가되면, 해당 맛의 사탕 갯수를 그 추가된/빼는 정도만큼 바꾸도록 업데이트 한 뒤, 이를 전체 구간합 노드에 반영해주면 된다.
쿼리를 할 때는, 마치 이분탐색을 하듯이 생각하면 된다.
만약 1부터 50만번째 사탕의 개수가 25개, 50만+1부터 100만까지 사탕의 개수가 25개라고 해보자.
(1부터 100만까지 사탕의 개수는 50개이다.)
이 상황에서 27번째 사탕은 어떻게 찾을 수 있을까?
27은 50이하 이므로 27순위의 사탕은 1부터 100만번째 맛 범위 안에 안에 있다.
이제 왼쪽 서브트리를 보자.
왼쪽 서브트리가 나타내는 구간인 1부터 50만까지 범위에는 사탕이 총 25개 있다.
즉, 1등부터 25등까지가 왼쪽 서브트리에 있는 것이다.
27번째는 27등이므로 절대 왼쪽에 있을 수 없다.
따라서 오른쪽을 탐색한다.
오른쪽 서브트리에는 (그 부모 노드의 등수 기준) 26등부터 50등까지가 존재한다.
하지만 나중에 오른쪽 서브트리를 탐색할 때는, 그 오른쪽 서브트리 기준으로 1등부터 25등 사이를 탐색해야 한다.
따라서 구하는 등수 27에서 기존 왼쪽 서브트리에 들어있던 사탕 개수인 25를 빼준다.
그러면 자연스럽게 오른쪽 서브트리에서 27 - 25 = 2등인 사탕을 구하게 된다.
이를 재귀적으로 수행하면 우리가 원하는 맛의 사탕을 찾을 수 있다.
원하는 맛의 사탕을 찾은 뒤, 그 사탕을 '빼야' 하므로, 다시 해당 맛의 사탕 개수를 1개 빼주도록 업데이트 하면 사탕을 찾는 문제 요구도 해결된다.
이를 소스코드로 구현하면 아래와 같다.
import sys
input = sys.stdin.readline
def update(node, start, end, taste, diff):
if taste < start or end < taste:
return seg_tree[node]
if start == end:
# start == end == taste:
seg_tree[node] += diff
return seg_tree[node]
mid = (start + end) // 2
left = update(node*2, start, mid, taste, diff)
right = update(node*2+1, mid+1, end, taste, diff)
seg_tree[node] = left + right
return seg_tree[node]
def query(node, start, end, rank):
if start == end:
return start
mid = (start + end) // 2
# start ~ mid 범위에 찾는 등수가 있다면 왼쪽만 탐사 / 아니라면 오른쪽을 탐사
if rank <= seg_tree[node*2]:
return query(node*2, start, mid, rank)
return query(node*2+1, mid+1, end, rank - seg_tree[node*2])
# (0하고) 1부터 100만까지 100만(+1)개의 리프노드가 존재한다.
seg_tree = [ 0 for _ in range(4000001)]
n = int(input())
for _ in range(n):
a, *b = map(int, input().split())
if a == 1:
taste = query(1, 1, 1000000, b[0])
print(taste)
update(1, 1, 1000000, taste, -1)
else:
update(1, 1, 1000000, b[0], b[1])
이 문제는 중복된 수가 존재하는 정렬된 수열에서 특정 숫자들을 추가하거나 뺐을 때 정렬을 유지하면서 원하는 시점에서 특정 위치의 숫자를 판별하는 문제와 같다.
'알고리즘 (PS) > BOJ' 카테고리의 다른 글
[백준] 20055 - 컨베이어 벨트 위의 로봇 (1) | 2024.01.31 |
---|---|
[백준] 3653 - 영화수집 (P4) (0) | 2023.11.17 |
[백준] 27172 - 수 나누기 게임 (G5) (0) | 2023.11.06 |
[백준] 30105 - 아즈버의 이빨 자국 (G5) (0) | 2023.10.31 |
[백준] 2629 - 양팔저울 (G3) (0) | 2023.10.28 |