PrerequisitePermalink

  • Segment Tree

Dynamic Segment TreePermalink

N1,000,000N \le 1,000,000 정도일 때, 구간합을 구하거나 특정 자리의 값을 업데이트 한다고 하자.

그럼 세그먼트 트리를 써서 구현을 하는것이 국룰이다.

그런데, N1,000,000,000N\le 1,000,000,000 이라고 해보자. 그럴때도 쓸 수 있을까?

바로 다이나믹 세그먼트 트리를 이용하면 된다.

구간이 넓어봤자 업데이트하거나 구간합을 구하는 쿼리 QQ는 문제에서 그렇게 크게 주어지지 않을 것이기 때문에 충분히 구할 수 있다.

결론부터 말하자면 다이나믹 세그먼트 트리도 일반 세그먼트 트리와 동일한 공간복잡도와 시간복잡도를 갖는다.

물론 구간이 증가할수록 트리의 depth는 깊어지기 때문에 더 메모리가 많이 들고, 느리고, 어떤 구현 방식을 사용하느냐에 따라도 조금씩 달라서 메모리가 넉넉할 때만 사용할 수 있다.

원리Permalink

일반적인 세그먼트 트리는 [1,N][1,N] 의 구간에 대해서 정적으로 배열을 선언해두고, 보통 11번 노드를 루트로 하고 대략 노드가 2N2N 개가 있다.

그런데, 다이나믹 세그먼트 트리는 이렇게 동작하지 않는다.

노드를 그때 그때 필요할 때 마다 만들어내서 관리를 한다.

다이나믹 세그먼트 트리도 일단 관리하려는 구간에 대해서 정해두긴 해야한다.

구현 1 - 포인터 이용Permalink

포인터를 이용해서 구현하는건 직관적이라는 장점이 있다.

다음과 같은 코드를 보자.

struct node {  
   int value = 0;  
   node *l = 0, *r = 0;  
};  
  
struct dynamic_seg_tree {  
   node *root = new node();  
   void update(node *n, int nl, int nr, int i, int v) {  
      if (i < nl || i > nr) return;  
      if (nl == nr) {  
         n->value = v;  
         return;  
      }  
      int m = nl + (nr - nl) / 2;  
      if (i <= m) {  
         if (!n->l) n->l = new node();  
         update(n->l, nl, m, i, v);  
      } else {  
         if (!n->r) n->r = new node();  
         update(n->r, m + 1, nr, i, v);  
      }  
      n->value = (n->l ? n->l->value : 0) + (n->r ? n->r->value : 0);  
   }  
   int query(node *n, int nl, int nr, int l, int r) {  
      if (!n || nr < l || nl > r) return 0;  
      if (nl >= l && nr <= r) return n->value;  
      int m = nl + (nr - nl) / 2;  
      return query(n->l, nl, m, l, r) + query(n->r, m + 1, nr, l, r);  
   }  
};
  • node 라는 구조체를 정의해 구간의 합을 의미하는 value와 각각 왼쪽 오른쪽 자식 포인터를 갖게 한다.
  • update에서는 일반 세그먼트 트리와 유사한데, i 가 실제로 가 있어야 할 구간쪽에만 자식이 없다면 할당해주는 식으로 파고들어가서 최대한 메모리를 절약한다.
  • query에서는 현재 노드가 정의되지 않은 구간이라면 이 구간에는 업데이트가 된 적이 없다는 뜻이므로 바로 00을 반환할 수 있다.

사용하기Permalink

BOJ 2042 - 구간 합 구하기 문제를 패보도록 하자.

우린 [0,][0,\,\infty] 구간을 관리하면서 모든 인덱스에 55를 곱해서 쿼리를 날리는 정상적이지 않은 짓(?)을 할 것이다.

바로 다음과 같이 코드를 짜도 문제를 풀 수 있다!

const int MAX = numeric_limits<long long>::max();  
void solve() {  
   int n, m, k;  
   cin >> n >> m >> k;  
   dynamic_seg_tree seg;  
   for (int i = 0; i < n; i++) {  
      int v;  
      cin >> v;  
      seg.update(seg.root, 0, MAX, i * 5, v);  
   }  
   for (int _ = 0; _ < m + k; _++) {  
      int a, b, c;  
      cin >> a >> b >> c;  
      if (a == 1) {  
         b--;  
         seg.update(seg.root, 0, MAX, b * 5, c);  
      } else {  
         b--, c--;  
         cout << seg.query(seg.root, 0, MAX, b * 5, c * 5) << endl;  
      }  
   }  
}

구현 2 - 배열 이용Permalink

배열을 이용해서 짜는 것도 구조는 매우 비슷한데, NULL 포인터 대신 1-1같은 값을 가리키게 해두는 것이다.

처음에 배열의 크기를 예상해서 미리 할당해두거나, push_back 으로 크기를 하나하나 늘려줄 수 있다.

웬만하면 미리 할당해두는 방식이 좀 더 빠르다.

실제 코드를 보도록 하자.

struct node {  
   int value = 0, l = -1, r = -1;  
};  
  
struct dynamic_seg_tree {  
   int nxt = 2;  
   vector<node> tree = vector<node>(3000000);  
   void update(int n, int nl, int nr, int i, int v) {  
      if (i < nl || i > nr) return;  
      if (nl == nr) {  
         tree[n].value = v;  
         return;  
      }  
      int m = nl + (nr - nl) / 2;  
      if (i <= m) {  
         if (tree[n].l == -1) tree[n].l = nxt++;  
         update(tree[n].l, nl, m, i, v);  
      } else {  
         if (tree[n].r == -1) tree[n].r = nxt++;  
         update(tree[n].r, m + 1, nr, i, v);  
      }  
      tree[n].value =  
         (~tree[n].l ? tree[tree[n].l].value : 0) +  
            (~tree[n].r ? tree[tree[n].r].value : 0);  
   }  
   int query(int n, int nl, int nr, int l, int r) {  
      if (n == -1 || nr < l || nl > r) return 0;  
      if (nl >= l && nr <= r) return tree[n].value;  
      int m = nl + (nr - nl) / 2;  
      return query(tree[n].l, nl, m, l, r) + query(tree[n].r, m + 1, nr, l, r);  
   }  
};

연습 문제Permalink

BOJ 20212 - 나무는 쿼리를 싫어해~

이 문제는 다이나믹 세그먼트 트리에 Lazy Propagation을 끼얹는 문제이다. 연습용으로 풀어보면 좋다. 구현이 꽤나 어려울 것이다.

Comments