세그먼트 트리에 대해서 이해한대로 정리해보자!

 

세그먼트 트리는 전체 구간 중에서 일부 구간에 대한 변경이 있을 때

그 변경으로 인한 변화를 빠르게 알기 위해서 사용한다 ==> 구간탐색을 위해서 사용한다

 

임의의 배열에 1 2 3 4 5 라는 값이 있고, 이에 대한 세그먼트 트리를 만들기 위해서는

1 2 3 4 5 를 리프노드로 하는 일정범위(구간)에 대한 정보를 알아야 한다

 

배열을 이용해서 세그먼트트리를 표현해보자!

세그먼트 트리의 대표적인 문제는 구간 합을 해결하는 문제를 예로 들 수 있다.

가장 기본적인 구간 합을 예로 들어서 세그먼트 트리를 설명하려고 한다

 

세그먼트 트리의 가장 윗 부분의 인덱스(노드번호)를 1로 지정하게 되면,

임의의 노드의 자식노드는 *2, *2+1 의 인덱스를 가지게 된다(완전트리의 형식이기 때문)

 

루트 노드인 1번 노드는 1,2,3,4,5 에 대한 모든 범위[1,5] 에 대한 정보를 가진다.

루트 노드의 자식노드인 2(1*2)번, 3(1*2+1)번 노드는 [1,5] 의 범위에 대한 중간값을 기준으로

각각 [1,3], [4,5] 에 대한 정보를 가지게 된다

 

-----------------------------------------------------------------------------------------------------------------------------------

가장 먼저 재귀를 통해 세그먼트 트리를 만드는 함수를 살펴보자!

1
2
3
4
5
6
7
8
9
10
ll segment(int left, int right, int node) {
    if (left == right)
        return seg[node] = arr[left];
 
    int mid = (left + right) / 2;
    seg[node] += segment(left, mid, node * 2);
    seg[node] += segment(mid + 1, right, node * 2 + 1);
 
    return seg[node];
}
 

ll 은 long long 을 의미한다.

left , right 는 현재 노드번호(node = 위에서 말한 배열인덱스)가 가지고 있는 정보이다

첫 호출 때는 segment(1,N,1) 으로 호출했다고 볼 수 있다.

 

left == right 를 만족하게 되면 node번쨰 인덱스의 배열에는 세그먼트트리의 리프노드 값인 실제 값이 들어갈 공간!

그게 아니라면 (left+right)/2 를 통해 재귀를 들어가주면서 += 를 해줌으로써 자식에 대한 총합을 가지게 된다!

 

이런식으로 모든 범위에 대한 세그먼트 트리를 만들 수 있다.

 

-------------------------------------------------------------------------------------------------------------------------------

원하는 위치의 값을 다른 값으로 바꾸는 update에 대해서 알아보자

** update 는 세그먼트 트리의 리프노드인 원하는 위치의 값을 바꿈과 동시에

리프노드와 관련된 모든 노드의 정보를 바꾸는 작업이다

 

지금 당장 원하는 위치의 값이 세그먼트 트리의 몆번째 노드에 있는지 알고 있다면

/2를 해가면서 리프노드부터 루트노드까지 올라가면서 값을 변경해주면 된다.

하지만 보통? 문제들은 세그먼트트리의 몆번째 노드에 원하는 값이 있는지 알 수 없다.

 

그러므로 루트노드부터 쭉 내려가면서 원하는 위치의 리프노드를 찾아가면서 값을 바꿔주면 된다

 

1
2
3
4
5
6
7
8
9
10
11
12
13
void update(int node, int left, int right, int change, int val) {
 
    if (!(left <= change && change <= right)) return;
 
    seg[node] += (val - arr[change]);
 
    if (left != right) {
        int mid = (left + right) / 2;
        update(node * 2, left, mid, change, val);
        update(node * 2 + 1, mid + 1, right, change, val);
    }
 
}
 

인자를 순서대로 살펴보면

node => 현재 노드번호(배열인덱스)

left, right => 현재 노드가 가지고 있는 범위

change => 바꾸고싶은 리프노드 위치

val => 바꿀 값

 

첫번째 if문을 살펴보자

 

나는 현재 3번째 값을 5로 바꾸고 싶다고 가정했을 때,

현재 탐색 중인 노드의 범위가 3번째위치를 포함하지 않는다면 빠르게 손절한다는 의미이다

[1,5] 의 범위에는 3번째위치가 포함되지만

[1,2], [4,5] 의 범위에는 3번째위치가 포함되지 않는다.

결국 left <= change && change <= right 를 만족하면 현재 세그먼트에 대한 정보를 변경해도 되는 것!

 

두번째 if문의 의미는 현재 탐색 중인 노드가 리프노드인지 아닌지를 확인하는 것이다

left == right이면 리프노드를 의미하게 되니 더이상 update를 수행할 필요가 없는 것이고,

left != right이면 자식노드에 대한 정보를 update 해줘야한다!

 

-----------------------------------------------------------------------------------------------------------------------------------

 

구간 합을 구하는 방법을 살펴보자

다음 예시를 빠르게 이해한다면 코드 역시 빠르게 이해할 수 있다.

 

리프노드가 아닌 현재 노드는 리프노드들의 정보를 가지고 있다고 했었다.

현재 나는 구간[2,5] 에 대한 정보를 알고싶다.

[1,5] 의 범위는 필요한 정보를 가지고 있는가?

정답은 x 이다. 1에 대한 정보를 가지고 있기 때문 => 불필요한 정보!

[4,5] 의 범위는 필요한 정보를 가지고 있는가?

정답은 o 이다. [2,5] 중에서 [4,5] 에 대한 정보를 가지고 있기 때문이다.

** 불필요한 정보는 버리고 필요한 범위만 가져오는 것이 세그먼트 트리의 구간을 이용하는 기본개념!

 

** 현재 범위를 left, right 내가 알고 싶은 범위를 s, e 라고 한다면

s 가 left 보다 작거나 같고, e 가 right 보다 크거나 같다면

현재 확인 중인 노드는 알고싶은 [s,e] 에 대한 정보를 가지고 있으니 더이상 탐색할 필요가 없게 된다.

 

** ex) [2,5] 의 정보를 알고자 하는데 현재 노드가 [4,5]를 가지고 있다면

2<=4 && 5 <= 5 이면 모든 정보를 가지고 있음을 의미함 **

 

1
2
3
4
5
6
7
ll sum(int node, int left, int right, int s, int e) {
    if (left > e || right < s) return 0;
    if (s <= left && e >= right) return seg[node];
 
    int mid = (left + right) / 2;
    return sum(node * 2, left, mid, s, e) + sum(node * 2 + 1, mid + 1, right, s, e);
}
 

** 첫번째 if 문은 현재 알고 싶은 범위가 노드가 가진 정보와 관련이 없다면 탐색종료 한다는 의미이다.

** 두번째 if 문은 현재 노드가 가진 범위가 내가 알고싶은 범위에 대한 정보를 모두 포함한다는 의미이다.

 

** 그렇지 않다면 재귀를 통해 탐색해주면 된다.

'algorithm > algorithm' 카테고리의 다른 글

볼록껍질 선분 처리하기  (0) 2020.02.28
유클리드 호제법  (0) 2020.02.28
선분 교차 판별하기  (0) 2020.02.02
삼분 탐색(Ternary search)  (0) 2020.01.16
LCS  (0) 2019.07.04

+ Recent posts