만약 문제를 읽는데
1: 배열이 등장하고,
2: 연속된 부분수열로 무언가를 수행하고
3: 연속된 부분 수열의 구간이 계속 바뀐다면
세그먼트 트리 문제일 확률이 높습니다.
세그먼트 트리로만 풀 수 있는 문제는 없지만, 세그먼트 트리를 활용하면 쉽게 풀리는 문제가 존재합니다.
원소에 대한 갱신이 일어나는 경우
세그먼트 트리에서 세그먼트가 의미하는 것
많이 계산할 유형에 대해서 미리 계산함.
절반씩 나눠서 조각으로 만들고 해당 조각 내 원소의 총합을 저장함.
궁금한 구간의 쿼리가 들어올 경우 미리 만들어놓은 조각을 합쳐서 쿼리와 동일한 구간을 만든다.
조각이 많이 생기면 어떡하나요?
생각보다 조각이 많이 생기지 않습니다. 조각의 개수는 최대 2~4배까지 생성됩니다. 마지막 층에는 길이 1짜리 조각이 데이터 개수 만큼 생성이 됩니다. 위로 올라갈 수록 조각의 수는 1/2배가 됩니다.
n, n/2, n/4, ..., 1개처럼 1/2가 공비인 수열의 총합이기 때문에 대부분의 케이스에서 데이터가 개수를n으로 할 때, 4배에서 2배 정도 사이의 조각이 생성됩니다.
부모 노드의 대표값은 자식 노드의 대표값을 활용해서 계산할 수 있음
원소의 값이 바뀐다면?
루트노드부터 시작하여 대표값을 변경합니다. 이후 루트에서 리프노드에 도달하는 경로를 찾는다. 리프노드에서 경로를 따라 루트노드까지 다시 이동하면서 각 노드의 대표값을 수정합니다.
특정 구간의 합을 구할 경우?
// 세그먼트 트리 초기화
public static void init(int node, int nodeLeft, int nodeRight) {
if (nodeLeft == nodeRight) {
maxTree[node] = a[nodeLeft];
minTree[node] = a[nodeLeft];
return;
}
int mid = (nodeLeft + nodeRight) / 2;
init(
node * 2,
nodeLeft,
mid
);
init(
node * 2 + 1,
mid + 1,
nodeRight
);
maxTree[node] = Math.max(maxTree[node * 2], maxTree[node * 2 + 1]);
minTree[node] = Math.min(minTree[node * 2], minTree[node * 2 + 1]);
}
노드의 왼쪽구간과 오른쪽 구간이 같은 경우는 현재 노드가 리프 노드인 상황입니다. 최소 트리, 최대 트리, 총합트리 모두 구간이 1인 경우의 대표값은 자기 자신이므로 바로 값을 초기화하면됩니다.
if (nodeLeft == nodeRight) {
maxTree[node] = a[nodeLeft];
minTree[node] = a[nodeLeft];
return;
}
구간을 분할할 때는 절반씩 나누어 분배합니다.
int mid = (nodeLeft + nodeRight) / 2;
init(
node * 2,
nodeLeft,
mid
);
init(
node * 2 + 1,
mid + 1,
nodeRight
);
왼쪽 자식과 오른쪽 자식의 대표값 계산이 끝났으므로, 현재 노드의 대표값을 계산해야합니다.
maxTree[node] = Math.max(maxTree[node * 2], maxTree[node * 2 + 1]);
minTree[node] = Math.min(minTree[node * 2], minTree[node * 2 + 1]);
특정 원소가 변경될 경우?
public static void update(int node, int nodeLeft, int nodeRight, int queryIndex, int value) {
if (queryIndex < nodeLeft || nodeRight < queryIndex) {
return;
}
if (nodeLeft == nodeRight) {
maxTree[node] = value;
minTree[node] = value;
return;
}
int mid = (nodeLeft + nodeRight) / 2;
update(
node * 2,
nodeLeft,
mid,
queryIndex,
value
);
update(
node * 2 + 1,
mid + 1,
nodeRight,
queryIndex,
value
);
maxTree[node] = Math.max(maxTree[node * 2], maxTree[node * 2 + 1]);
minTree[node] = Math.min(minTree[node * 2], minTree[node * 2 + 1]);
}
update 함수에 들어오는 인자는 5개입니다.
queryIndex 위치의 값을 value 로 변경해야합니다.
node는 현재 탐색중인 노드를 의미하며 nodeLeft와 nodeRight는 현재 노드가 담당하고 있는 구간이 됩니다.
내가 담당하고 있지 않는 구간의 업데이트일 경우
if (queryIndex < nodeLeft || nodeRight < queryIndex) {
return;
}
하위 노드에 대해서 먼저 업데이트 처리를 진행해봅니다.
int mid = (nodeLeft + nodeRight) / 2;
update(
node * 2,
nodeLeft,
mid,
queryIndex,
value
);
update(
node * 2 + 1,
mid + 1,
nodeRight,
queryIndex,
value
);
자식 노드를 이용해서 현재 노드의 대표값을 업데이트합니다.
maxTree[node] = Math.max(maxTree[node * 2], maxTree[node * 2 + 1]);
minTree[node] = Math.min(minTree[node * 2], minTree[node * 2 + 1]);
특정 구간에서 변경이 일어날 경우
public static int queryMax(int node, int nodeLeft, int nodeRight, int queryLeft, int queryRight) {
if (queryRight < nodeLeft || nodeRight < queryLeft) {
return 0;
}
if (queryLeft <= nodeLeft && nodeRight <= queryRight) {
return maxTree[node];
}
int mid = (nodeLeft + nodeRight) / 2;
int leftMax = queryMax(
node * 2,
nodeLeft,
mid,
queryLeft,
queryRight
);
int rightMax = queryMax(
node * 2 + 1,
mid + 1,
nodeRight,
queryLeft,
queryRight
);
return Math.max(leftMax, rightMax);
}
case1. 완전히 안겹치는 경우 = 업데이트할 필요 없는 경우
쿼리의 최대범위 < 현대 노드의 최소 범위
현재 노드의 최대 범위 <쿼리의 최소 범위
if (queryRight < nodeLeft || nodeRight < queryLeft) {
return 0;
}
이 때 아무값이나 반환하면 안되고, 내가 구하고자하는 연산의 항등원 반환해야합니다.
더하거나 뺄 경우에는 0
최대값을 계산할 경우에는 -무한대나 문제 범위의 최소값
case2. 궁금한 구간이 현재 노드에서 전부 커버하는 경우
if (queryLeft <= nodeLeft && nodeRight <= queryRight) {
return maxTree[node];
}
case3. 그 외 경우
int mid = (nodeLeft + nodeRight) / 2;
int leftMax = queryMax(
node * 2,
nodeLeft,
mid,
queryLeft,
queryRight
);
int rightMax = queryMax(
node * 2 + 1,
mid + 1,
nodeRight,
queryLeft,
queryRight
);
return Math.max(leftMax, rightMax);
하위 노드를 찾아보면서 진행합니다.
비재귀 형식 vs 재귀형식
비재귀 형식 구현 (바텀업)은 특정 원소를 바꾸는 연산을 구현할 수 있지만, 특정 구간의 원소 전체를 변경하는 연산은 구현할 수 없습니다.