트리 체인 분할을 이용한 알고리즘 구현 및 응용

개요

트리 체인 분할은 트리를 여러 체인으로 나누어 선형 자료구조를 활용할 수 있도록 하는 기법이다. 이 중 가장 널리 사용되는 방식은 Heavy-Light Decomposition(HLD)이다.

다음과 같은 개념들을 정의한다:

  • 무거운 자식: 특정 노드의 모든 자식 중 서브트리 크기가 가장 큰 자식
  • 가벼운 자식: 무거운 자식을 제외한 나머지 자식들
  • 무거운 간선: 부모와 무거운 자식을 연결하는 간선
  • 가벼운 간선: 부모와 가벼운 자식을 연결하는 간선

구현 방법

HLD는 두 번의 DFS를 통해 구현된다. 첫 번째 DFS에서는 각 노드의 깊이, 부모, 서브트리 크기, 무거운 자식을 계산한다. 두 번째 DFS에서는 각 체인의 시작점(헤드)를 설정한다.

void preprocess(int node, int parent, int level) {
    depth[node] = level;
    father[node] = parent;
    subtree_size[node] = 1;
    int max_child_size = 0;
    
    for(int child : adjacency[node]) {
        if(child != parent) {
            preprocess(child, node, level + 1);
            subtree_size[node] += subtree_size[child];
            
            if(subtree_size[child] > max_child_size) {
                heavy_child[node] = child;
                max_child_size = subtree_size[child];
            }
        }
    }
}

void decompose(int node, int chain_head) {
    chain_top[node] = chain_head;
    position_in_array[node] = ++timer;
    
    if(heavy_child[node] != -1) {
        decompose(heavy_child[node], chain_head);
    }
    
    for(int child : adjacency[node]) {
        if(child != father[node] && child != heavy_child[node]) {
            decompose(child, child);
        }
    }
}

특성

HLD의 핵심 특성은 임의의 노드에서 루트까지 경로상에 있는 가벼운 간선의 개수가 O(log n)이라는 것이다. 이를 통해 트리 상의 경로 쿼리를 효율적으로 처리할 수 있다.

LCA 구하기

HLD를 이용해 두 노드의 최소 공통 조상(LCA)을 구할 수 있다. 두 노드가 서로 다른 체인에 속해 있다면, 더 깊은 체인의 헤드를 위로 올려 같은 체인에 도달할 때까지 반복한다.

int find_lca(int u, int v) {
    while(chain_top[u] != chain_top[v]) {
        if(depth[chain_top[u]] > depth[chain_top[v]]) {
            u = father[chain_top[u]];
        } else {
            v = father[chain_top[v]];
        }
    }
    return (depth[u] < depth[v]) ? u : v;
}

세그먼트 트리와의 결합

HLD와 세그먼트 트리를 결합하면 트리 상의 경로 쿼리와 업데이트를 효율적으로 처리할 수 있다. 노드들의 값을 배열에 매핑하고, 해당 배열을 기반으로 세그먼트 트리를 구성한다.

// 경로 업데이트 함수
void update_path(int x, int y, int value) {
    while(chain_top[x] != chain_top[y]) {
        if(depth[chain_top[x]] > depth[chain_top[y]]) {
            segment_update(1, n, position_in_array[chain_top[x]], position_in_array[x], 1, value);
            x = father[chain_top[x]];
        } else {
            segment_update(1, n, position_in_array[chain_top[y]], position_in_array[y], 1, value);
            y = father[chain_top[y]];
        }
    }
    
    if(depth[x] > depth[y]) {
        segment_update(1, n, position_in_array[y], position_in_array[x], 1, value);
    } else {
        segment_update(1, n, position_in_array[x], position_in_array[y], 1, value);
    }
}

// 서브트리 업데이트 함수
void update_subtree(int node, int value) {
    segment_update(1, n, position_in_array[node], position_in_array[node] + subtree_size[node] - 1, 1, value);
}

간선 가중치 처리

트리의 간선에 가중치가 주어지는 경우, 간선 가중치를 점 가중치로 변환하여 처리할 수 있다. 각 노드의 가중치를 부모와의 간선 가중치로 정의하면 된다.

void convert_edge_weights() {
    for(int i = 2; i <= n; i++) {
        node_weight[i] = edge_weight[parent[i]][i];
    }
}

경로 쿼리를 수행할 때는 두 노드가 같은 체인에 속해 있을 경우, 깊이가 더 낮은 노드의 다음 위치부터 시작해야 한다.

태그: tree-decomposition heavy-light-decomposition segment-tree LCA graph-algorithms

7월 14일 21:25에 게시됨