알고리즘/HLD(Heavy-Light Decomposition)

BOJ 백준 16453 Linhas de Metrô

kyjdummy 2025. 4. 28. 20:26

 

문제 : https://www.acmicpc.net/problem/16453

[ 문제 요약 ]

  • 노드의 개수와 질의 개수가 주어집니다
  • 노드 개수 - 1개의 간선이 있는 트리 정보가 주어지며, 트리 정보는 두 노드 번호로 주어집니다.
  • 질의는 a, b, c, d로 되어있으며, a-b 구간과 c-d 구간의 겹치는 노드의 개수를 출력하는 문제입니다.

 

[ 테스트 케이스 설명 ]

10 4	// 노드 개수(5<=100,000), 질의 수(1<=20,000)
1 4	// 이어진 두 노드 번호
4 5
3 4
3 2
7 3
6 7
7 8
10 8
8 9
6 10 2 5	// (A,B), (C,D) 노드가 주어짐
1 9 5 10
9 10 2 1
5 10 2 9
//각 질의마다 두 노드가 서로 공유하는 간선을 출력
0
4
0
3

 

[ 문제 해설 ]

 

이 문제는 노드 수가 10만이고, 질의가 2만이기 때문에 완전 탐색으로는 풀 수 없습니다.

 

노드에서 노드까지 경로를 한 번에 찾아야 하고, 추가로 해당 노드가 겹치는지까지 알아야 하기 때문에 느리게 갱신되는 세그먼트 트리와 HLD 알고리즘을 사용해야 합니다.

 

트리상에서 HLD는 두 노드 사이의 경로를 정확히 한 번에 찾아갈 수 있도록 하고,

 

느리게 갱신되는 세그먼트 트리는, 구간 업데이트를 할 때 사용됩니다. 구간 업데이트가 필요한 이유는 아래와 같습니다.

 

A-B 노드로 갈 때 해당 구간에 있는 노드들에 모두 1을 더합니다.

 

그리고 C-B 노드를 탐색할 때 그 사이 경로에 있는 노드들 값의 합을 구간 합 쿼리로 구해옵니다.

 

그러면 겹치는 노드가 뭔지 알 수 있게 됩니다.

 

마지막으로 1로 업데이트했던 A-B 구간의 노드 값들을 다시 -1을 더 해주어 모든 노드 값을 0으로 복구시켜줍니다.

 

HLD 알고리즘에 대략적인 설명은 아래 문제에서 참고 부탁드립니다.

https://kyjdummy.tistory.com/21

 

디테일은 코드를 통해 확인 가능합니다.

 

[ 정답 코드 ]

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.StringTokenizer;
class Main{
	
	static int N, Q;
	static int idx;
	static int [] treeIdx;
	static int [] tree;
	static int [] lazy;
	static int [] chainHead;
	static int [] chainLevel;
	static int [] chainParent;
	static ArrayList<Integer>[] adNode;
	
	public static void main(String[] args)throws Exception{
		BufferedReader	br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = new StringTokenizer(br.readLine());
		StringBuilder	sb = new StringBuilder();
		N		= Integer.parseInt(st.nextToken());// 노드 개수(5<=100,000)
		Q		= Integer.parseInt(st.nextToken());// 질의 수(1<=20,000)
		treeIdx		= new int[N + 1];
		tree		= new int[N * 4];
		lazy		= new int[N * 4];
		chainHead	= new int[N + 1];
		chainLevel	= new int[N + 1];
		chainParent = new int[N + 1];
		adNode		= new ArrayList[N + 1];
		
		for(int i=0; i<=N; i++)
			adNode[i] = new ArrayList<>();
		
		for(int i=1; i<N; i++)
		{
			st = new StringTokenizer(br.readLine());
			int a = Integer.parseInt(st.nextToken());
			int b = Integer.parseInt(st.nextToken());
			adNode[a].add(b);
			adNode[b].add(a);
		}
		// 해당 함수에서 자식 노드가 큰것을 인접리스트에 가장 앞으로 옮김
		setSize(1, 0, new int[N + 1]);
		
		chainHead[1] = 1;// 1번 노드의 헤드는 자기자신
		// chain정보 입력
		setHLD(1, 0, 1);
		
		while(Q-->0)
		{
			st = new StringTokenizer(br.readLine());
			// (A,B), (C,D) 노드가 주어짐
			int a = Integer.parseInt(st.nextToken());
			int b = Integer.parseInt(st.nextToken());
			int c = Integer.parseInt(st.nextToken());
			int d = Integer.parseInt(st.nextToken());
			
			rangeUpdate(a, b, 1);	// a-b구간을 탐색하며 해당 구간에 1을 업데이트
			
			int ans = getAns(c, d);	// c-d구간을 탐색하며 구간 sum을 구해서 1인 노드들의 합을 구해옴
			
			rangeUpdate(a, b, -1);	// a-b구간을 탐색하며 해당 구간에 -1로 업데이트
			
			sb.append(ans).append('\n');
		}
		System.out.print(sb);
	}
	static int getAns(int n1, int n2) {
		int ans = 0;
		// chainHead가 같아질 때 까지 레벨을 낮춤
		while(chainHead[n1] != chainHead[n2])
		{
			if(chainLevel[n1] > chainLevel[n2])
			{
				int tmp = n1;
				n1 = n2;
				n2 = tmp;
			}
			ans += query(1, 1, N, treeIdx[chainHead[n2]], treeIdx[n2]);
			n2 = chainParent[n2];
		}
		// 같은 헤드일 때 마지막 연산
		if(treeIdx[n1] > treeIdx[n2])
		{
			int tmp = n1;
			n1 = n2;
			n2 = tmp;
		}
		ans += query(1, 1, N, treeIdx[n1], treeIdx[n2]);
		
		return ans;
	}
	static void rangeUpdate(int n1, int n2, int value) {
		// chainHead가 같아질 때 까지 레벨을 낮춤
		while(chainHead[n1] != chainHead[n2])
		{
			if(chainLevel[n1] > chainLevel[n2])
			{
				int tmp = n1;
				n1 = n2;
				n2 = tmp;
			}
			update(1, 1, N, treeIdx[chainHead[n2]], treeIdx[n2], value);
			n2 = chainParent[n2];
		}
		// 같은 헤드일 때 마지막 연산
		if(treeIdx[n1] > treeIdx[n2]) {
			int tmp = n1;
			n1 = n2;
			n2 = tmp;
		}
		update(1, 1, N, treeIdx[n1], treeIdx[n2], value);
	}
	static void update(int treeNode, int s, int e, int left, int right, int value) {
		propagate(treeNode, s, e);
		if(e < left || right < s)
			return;
		if(left<=s && e<= right)
		{
			lazy[treeNode] = value;
			propagate(treeNode, s, e);
			return;
		}
		int mid = (s + e) >> 1;
		
		update(treeNode << 1, s, mid, left, right, value);
		update(treeNode << 1 | 1, mid + 1, e, left, right, value);
		
		tree[treeNode] = tree[treeNode << 1] + tree[treeNode << 1 | 1];
	}
	static int query(int treeNode, int s, int e, int left, int right) {
		propagate(treeNode, s, e);
		if(e < left || right < s)
			return 0;
		if(left<=s && e<=right)
			return tree[treeNode];
		
		int mid = (s + e) >> 1;
		
		int l = query(treeNode << 1, s, mid, left, right);
		int r = query(treeNode << 1 | 1, mid + 1, e, left, right);
		
		return l + r;
	}
	static void propagate(int treeNode, int s, int e) {
		if(lazy[treeNode] != 0)
		{
			tree[treeNode] += (e - s + 1) * lazy[treeNode];
			if(s != e)
			{
				lazy[treeNode << 1]		+= lazy[treeNode];
				lazy[treeNode << 1 | 1] += lazy[treeNode];
			}
			lazy[treeNode] = 0;
		}
	}
	static void setSize(int nowNode, int parentNode, int[] size) {
		int heavySize = 0;
		int heavyIdx = 0;
		size[nowNode] = 1;
		for(int i=0; i<adNode[nowNode].size(); i++)
		{
			int next = adNode[nowNode].get(i);
			
			if(next == parentNode)
				continue;
			
			setSize(next, nowNode, size);
			
			size[nowNode] += size[next];
			if(heavySize < size[next])
			{
				heavySize = size[next];
				heavyIdx = i;
			}
		}
		if(adNode[nowNode].size() > 0)
			Collections.swap(adNode[nowNode], 0, heavyIdx);
	}
	static void setHLD(int nowNode, int parentNode, int level) {
		treeIdx[nowNode] = ++idx;
		chainLevel[nowNode] = level;
		
		for(int i=0; i<adNode[nowNode].size(); i++)
		{
			int next = adNode[nowNode].get(i);
			
			if(next == parentNode)
				continue;
			
			if(i == 0)	// heavy 노드일 경우 체인 유지
			{
				chainHead[next] = chainHead[nowNode];
				chainParent[next] = chainParent[nowNode];
				setHLD(next, nowNode, level);
			}
			else		// light일 경우 새 체인 생성
			{
				chainHead[next] = next;		// 체인의 헤드는 자기자신
				chainParent[next] = nowNode;// 이전 체인으로 바로 점프를 위한 이전 노드 값 삽입
				setHLD(next, nowNode, level + 1);// 새 체인시 레벨을 올린다.
			}
		}
	}
}

 

'알고리즘 > HLD(Heavy-Light Decomposition)' 카테고리의 다른 글

BOJ 백준 13038 Tree  (0) 2025.04.29
BOJ 백준 30092 슥삭슥삭 나무자르기  (1) 2025.04.28
BOJ 백준 31234 대역폭 관리  (0) 2025.04.27
BOJ 백준 17033 Cow Land  (1) 2025.04.26
BOJ 백준 13309 트리  (0) 2025.04.26