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

[ 문제 요약 ]

  • 트리가 주어지고, 아래 6가지 명령어를 실행합니다.
  • 1 X V : 금고 X의 서브 트리에 있는 모든 금고에 V 원을 더합니다. (1 ≤ X ≤ N, 1 ≤ V ≤ 10^9)
  • 2 X Y V : 금고 X부터 금고 Y까지의 경로에 있는 모든 금고에 V 원을 더합니다. 단, 경로는 경계를 포함합니다. (1 ≤ X, Y ≤ N, 1 ≤ V ≤ 10^9)
  • 3 X V : 금고 X의 서브 트리에 있는 모든 금고의 돈을 V 배합니다. (1 ≤ X ≤ N, 0 ≤ V ≤ 10^9)
  • 4 X Y V : 금고 X부터 금고 Y까지의 경로에 있는 모든 금고의 돈을 V 배합니다. 단, 경로는 경계를 포함합니다. (1 ≤ X, Y ≤ N, 0 ≤ V ≤ 10^9)
  • 5 X : 금고 X의 서브 트리에 있는 모든 금고의 돈을 합한 값을 출력합니다. (1 ≤ X ≤ N)
  • 6 X Y : 금고 X부터 금고 Y까지의 경로에 있는 모든 금고의 돈을 합한 값을 출력합니다. 단, 경로는 경계를 포함합니다. (1 ≤ X, Y ≤ N)

 

[ 테스트 케이스 설명 ]

5 10		// 노드 수(1<=500,000), 쿠리 수(1<=100,000)
2 4		// N-1개 줄에 간선 정보
4 3
5 4
2 1
3 1 82		// 3 X V : 금고 X의 서브트리에 있는 모든 금고의 돈을 V배(V=10^9)
6 3 5		// 6 X Y : 금고 X부터 금고 Y까지 경로에 있는 모든 금고의 돈을 합하여 출력
2 2 5 45	// 2 X Y V : 금고 X부터 Y까지 경로의 모든 금고에 V를 더함
2 3 2 70
6 3 5
5 3		// 5 X : 금고 X의 서브트리에 있는 모든 금고의 돈을 합한 값을 출력
4 2 1 47
1 1 95		// 1 X V : 금고 X의 서브트리에 있는 모든 금고에 V원을 더함
6 3 2
4 5 1 38	// 4 X Y V : 금고 X부터 금고 Y까지 경로에 있는 모든 금고의 돈을 V배함
// 답( 2^32로 나눈 나머지 출력 )
0
230
70
5875

 

 

[ 알고리즘 분류 ]

  • 자료 구조, 트리, 느리게 갱신되는 세그먼트 트리
  • 오일러 경로 테크닉, heavy-light 분할

 

[ 문제 해설 ]

 

이 문제는 여러 가지 알고리즘을 복합적으로 구현 해야 제한 시간 내에 통과하는 코드를 만들 수 있습니다.

 

사용되는 알고리즘은 아래와 같습니다.

 

[ HDL + 오일러 경로 테크닉 + 느리게 갱신되는 세그먼트 트리 ]

 

먼저 HLD와 오일러 경로 테크닉 알고리즘을 이용해 트리 정보를 세그먼트 트리 인덱스에 대응되도록 할 수 있습니다.

 

그리고 특정 노드에 값 V를 더하거나, 곱하는 것은 느리게 갱신되는 세그먼트 트리를 이용합니다.

 

곱하고 더하는 것을 lazy로 전파하는 것에 대해 공식이 있는데 이에 관한 문제 해설이 아래 13925문제에 되어있으니 참고하셔도 됩니다. lazy 배열의 더할 숫자와 곱할 숫자를 따로 관리하는 것이 핵심입니다.

https://kyjdummy.tistory.com/15

 

BOJ 백준 13925 수열과 쿼리 13

문제 : https://www.acmicpc.net/problem/13925 [ 문제 요약 ]배열의 초깃값이 주어지고, 명령어가 주어졌을 때, 명령어에 따라 배열의 값을 수정하는 것입니다.명령어 코드가 1일 때는 x, y 범위의 모든 값에

kyjdummy.tistory.com

 

문제에서 출력은 2^32로 나눈 나머지를 출력하라고 해서 무턱대고 MOD를 2^32로 잡고 나머지 연산을 하면 틀립니다.

 

그 이유는 컴퓨터 자체가 32비트를 쓴다고 했기 때문에, 32비트 이상은 버려야 합니다.

 

이를 표현하기 위해 모든 결괏값에 (1<<32) - 1한 값을 AND 비트 연산(&) 해야 그 이상은 모두 버려지게 되어 AC입니다.

 

이 부분을 지키지 않으면 틀립니다.

 

코드에서 세그먼트 트리 부분만 볼 수 있게 클래스로 분리했습니다.

 

[ 정답 코드 ]

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.StringTokenizer;

class Main{
	
	static final long MOD = (1L<<32) - 1;
	static int N, Q;
	static int idx;			// 노드번호 -> 트리번호로 바꿀 순차증가 인덱스
	static int [] treeIdx;		// 노드번호 -> 트리번호 정보 저장할 배열
	static int [] chainHead;	// 각 체인의 head번호
	static int [] chainLevel;	// 각 체인의 level
	static int [] chainParent;	// 이전 체인으로 바로 점프할 수 있도록 노드마다 이전 체인의 정보를 담을 배열
	static int [][] range;		// 각 노드의 서브트리 범위를 알기 위한 배열( 오일러 경로 테크닉 )
	static SegmentTree seg;		// 세그먼트 트리 연산을 진행할 세그먼트 트리 클래스
	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());
		Q		= Integer.parseInt(st.nextToken());
		treeIdx		= new int[N + 1];
		chainHead	= new int[N + 1];
		chainLevel	= new int[N + 1];
		chainParent = new int[N + 1];
		range		= new int[N + 1][2];
		adNode		= new ArrayList[N + 1];
		seg		= new SegmentTree(N, MOD);
		
		for(int i=0; i<=N; i++)
			adNode[i] = new ArrayList<>();
		
		for(int i=1; i<N; i++)
		{
			st = new StringTokenizer(br.readLine());
			int n1 = Integer.parseInt(st.nextToken());
			int n2 = Integer.parseInt(st.nextToken());
			adNode[n1].add(n2);
			adNode[n2].add(n1);
		}
		// setSize : 이 함수에서 heavy한 자식노드를 인접 노드의 가장 앞쪽으로 옮긴다.
		setSize(1, 0, new int[N + 1]);
		
		chainParent[1] = 1;
		chainHead[1] = 1;	// 1번 노드의 헤드는 자기 자신
		// setHLD : 이 함수에서 HLD를 위한 chain정보를 입력하며 동시에 노드당 서브트리의 범위 + 노드 -> 세그먼트 트리 인덱스 번호를 저장
		setHLD(1, 0, 1);
		
		int op, x, y, v;
		
		while(Q-->0)
		{
			st = new StringTokenizer(br.readLine());
			op = Integer.parseInt(st.nextToken());
			switch(op)
			{
			case 1: // 1 X V : 금고 X의 서브트리에 있는 모든 금고에 V원을 더함
				x = Integer.parseInt(st.nextToken());
				v = Integer.parseInt(st.nextToken());
				seg.update(1,  1,  N, range[x][0], range[x][1], v, true);
				break;
			case 2: // 2 X Y V : 금고 X부터 Y까지 경로의 모든 금고에 V를 더함
				x = Integer.parseInt(st.nextToken());
				y = Integer.parseInt(st.nextToken());
				v = Integer.parseInt(st.nextToken());
				updatePath(x, y, v, true);
				break;
			case 3: // 3 X V : 금고 X의 서브트리에 있는 모든 금고의 돈을 V배(V=10^9)
				x = Integer.parseInt(st.nextToken());
				v = Integer.parseInt(st.nextToken());
				seg.update(1,  1,  N, range[x][0], range[x][1], v, false);
				break;
			case 4: // 4 X Y V : 금고 X부터 금고 Y까지 경로에 있는 모든 금고의 돈을 V배함
				x = Integer.parseInt(st.nextToken());
				y = Integer.parseInt(st.nextToken());
				v = Integer.parseInt(st.nextToken());
				updatePath(x, y, v, false);
				break;
			case 5: // 5 X : 금고 X의 서브트리에 있는 모든 금고의 돈을 합한 값을 출력
				x = Integer.parseInt(st.nextToken());
				sb.append( seg.query(1, 1, N, range[x][0], range[x][1])).append('\n');
				break;
			case 6: // 6 X Y : 금고 X부터 금고 Y까지 경로에 있는 모든 금고의 돈을 합하여 출력
				x = Integer.parseInt(st.nextToken());
				y = Integer.parseInt(st.nextToken());
				sb.append( queryPath(x, y) ).append('\n');
				break;
			}
		}
		
		System.out.print(sb);
	}
	public static void updatePath(int x, int y, int v, boolean isPlus)
	{
		while(chainHead[x] != chainHead[y])
		{
			if(chainLevel[x] > chainLevel[y])
			{
				int tmp = x;
				x = y;
				y = tmp;
			}
			
			seg.update(1,  1,  N, treeIdx[chainHead[y]], treeIdx[y], v, isPlus);
			
			y = chainParent[y];
		}
		
		if(treeIdx[x] > treeIdx[y])
		{
			int tmp = x;
			x = y;
			y = tmp;
		}
		
		seg.update(1,  1,  N, treeIdx[x], treeIdx[y], v, isPlus);
	}
	public static long queryPath(int x, int y)
	{
		long sum = 0;
		// 금고 X부터 금고 Y까지 경로에 있는 모든 금고의 돈을 합하여 출력
		while(chainHead[x] != chainHead[y])
		{
			if(chainLevel[x] > chainLevel[y])
			{
				int tmp = x;
				x = y;
				y = tmp;
			}
			
			sum += seg.query(1, 1, N, treeIdx[chainHead[y]], treeIdx[y]);
			sum &= MOD;
			
			y = chainParent[y];
		}
		if(treeIdx[x] > treeIdx[y])
		{
			int tmp = x;
			x = y;
			y = tmp;
		}
		
		sum += seg.query(1, 1, N, treeIdx[x], treeIdx[y]);
		
		return sum & MOD;
	}
	public static void setHLD(int nowNode, int parentNode, int level) {
		++idx;
		chainLevel[nowNode] = level;
		treeIdx[nowNode]	= idx;	// 노드번호 -> 세그먼트 트리 인덱스
		range[nowNode][0]	= idx;	// 서브 트리의 시작 범위
		
		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;			// 체인의 head는 자기자신
				chainParent[next] = nowNode;	// 이전 체인으로 바로 점프할 수 있게 이전 체인의 특정 노드 입력
				setHLD(next, nowNode, level + 1);// 새 체인 시작시 레벨 증가
			}
		}
		range[nowNode][1] = idx;	// 서브 트리의 마지막 인덱스 
	}
	public 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);
	}
}


class SegmentTree{
	long MOD;
	long N;
	long [] tree;
	Node [] lazy;
	SegmentTree(int n, long MOD){
		this.N		= n;
		this.MOD	= MOD;
		this.tree	= new long[n * 4];
		this.lazy	= new Node[n * 4];
		
		for(int i=0; i<n*4; i++)
			lazy[i] = new Node(0, 1);
	}
	public void propagate(int treeNode, int s, int e) {
		if(lazy[treeNode].sum != 0 || lazy[treeNode].mul != 1)
		{
			tree[treeNode] *= lazy[treeNode].mul;
			tree[treeNode] &= MOD;
			tree[treeNode] += (e - s + 1) * lazy[treeNode].sum;
			tree[treeNode] &= MOD;
			
			if(s != e)
			{
				int left = treeNode << 1;
				int right= treeNode << 1 | 1;
				
				lazy[left].mul	= (lazy[left].mul	* lazy[treeNode].mul) & MOD;
				lazy[right].mul = (lazy[right].mul	* lazy[treeNode].mul) & MOD;
				
				lazy[left].sum	*= lazy[treeNode].mul;
				lazy[left].sum	&= MOD;
				lazy[left].sum	+= lazy[treeNode].sum;
				lazy[left].sum	&= MOD;
				
				lazy[right].sum	*= lazy[treeNode].mul;
				lazy[right].sum	&= MOD;
				lazy[right].sum	+= lazy[treeNode].sum;
				lazy[right].sum	&= MOD;
			}
			lazy[treeNode].sum = 0;
			lazy[treeNode].mul = 1;
		}
	}
	public void update(int treeNode, int s, int e, int left, int right, int value, boolean isPlus) {
		
		propagate(treeNode, s, e);
		
		if(e < left || right < s)
			return;
		if(left <= s && e <= right)
		{
			if(isPlus)
			{
				lazy[treeNode].sum = value;
				lazy[treeNode].mul = 1;
			}
			else
			{
				lazy[treeNode].sum = 0;
				lazy[treeNode].mul = value;
			}
			propagate(treeNode, s, e);
			return;
		}
		
		int mid = (s + e) >> 1;
		
		update(treeNode << 1, s, mid, left, right, value, isPlus);
		update(treeNode << 1 | 1, mid + 1, e, left, right, value, isPlus);
		
		tree[treeNode] = (tree[treeNode << 1] + tree[treeNode << 1 | 1]) & MOD;
	}
	public long 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;
		
		long l = query(treeNode << 1, s, mid, left, right);
		long r = query(treeNode << 1 | 1, mid + 1, e, left, right);
		
		return (l + r) & MOD;
	}
}
class Node{
	long sum, mul;
	Node(long s, long m){
		sum = s;
		mul = m;
	}
}​

 

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

 

[ 문제 요약 ]

  • 총 3가지 명령어가 주어집니다.
  • bridge A B : A-B 노드를 연결하는 명령어로 이미 연결되어 있으면 no 출력, 연결되어 있지 않다면 연결하고 yes 출력
  • penguins A X : A 노드의 값을 X로 바꾸는 것, 출력은 없음
  • excursion A B : A-B 노드를 포함한 노드들의 가치의 총합을 출력, 연결되어 있지 않다면 impossible 출력

 

[ 테스트 케이스 설명 ]

5			// 섬의 수 1<=30,000
4 2 4 5 6		// 각 섬의 펭귄수 0<=1,000
10			// 쿼리 수 1<=300,000
excursion 1 1		// bridge , excursion 일 때 출력(penguins A X - 섬 A에 살고있는 펭귄 수를 X로 교체 출력필요없음)
excursion 1 2		// A-B로 갈수 있는 경우 모든 펭귄의 수를 출력, 이동 불가시 impossible출력
bridge 1 2		// 두 노드를 연결 명령, 이전까지 지어진 다리를 이용해 이동할 수 없을 경우에만 다리를 지어야함, 지어야하면 yes, 아니면no출력
excursion 1 2
bridge 3 4
bridge 3 5
excursion 4 5
bridge 1 3
excursion 2 4
excursion 2 5

 

[ 알고리즘 분류 ]

  • 자료 구조, 트리, 세그먼트 트리
  • 분리 집합, 오프라인 쿼리, heavy-light 분할

 

[ 문제 해설 ]

다리를 연결하기 전에 두 노드의 연결 유무를 알아야 하기 때문에 오프라인 쿼리로 처리합니다.

 

오프라인 쿼리로 처리한다는 것은, 주어진 쿼리를 별도의 자료구조로 다 저장하고, 전처리 작업을 한 후, 다시 처음부터 쿼리를 처리한다는 의미로 보시면 됩니다.

 

전처리 작업은, 먼저 bridge 명령을 통해 인접 노드 정보를 미리 생성해 놓고, excursion 명령어에 대해 연결 가능 유무를 저장해 놓습니다.

 

예를 들어 bridge 1 2라는 명령어 전에 excursion 1 2가 나왔으면, 1 2 노드는 서로 연결되기 전이므로 이 당시 excursion 1 2라는 명령어는 결과적으로 impossible을 출력해야 합니다.

 

이런 조건을 알기 위해 유니언 파인인 드 알고리즘을 이용하고, excursion 1 2 라는 질의에 별도 변수를 생성해 연결 유무를 저장해 놓습니다.

 

그러면 추후 모든 질의를 순차적으로 다시 탐색할 때, 이 질의는 impossible을 바로 출력할 수 있습니다.

 

노드들의 가치의 합은 구간 합 세그먼트 트리를 이용하고, 트리로 주어진 노드 정보를 세그먼트 트리로 진행할 수 있도록 HLD 알고리즘을 사용합니다.

 

필요한 알고리즘은 [ 유니온 파인드 + 구간 합 세그먼트 트리 + HLD + 오프라인 쿼리 ]입니다.

 

HLD 관련 내용은 아래 문제에 잘 설명되어 있으니 참고하시면 도움이 됩니다.

https://kyjdummy.tistory.com/21

 

BOJ 백준 13510 트리와 쿼리 1

문제 : https://www.acmicpc.net/problem/13510[ 문제 요약 ]N 개의 노드와 N - 1개의 간선이 주어지며 2가지 명령어가 임의로 주어집니다.1 i c : i 번 간선의 비용을 c로 변경2 u v : u에서 v로 가는 단순 경로에

kyjdummy.tistory.com

 

이 문제에서 HLD 구현 시 중요한 점은, 주어지는 노드들이 모두 하나의 트리로 연결되어 있지 않고, 여러 개의 트리일 수 있습니다.

 

이럴 경우 HLD 세팅을 각 트리마다 해주어야 하기 때문에 모든 노드에 대해서 확인하며 HLD 세팅을 해야 합니다.

 

자세한 것은 코드 주석으로 대신합니다.

 

[ 정답 코드 ]

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 chainLevelValue;		// 각 체인당 레벨
	static int[] size;			// 각 체인의 사이즈
	static int[] arr;			// 초기 입력되는 노드의 기본값
	static int[] tree;			// 구간합 세그먼트 트리
	static int[] treeIdx;			// 노드 번호 -> 세그먼트 트리 인덱스 전환 값을 담을 배열
	static int[] chainHead;			// 각 체인의 head 값
	static int[] chainLevel;		// 각 체인의 level
	static int[] chainParent;		// 각 체인의 이전 체인으로 점프할 도착노드번호
	static Query[] query;			// 주어지는 질의 정보를 담을 배열
	static UnionFind uf;			// 유니온 파인드 알고리즘을 위한 배열
	static ArrayList<Integer>[] adNode;// 인접노드
	
	public static void main(String[] args)throws Exception{
		BufferedReader	br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st;
		N		= Integer.parseInt(br.readLine());// 섬의 수 1<=30,000
		size		= new int[N + 1];
		arr		= new int[N + 1];
		tree		= new int[N * 4];
		treeIdx		= new int[N + 1];
		chainHead	= new int[N + 1];
		chainLevel	= new int[N + 1];
		chainParent = new int[N + 1];
		uf		= new UnionFind(N);
		adNode		= new ArrayList[N + 1];
		
		st = new StringTokenizer(br.readLine());
		for(int i=1; i<=N; i++)
		{
			arr[i] = Integer.parseInt(st.nextToken());// 각 섬의 펭귄수 0<=1,000
			adNode[i] = new ArrayList<>();
		}

		Q = Integer.parseInt(br.readLine());// 쿼리 수 1<=300,000
		
		query = new Query[Q + 1];
		
		for(int idx=1; idx<=Q; idx++)
		{
			st = new StringTokenizer(br.readLine());
			
			int cmd = 0, n1 = 0, n2 = 0;
			boolean ok = false;
			
			String o = st.nextToken();				// 명령어
			n1 = Integer.parseInt(st.nextToken());
			n2 = Integer.parseInt(st.nextToken());
			if("excursion".equals(o))// A-B로 갈수 있는 경우 모든 펭귄의 수를 출력, 이동 불가시 impossible출력
			{
				cmd = 1;
				int p1 = uf.find(n1);	// 부모노드 가져옴
				int p2 = uf.find(n2);	// 부모노드 가져옴
				if(p1 == p2)			// 부모노드가 같으면 연결되어 있는 것
					ok = true;
			}
			else if("bridge".equals(o))// 두 노드를 연결 명령, 이전까지 지어진 다리를 이용해 이동할 수 없을 경우에만 다리를 지어야함, 지어야하면 yes, 아니면no출력
			{
				cmd = 2;
				// 연결 되어있지 않다면 연결 후 true반환, 연결 되어 있었다면 false
				boolean isConnect = uf.union(n1, n2);
				if(isConnect)
				{
					ok = true;			// brigde일 때, 1은 yes를 의미
					adNode[n1].add(n2);	// 인접 노드연결
					adNode[n2].add(n1);	// 인접 노드연결
				}
			}
			else// penguins A X - 섬 A에 살고있는 펭귄 수를 X로 교체 출력필요없음
			{
				cmd = 3;
			}
			
			query[idx] = new Query(cmd, n1, n2, ok);
		}
		// setSize함수에서 체인 분할을 위해 가장 무거운 노드를 인접노드 중 가장 앞으로 옮긴다.
		for(int i=1; i<=N; i++)	// 트리가 여러개일 수 있어서 각 노드마다 모두 탐색
			if(size[i] == 0)
				setSize(i, 0);
		
		// 트리가 모두 연결되어있지 않을 수 있다. 그래서 모든 노드에 대해 체인 세팅을 해주어야 함, 체인 미지정 노드는 chainHead 값이 0
		// setHLD함수에서 세그먼트트리에 대응하는 노드 인덱스를 생성 및 체인 정보들 입력
		for(int i=1; i<=N; i++)
		{
			if(chainHead[i] == 0)
			{
				chainParent[i] = i;
				chainHead[i] = i;
				setHLD(i, 0, ++chainLevelValue);
			}
		}

		StringBuilder sb = new StringBuilder();
	
		for(int idx=1; idx<=Q; idx++)// 결과 연산
		{
			Query q = query[idx];
			
			if(q.cmd == 1 && !q.ok)	// 연결 불가시 바로 impossible출력 후 다음 연산
			{
				sb.append("impossible").append('\n');
				continue;
			}
			
			if(q.cmd == 2)	// bridge 명령일 때는 이전에 이미 구했으니 스킵
			{
				sb.append(q.ok ? "yes" : "no").append('\n');
				continue;
			}
			
			if(q.cmd == 3)// penguins A X - 섬 A에 살고있는 펭귄 수를 X로 교체 출력필요없음
			{
				update(1, 1, N, treeIdx[q.n1], q.n2);
				continue;
			}

			// 이하 excursion 명령으로 A-B로 갈수 있는 경우 모든 펭귄의 수를 출력
			
			// 먼저 chain을 올린다.
			int n1 = q.n1;
			int n2 = q.n2;
			if(n1 == n2)	// 한 정점에 대해 탐색시 바로 결과 출력
			{
				sb.append(query(1, 1, N, treeIdx[n1], treeIdx[n1])).append('\n');
				continue;
			}
			
			int ans = 0;
			
			// 체인이 같아 질 때 까지 더 깊은 레벨인 n2를 올린다. 
			while(chainHead[n1] != chainHead[n2])
			{
				if(chainLevel[n1] > chainLevel[n2])	// 체인 레벨은 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])	// n2가 트리상 인덱스가 더 커야한다.
			{
				int tmp = n1;
				n1 = n2;
				n2 = tmp;
			}
			
			ans += query(1, 1, N, treeIdx[n1], treeIdx[n2]);
			
			sb.append(ans).append('\n');
		}
		
		System.out.print(sb);
	}
	public static int query(int treeNode, int s, int e, int left, int right) {
		if(e < left || right < s)
			return 0;
		
		if(left <=s && e <= right)
			return tree[treeNode];
		
		int mid = (s + e) >> 1;
		
		return query(treeNode << 1, s, mid, left, right)
				+ query(treeNode << 1 | 1, mid + 1, e, left, right);
	}
	public static void update(int treeNode, int s, int e, int targetIdx, int value) {
		if(e < targetIdx || targetIdx < s)
			return;
		if(s == e)
		{
			tree[treeNode] = value;
			return;
		}
		int mid = (s + e) >> 1;
		update(treeNode << 1, s, mid, targetIdx, value);
		update(treeNode << 1 | 1, mid + 1, e, targetIdx, value);
		
		tree[treeNode] = tree[treeNode << 1] + tree[treeNode << 1 | 1];
	}
	public 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;				// head는 자기자신
				chainParent[next] = nowNode;		// 다음 체인으로 점프를 위한 이전노드 삽입
				setHLD(next, nowNode, level + 1);	// 새 체인은 level증가
			}
		}
		
		update(1, 1, N, treeIdx[nowNode], arr[nowNode]);
	}
	public static void setSize(int nowNode, int parentNode) {
		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[nowNode] += size[next];
			if(heavySize < size[next])
			{
				heavySize = size[next];
				heavyIdx = i;
			}
		}
		if(adNode[nowNode].size() > 0)
			Collections.swap(adNode[nowNode], 0, heavyIdx);
	}
}
class UnionFind{
	int N;
	int parent[];
	UnionFind(int N){
		this.N = N;
		this.parent = new int[N + 1];
		
		for(int i=1; i<=N; i++)
			parent[i] = i;
	}
	public boolean union(int node1, int node2) {
		int parent1 = find(node1);
		int parent2 = find(node2);
		
		if(parent1 != parent2)
		{
			if(parent1 < parent2)
				parent[parent2] = parent1;
			else
				parent[parent1] = parent2;

			return true;// 다리를 지어야하면 yes반환
		}
		
		return false; // 다리를 지을 필요가 없으면 no반환
	}
	public int find(int node) {
		if(parent[node] == node)
			return node;
		
		return parent[node] = find(parent[node]);
	}
}
class Query{
	// cmd = (1)excursion, (2)bridge, (3)penguins
	int cmd, n1, n2;
	boolean ok;
	Query(int cmd, int n1, int n2, boolean ok){
		this.cmd = cmd;
		this.n1 = n1;
		this.n2 = n2;
		this.ok = ok;
	}
}

 

 

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

[ 문제 요약 ]

  • 트리가 주어지며, 정점 사이 거리는 1입니다.
  • 2개 유형의 쿼리가 주어지며, 쿼리 유형이 1이면 주어지는 두 a, b 노드 사이의 거리를 출력, 쿼리 유형이 2이면 주어지는 v 노드의 부모와 연결을 끊고, v 노드의 자식 노드들은 그 부모와 연결시킵니다.

 

[ 테스트 케이스 설명 ]

8		// 정점 개수(1<=100,000)
1 5 2 1 2 5 5	// 1번은 루트, 2번노드부터 그 부모 노드가 주어짐
7		// 질문개수 (1<=100,000)
2 4
1 7 6		// 쿼리 유형이 1이면 a,b가 주어지고, 두 정점 사이 거리 출력
2 5		// 쿼리 유형이 2이면 그 뒤 정수 v가 주어지며, v정점을 제거한다.
1 3 8
1 8 6
2 2
1 8 6
//답
4
2
3
2

 

 

[ 알고리즘 분류 ]

  • 자료구조
  • 트리
  • 세그먼트 트리
  • 최소 공통 조상
  • 오일러 경로 테크닉
  • heavy-light 분할

 

[ 문제 해설 ]

 

이 문제도 전형적인 HLD + 세그먼트 트리 문제입니다.

 

주어지는 트리 정보는 바로 세그먼트 트리에 적용할 수 없기 때문에, HLD를 사용해 트리를 여러 개의 체인으로 분할하고, 각 노드당 세그먼트 트리에 대응하는 인덱스를 저장하여 문제를 풉니다.

 

세그먼트 트리는 구간 합 세그먼트 트리를 작성합니다. 그래야 경로를 따라 올라가며 구간 합으로 거리를 쉽게 구할 수 있습니다.

 

그리고 주어지는 쿼리 유형에 따라 노드의 간선을 제거하던가, 두 노드의 거리를 출력합니다.

 

여기서 중요한 것은, 두 노드 사이에 간선의 정보를 어디에 저장해야 하느냐입니다.

 

A-B 노드가 있을 때, A를 부모 노드, B를 자식 노드라 하면 간선 정보는 B 노드 즉, 자식 노드에 저장합니다. 그래야 문제 조건에 맞게 간단한 게 코드를 짤 수 있습니다.

 

간선 정보를 자식 노드에 저장했기 때문에, 최종적으로 마지막 구간 합 쿼리를 실행할 때는

 

부모 노드 자신은 쿼리 구간에서 빠져야 합니다. 코드는 아래와 같습니다.

 

ans += query(1, 1, N, treeIdx[a] + 1, treeIdx[b]);

 

위 코드에서 treeIdx[a] + 1을 하는데, 이렇게 하지 않고, treeIdx[a]를 해버리면, a의 부모 노드로 가는 간선의 값까지 가져오므로 제대로 된 값을 갖고 올 수 없습니다.

 

HLD에 대한 설명은 아래 문제에서 디테일하게 설명했습니다.

https://kyjdummy.tistory.com/21

 

BOJ 백준 13510 트리와 쿼리 1

문제 : https://www.acmicpc.net/problem/13510[ 문제 요약 ]N 개의 노드와 N - 1개의 간선이 주어지며 2가지 명령어가 임의로 주어집니다.1 i c : i 번 간선의 비용을 c로 변경2 u v : u에서 v로 가는 단순 경로에

kyjdummy.tistory.com

 

나머지 설명은 코드를 통해 확인 가능합니다.

 

[ 정답 코드 ]

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 [] tree;		// 세그먼트 트리 배열
	static int [] treeIdx;		// 노드 번호 -> 세그먼트 트리 인덱스로 변환할 배열
	static int [] chainHead;	// 각 체인의 head값
	static int [] chainLevel;	// 각 체인의 level
	static int [] chainParent;	// 각 체인의 이전 체인 값
	static ArrayList<Integer>[] adNode;
	
	public static void main(String[] args)throws Exception{
		BufferedReader	br = new BufferedReader(new InputStreamReader(System.in));
		StringBuilder	sb = new StringBuilder();
		
		N		= Integer.parseInt(br.readLine());
		tree		= new int[N * 4];
		treeIdx		= new int[N + 1];
		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<>();
		
		StringTokenizer	st = new StringTokenizer(br.readLine());
		for(int child=2; child<=N; child++)
		{
			int parent = Integer.parseInt(st.nextToken());
			// 양방향 연결
			adNode[parent].add(child);
			adNode[child].add(parent);
		}
		
		// HLD 세팅을 위해 자식 노드가 큰것을 인접리스트에서 가장 첫번째로 옮긴다.
		setSize(1, 0, new int[N + 1]);
		
		chainHead[1] = 1;// 1번 노드 헤드는 자기자신
		
		// 해당 함수에서 논리적인 체인으로 나누고, chain정보를 입력한다.
		setHLD(1, 0, 1);
		
		// 간선의 정보를 세그먼트 트리에 저장해야 하므로, 부모-자식 노드 관계에서 자식노드에 간선 정보를 저장한다.
		for(int child=2; child<=N; child++)
			update(1, 1, N, treeIdx[child], 1); // 1번 노드를 제외한 자식 노드들의 위치에 1을 더해서 간선 연결을 표현한다.

		Q = Integer.parseInt(br.readLine());// 질문개수 (1<=100,000)
		
		while(Q-->0)
		{
			st = new StringTokenizer(br.readLine());
			int op = Integer.parseInt(st.nextToken());

			if(op == 1)// 쿼리 유형이 1이면 a,b가 주어지고, 두 정점 사이 거리 출력
			{
				int a = Integer.parseInt(st.nextToken());
				int b = Integer.parseInt(st.nextToken());
				int ans = 0;
				// 같은 체인이 될 때까지 레벨을 낮춘다.
				while(chainHead[a] != chainHead[b])
				{
					if(chainLevel[a] > chainLevel[b])
					{
						int tmp = a;
						a = b;
						b = tmp;
					}
					// b노드가 속한 체인의 전체 간선 정보를 쿼리해와서 ans에 더함 
					ans += query(1, 1, N, treeIdx[chainHead[b]], treeIdx[b]);
					
					b = chainParent[b];	// 다음체인으로 바로 점프
				}
				// 같은 체인이 되었다면, 같은 체인 내에서 마지막 연산을 진행
				if(treeIdx[a] > treeIdx[b])
				{
					int tmp = a;
					a = b;
					b = tmp;
				}
				
				ans += query(1, 1, N, treeIdx[a] + 1, treeIdx[b]);// 자식 노드에 간선정보를 저장하였으므로 시작은 + 1을 한다.
				
				sb.append(ans).append('\n');
				
				continue;
			}
			
			// 쿼리 유형이 2이면 그 뒤 정수 v가 주어지며, v정점을 제거한다.
			int v = Integer.parseInt(st.nextToken());
			// 해당 노드에 0을 대입해 노드 삭제를 표현
			update(1, 1, N, treeIdx[v], 0);
		}
		System.out.print(sb);
	}
	static int query(int treeNode, int s, int e, int left, int right) {
		if(e < left || right < s)
			return 0;
		
		if(left<=s && e<=right)
			return tree[treeNode];
		
		int mid = (s + e) >> 1;
		
		return query(treeNode << 1, s, mid, left, right)
				+ query(treeNode << 1 | 1, mid + 1, e, left, right);
	}
	static void update(int treeNode, int s, int e, int targetIdx, int value) {
		if(e < targetIdx || targetIdx < s)
			return;
		
		if(s == e)
		{
			tree[treeNode] = value;
			return;
		}
		
		int mid = (s + e) >> 1;
		
		update(treeNode << 1, s, mid, targetIdx, value);
		update(treeNode << 1 | 1, mid + 1, e, targetIdx, value);
		
		tree[treeNode] = tree[treeNode << 1] + tree[treeNode << 1 | 1];
	}
	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);	// 새 체인시 level 증가
			}
		}
	}
	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);
	}
}

 

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

[ 문제 요약 ]

  • 노드 N 개와, 간선이 N-1 개인 트리 정보가 주어지며, 쿼리에 답합니다.
  • 쿼리는 A, B, C, D가 주어지며, A에서 B로 가는 최단 경로의 간선을 삭제했을 때, C에서 D로 갈 수 있다면 YES 출력, 없다면 NO 출력입니다.

 

[ 테스트 케이스 설명 ]

8 4		// 노드 수(2<=100,000), 쿼리수(1<=300,000)
1 2		// 연결된 두 노드번호로 양방향 연결
1 3
3 4
3 5
3 6
4 7
4 8
6 7 2 3	// A,B,C,D가 주어지며 A->B로 가는 최단 경로에 모든 간선제거 후, C->D로 가는 경로가 있다면 YES, 없으면 NO출력
6 7 7 8
4 6 1 5
4 7 4 8
//답
YES
NO
YES
YES

 

[ 문제 해설 ]

 

이 문제는 기본적인 HLD와 Lazy Propagation 문제입니다.

 

https://www.acmicpc.net/problem/16453 이 문제와 비슷하지만, 쿼리수가 더 많습니다.

 

최댓값 세그먼트 트리를 만들고, 간선이 연결되어 있는 것은 값이 0, 연결되어 있지 않다면 1을 표현하면 노드 간 이동 시 쉽게 연결 유무를 확인할 수 있습니다.

 

처음 A-B 노드를 지날 때, 해당 구간에 전부 1을 더해주고, 나중에 C-D 구간 지나면서 구간의 MAX 값을 찾아오면, 1이면 연결되어 있지 않은 것, 0이면 연결되어 있는 것으로 판단합니다.

 

그리고 A-B 노드를 다시 지나면서, 1을 더했던 것을 0으로 만들어 주기 위해 -1을 다시 더해줍니다.

 

여기서 중요한 것은, 세그먼트 트리의 인덱스마다 저장되는 값은 간선의 유무를 저장하는 것입니다. 예를 들어 A, B 노드가 있을 때 두 노드의 세그먼트 트리에 대응하는 인덱스가 ㄴ1,2라 하면, 간선의 값을 2에 저장합니다. 그래야 나중에 쿼리 시 정확한 간선을 탐색할 수 있습니다.

 

단순히 각 노드의 가중치를 저장하는 것이면 1,2에 각각의 가중치를 저장할 테지만, 간선 정보기 때문에 두 노드 중 자식 노드에 가중치를 저장해야 일관적으로 값을 구할 수 있습니다.

 

HLD 알고리즘의 기본 설명은 아래 링크에 나와있습니다.

https://kyjdummy.tistory.com/21

 

BOJ 백준 13510 트리와 쿼리 1

문제 : https://www.acmicpc.net/problem/13510[ 문제 요약 ]N 개의 노드와 N - 1개의 간선이 주어지며 2가지 명령어가 임의로 주어집니다.1 i c : i 번 간선의 비용을 c로 변경2 u v : u에서 v로 가는 단순 경로에

kyjdummy.tistory.com

 

[ 정답 코드 ]

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[] tree;			// 세그먼트 트리
	static int[] lazy;			// lazy propagation을 위한 배열
	static int[] treeIdx;			// 노드번호 -> 세그먼트 트리 인덱스 변환 정보를 담은 배열
	static int[] chainHead;			// 체인의 head값
	static int[] chainLevel;		// 체인의 level
	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());
		Q		= Integer.parseInt(st.nextToken());
		treeIdx		= new int[N + 1];
		chainHead	= new int[N + 1];
		chainLevel	= new int[N + 1];
		chainParent	= new int[N + 1];
		tree		= new int[N * 4];
		lazy		= new int[N * 4];
		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]);
		// 1번 노드의 헤드는 자기자신
		chainHead[1] = 1;
		// HLD에 필요한 값 세팅
		setHLD(1, 0, 1);
		
		while(Q-->0)
		{
			st = new StringTokenizer(br.readLine());
			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로 업데이트하여 노드가 끊어짐을 표현
			
			sb.append(isConnect(c,d) ? "YES" : "NO").append('\n');
			
			rangeUpdate(a, b, -1);	// a-b 노드 구간을 다시 -1을 더해 노드 연결을 복구
		}
		System.out.print(sb);
	}
	static boolean isConnect(int n1, int n2) {
		// 체인 헤드가 같아질 때 까지 레벨을 올리면서 query결과가 true면 false를 반환해서 연결이 끊어짐을 알린다.
		while(chainHead[n1] != chainHead[n2])
		{
			if(chainLevel[n1] > chainLevel[n2])
			{
				int tmp = n1;
				n1 = n2;
				n2 = tmp;
			}
			// 쿼리 결과가 1이면 끊어진 것
			if(1 == query(1, 1, N, treeIdx[chainHead[n2]], treeIdx[n2]))
				return false;

			n2 = chainParent[n2];
		}
		if(treeIdx[n1] > treeIdx[n2])
		{
			int tmp = n1;
			n1 = n2;
			n2 = tmp;
		}
		// 쿼리 결과가 1이 아니여야 연결되어 있는 것
		return 1 != query(1, 1, N, treeIdx[n1] + 1, treeIdx[n2]);
	}
	static void rangeUpdate(int n1, int n2, int value) {
		// 체인 헤드가 같아질 때 까지 레벨을 올린다. 
		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] + 1, treeIdx[n2], value);
	}
	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;
		
		return Math.max(query(treeNode << 1, s, mid, left, right),
						query(treeNode << 1 | 1, mid + 1, e, left, right));
	}
	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] = Math.max(tree[treeNode << 1] , tree[treeNode << 1 | 1]);
	}
	static void propagate(int treeNode, int s, int e) {
		if(lazy[treeNode] != 0)
		{
			tree[treeNode] += lazy[treeNode];
			if(s != e)
			{
				lazy[treeNode << 1] += lazy[treeNode];
				lazy[treeNode << 1 | 1] += lazy[treeNode];
			}
			lazy[treeNode] = 0;
		}
	}
	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);// 새 체인은 level이 1증가한다.
			}
		}
	}
	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);
	}
}

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

BOJ 백준 2927 남극 탐험  (1) 2025.04.30
BOJ 백준 13038 Tree  (0) 2025.04.29
BOJ 백준 16453 Linhas de Metrô  (0) 2025.04.28
BOJ 백준 31234 대역폭 관리  (0) 2025.04.27
BOJ 백준 17033 Cow Land  (1) 2025.04.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

 

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

[ 문제 요약 ]

  • 정점과 트리가 주어지고, 각 노드당 최대 대역폭이 주어집니다. 각 노드의 대역폭은 0부터 시작이고, 노드당 주어진 최대 대역폭을 넘을 수 없습니다.
  • 2개의 노드와 추가할 대역폭이 쿼리로 주어집니다. 주어진 2개의 노드 사이 경로의 모든 노드에 주어진 대역폭을 업데이트합니다. 이때, 경로 사이에 최대 대역폭을 초과하는 노드가 있다면 반복을 종료하고 지금까지 실행한 쿼리의 개수를 출력합니다.

[ 테스트 케이스 설명 ]

3 2	// 정점개수(2<=100,000), 예약 개수(2<=100,000)
1 2	// 연결된 두 정점 번호(v!=u)
2 3
5 5 5	// 각 노드의 한계 대역폭(0<=1,000,000,000)
2 1 5	// x, y, w 로 x,y노드에 w 대역폭이 증가함(w 1<=1,000,000,000)
3 2 5
답 : 1

 

[ 문제 해설 ]

  • 두 정점 사이 경로에 대해 대역폭을 모두 증가시켜야 하므로 느리게 갱신되는 세그먼트 트리를 사용합니다.
  • 그리고 최소 경로를 찾기 위해 HLD 알고리즘을 사용합니다.
  • 기본적인 HLD 알고리즘은 https://kyjdummy.tistory.com/21 여기에 설명되어 있습니다.
  • 각 노드당 대역폭을 업데이트하는데 최대 대역폭을 초과하는 것에 대해 어떻게 알 수 있을까요?
  • 방법은, 최솟값 세그먼트 트리를 활용하는 것입니다.
  • 최솟값 세그먼트 트리에 초깃값으로 최대 대역폭을 모두 입력해 놓고, 쿼리가 주어질 때마다 값 업데이트를 하며 세그먼트 트리 값에서 빼주면 됩니다. 그럼 처음으로 0미만이 되는 값이 나올 때, 그 노드는 더 이상 대역폭을 사용할 수 없게 되는 것입니다.

 

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

 

[ 정답 코드 ]

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.StringTokenizer;
class Main{
	
	static boolean isEnd = false;	// 유효한지, 아닌지를 판별, 끝내야하면 true
	static int N, Q;
	static int idx;			// 입력된 노드 번호를 세그먼트 트리의 인덱스 번호로 변환할 변수
	static int[] arr;		// 최초 입력되는 최대 대역폭
	static int[] tree;		// 최솟값 세그먼트 트리 배열
	static int[] lazy;		// 구간 업데이트를 위한 lazy 배열
	static int[] treeIdx;		// 입력된 노드번호 -> 세그먼트 트리 인덱스로 변환할 배열
	static int[] nodeIdx;		// 세그먼트 트리 인덱스 -> 입력된 노드번호로 역으로 변환할 배열
	static int[] chainHead;		// HLD에서 해당 체인의 첫번째 값
	static int[] chainLevel;	// HLD에서 노드의 레벨을 저장
	static int[] chainParent;	// HLD에서 이전 체인으로 넘어가기 위해 점프할 노드 값
	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());
		N			= Integer.parseInt(st.nextToken());// 정점개수(2<=100,000)
		Q			= Integer.parseInt(st.nextToken());// 예약 개수(2<=100,000)
		arr			= new int[N + 1];
		tree		= new int[N * 4];
		lazy		= new int[N * 4];
		treeIdx		= new int[N + 1];
		nodeIdx		= new int[N + 1];
		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);
		}
		
		st = new StringTokenizer(br.readLine());
		for(int i=1; i<=N; i++)
			arr[i] = Integer.parseInt(st.nextToken());// 각 노드의 한계 대역폭(0<=1,000,000,000)

		// 해당 함수에서 체인의 크기를 확인하여 무거운 노드를 인접리스트의 가장 첫번째로 옮김
		setSize(1, 0, new int[N + 1]);
		
		chainHead[1] = 1;	// 1번 노드의 헤드는 자기자신
		// 해당 노드에서 chain정보를 입력
		setHLD(1, 0, 1);
		init(1, 1, N);	// 최솟값 세그먼트 트리 초기화
		
		int ans = 0;
		
		while(Q-->0)
		{
			st = new StringTokenizer(br.readLine());
			// x, y, w 로 x,y노드에 w 대역폭이 증가함(w 1<=1,000,000,000)
			int x = Integer.parseInt(st.nextToken());
			int y = Integer.parseInt(st.nextToken());
			int w = Integer.parseInt(st.nextToken());
			// 체인의 head값이 같아질 때 까지 높은 레벨을 낮은 레벨로 지속적으로 올리면서, 유효성을 판단한다.
			// 이 때 update를 같이함. 최솟값 세그먼트 트리에서 0미만으로 값이 떨어지면 대역폭 초과이므로 종료함
			while(!isEnd && chainHead[x] != chainHead[y])
			{
				if(chainLevel[x] > chainLevel[y])
				{
					int tmp = x;
					x = y;
					y = tmp;
				}
				// 해당 체인의 범위에 w만큼 대역폭 감소 처리, 최솟값 세그먼트 트리이므로, 0미만이 되면 불가
				update(1, 1, N, treeIdx[chainHead[y]], treeIdx[y], w);
				y = chainParent[y];	// 다음 체인으로 바로 점프
			}
			// 같은 체인이 되었고, 대역폭 감소가 유효하면
			if(!isEnd)
			{
				// 트리 인덱스가 낮은게 왼쪽, 큰게 오른쪽 오도록 값 변경 후 update 연산
				if(treeIdx[x] > treeIdx[y])
				{
					int tmp = x;
					x = y;
					y = tmp;
				}
				update(1, 1, N, treeIdx[x], treeIdx[y], w);
			}
			// 모든 것이 유효하면 ans에  + 1
			if(!isEnd)
				++ans;
			else
				break;
		}
		System.out.print(ans);
	}
	// 해당 함수에서는 트리를 순회하며 가장 무거운 노드를 인접리스트에서 맨앞으로 옮기는 역할만 한다.
	public 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);
	}
	// 트리를 순회하며 HLD 체인 값들을 입력 + 세그먼트 트리 인덱스와 노드 번호간 치환 배열 세팅
	static void setHLD(int nowNode, int parentNode, int level) {
		chainLevel[nowNode] = level;
		treeIdx[nowNode] = ++idx;
		nodeIdx[idx] = nowNode;
		for(int i=0; i<adNode[nowNode].size(); i++)
		{
			int next = adNode[nowNode].get(i);
			
			if(next == parentNode)
				continue;
			
			if(i == 0)	// heay일 경우, 기존 체인 유지
			{
				chainHead[next] = chainHead[nowNode];
				chainParent[next] = chainParent[nowNode];
				setHLD(next, nowNode, level);
			}
			else		// light일 경우 새로운 체인 시작
			{
				chainHead[next] = next;			// 새 체인의 head는 자기자신
				chainParent[next] = nowNode;	// 이전 체인 점프할 정보는 이전 노드
				setHLD(next, nowNode, level + 1);	// 새 체인에서는 level이 증가한다
			}
		}
	}
	// 최초 입력된 정점의 최대 대역폭을 세팅하고 최솟값 세그먼트 트리로 만든다.
	static void init(int treeNode, int s, int e) {
		if(s == e)
		{
			// 세그먼트 트리 인덱스를 node번호로 치환한 후 값을 대입해야 한다.
			tree[treeNode] = arr[nodeIdx[s]];
			return;
		}
		
		int mid = (s + e) >> 1;
		
		init(treeNode << 1, s, mid);
		init(treeNode << 1 | 1, mid + 1, e);
		
		tree[treeNode] = Math.min(tree[treeNode << 1], tree[treeNode << 1 | 1]);
	}
	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] = Math.min(tree[treeNode << 1], tree[treeNode << 1 | 1]);
	}
	// 구간 업데이트를 위한 propagate함수, 업데이트 숫자를 tree 값에 빼주고, 값이 0 이하로 내려가면,
	// 즉, 대역폭 초과를 처리하려고 하면 isEnd를 true로 바꿔 끝낸다.
	static void propagate(int treeNode, int s, int e) {
		if(lazy[treeNode] != 0)
		{
			tree[treeNode] -= lazy[treeNode];
			
			if(tree[treeNode] < 0)
				isEnd = true;
			
			if(s != e)
			{
				lazy[treeNode << 1]		+= lazy[treeNode];
				lazy[treeNode << 1 | 1] += lazy[treeNode];
			}
			
			lazy[treeNode] = 0;
		}
	}
}

 

 

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

 

[ 문제 요약 ]

  • 노드와 질의 가 주어지며, 각 노드에 대해 초깃값이 주어집니다.
  • 1 i v 입력 : i 노드의 값을 v로 변경
  • 2 i j 입력 : i 노드부터 j 노드까지 경로의 모든 값들을 XOR 하여 출력

[ 테스트 케이스 설명 ]

5 5			// 노드개수N(2<=100,000), 질의 수Q(1<=100,000)
1 2 4 8 16		// 각 노드의 초기값 0<=1,000,000,000
1 2			// 노드의 연결 관계
1 3
3 4
3 5
2 1 5			// 2인 경우, 1과 5노드 경로사이의 XOR값을 출력하라
1 1 16			// 1인 경우, 1번노드를 16값으로 업데이트하라는 뜻
2 3 5
2 1 5
2 1 3
답
21
20
4
20

 

[ 문제 해설 ]

  • 이 문제를 풀기 위해서는 HLD 알고리즘을 사용해야 합니다. HLD 알고리즘 관련 설명은 아래 링크의 [ 문제 해설 ]에 설명되어 있으니 참고 부탁드립니다.
  • https://kyjdummy.tistory.com/21
  • HLD 알고리즘을 사용해 트리를 세그먼트 트리에서 사용할 수 있도록 체인으로 분할 후 각 범위에 맞게 XOR 값을 출력해 주면 되는 전형적인 HLD 문제입니다.

[ 정답 코드 ]

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[] value;			// 초기에 입력되는 값을 입력받을 배열
	static int[] treeIdx;		// 노드번호 -> 세그먼트 트리 인덱스 치환
	static int[] tree;			// 세그먼트 트리
	static int[] chainHead;		// 해당 체인의 Head값을 담을 배열
	static int[] chainLevel;	// 해당 체인의 Level을 담을 배열
	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());// 노드개수N(2<=100,000)
		Q			= Integer.parseInt(st.nextToken());// 질의 수Q(1<=100,000)
		value		= new int[N + 1];
		treeIdx		= new int[N + 1];
		tree		= new int[N * 4];
		chainHead	= new int[N + 1];
		chainLevel	= new int[N + 1];
		chainParent = new int[N + 1];
		adNode		= new ArrayList[N + 1];
		
		st = new StringTokenizer(br.readLine());
		for(int i=1; i<=N; i++)
		{
			adNode[i] = new ArrayList<>();
			value[i] = Integer.parseInt(st.nextToken());
		}
		
		for(int i=1; i<N; i++)
		{
			st = new StringTokenizer(br.readLine());
			int n1 = Integer.parseInt(st.nextToken());
			int n2 = Integer.parseInt(st.nextToken());
			adNode[n1].add(n2);
			adNode[n2].add(n1);
		}
		// 해당 함수에서 자식 서브트리 크기가 가장 큰 노드를 맨 앞으로 옮김
		setSize(1, 0, new int[N + 1]);
		
		chainHead[1] = 1;// 1번 노드의 헤드는 자기자신
		
		// HLD정보 세팅 및 세그먼트 트리 값 업데이트
		setHLD(1, 0, 1);
		
		while(Q-->0)
		{
			st = new StringTokenizer(br.readLine());
			int op = Integer.parseInt(st.nextToken());
			int i = Integer.parseInt(st.nextToken());
			int j = Integer.parseInt(st.nextToken());

			if(op == 1)// 1인 경우, i번노드를 j값으로 업데이트
			{
				update(1, 1, N, treeIdx[i], j);
				continue;
			}
			// 2인 경우, i, j 노드 경로사이의 XOR값을 출력
			
			int ans = 0;
			// 같은 체인이 될 때 까지 level을 올림
			while(chainHead[i] != chainHead[j])
			{
				if(chainLevel[i] > chainLevel[j])
				{
					int tmp = i;
					i = j;
					j = tmp;
				}
				
				ans ^= query(1, 1, N, treeIdx[chainHead[j]], treeIdx[j]);
				
				j = chainParent[j];
			}
			
			// 같은 체인에 속하면 그 안에서 다시 한번 해준다.
			if(treeIdx[i] > treeIdx[j])
			{
				int tmp = i;
				i = j;
				j = tmp;
			}
			
			ans ^= query(1, 1, N, treeIdx[i], treeIdx[j]);
			
			sb.append(ans).append('\n');
		}
		System.out.print(sb);
	}
	static int query(int treeNode, int s, int e, int left, int right) {
		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 update(int treeNode, int s, int e, int targetIdx, int value) {
		if(e < targetIdx || targetIdx < s)
			return;
		if(s == e)
		{
			tree[treeNode] = value;
			return;
		}
		
		int mid = (s + e) >> 1;
		
		update(treeNode << 1, s, mid, targetIdx, value);
		update(treeNode << 1 | 1, mid + 1, e, targetIdx, value);
		
		tree[treeNode] = tree[treeNode << 1] ^ tree[treeNode << 1 | 1];
	}
	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);	// 새 체인일 경우 레벨 증가
			}
		}
		update(1, 1, N, treeIdx[nowNode], value[nowNode]);
	}
	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);
	}
}

 

 

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

[ 문제 요약 ]

  • 트리가 주어지고, 두 정점이 주어집니다. 이때 단순히 두 정점이 이어져 있는지 체크하는 쿼리와, 이어져 있거나 이어져 있지 않을 경우를 파악하고, 이어져 있다면 먼저 입력된 노드를 그 부모노드와 끊고, 이어져있지 않다면 나중에 입력된 노드를 그 노드의 부모 노드와 끊습니다.

 

[ 테스트 케이스 설명 ]

3 4	// 트리의 정점개수(1 ≤ 200,000), 질의 개수(1 ≤ 200,000)
1		// 부모 정점 번호(1<=N)
1		// 부모 정점 번호(1<=N)
2 3 1	// b,c,d / d가1이면 b와 c를 연결하는 경로가 있는지 파악후 있으면 b의 부모 정점과 b연결 엣지 제거, 없으면, c의 부모정점과 c를 연결하는 에지 제거
1 3 0	// b,c,d / d가0이면 b와 c를 연결하는 경로가 있는지만 파악
2 3 1
1 3 1
//답
YES
YES
NO
NO

[ 문제 해설 ]

  • 이 문제는 HLD 알고리즘을 쉽게 풀 수 있습니다. HLD 알고리즘은 아래 링크의 [문제 해설]을 보시면 설명되어 있습니다.
  • https://kyjdummy.tistory.com/21
  • 문제에서 요구하는 것은 두 정점이 연결되어 있는지, 아닌지 확인하는 것이고, 또한 연결을 끊는 기능도 필요로 합니다.
  • 이를 구현하기 위해 boolean 세그먼트 트리를 만듭니다. 연결되어 있다면 false, 연결이 끊기면 true로 하여, 두 노드 사이에서 세그먼트 트리 쿼리 연산 결과가 true가 나오면 두 노드는 끊어져있는 것으로 보면 됩니다.
  • d 값이 1인 경우 연결 유무에 따라 끊어야 하는 노드가 다르기 때문에 분기를 잘해야 합니다.
  • 그리고 두 노드의 간선 정보를 세그먼트 트리에 저장해야 합니다. 부모 노드, 자식 노드가 있을 때, 간선의 정보는 자식 노드에 저장해야 합니다.
  • 디테일은 코드를 통해 확인 가능합니다.

 

 

[ 정답 코드 ]

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[] chainHead;
	static int[] chainLevel;
	static int[] chainParent;
	static boolean[] tree;
	static ArrayList<Integer>[] adNode;

	public static void main(String[] ags)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());	// 트리의 정점개수(1 ≤ 200,000)
		Q			= Integer.parseInt(st.nextToken());	// 질의 개수(1 ≤ 200,000)
		tree		= new boolean[N * 4];				// 초기값 다 false, 연결시 false, 연결을 끊으면 true
		treeIdx		= new int[N + 1];
		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 child=2; child<=N; child++)
		{
			int parent = Integer.parseInt(br.readLine());
			adNode[parent].add(child);// 단방향 연결
		}
		
		// 해당 함수에서 HLD를 위한 무거운 자식 노드를 맨앞으로 옮긴다.
		moveHeavyChildToFront(1, 0, new int[N + 1]);
		
		chainHead[1] = 1;	// 1번노드의 헤드는 자기 자신
		
		setHLD(1, 0, 1);	// HLD 세팅
		
		while(Q-->0)
		{
			st = new StringTokenizer(br.readLine());
			int b = Integer.parseInt(st.nextToken());
			int c = Integer.parseInt(st.nextToken());
			int d = Integer.parseInt(st.nextToken());
			boolean isPossible = true;
			
			int node1 = b;
			int node2 = c;
			// chainHead가 같아질 때 까지, 즉, 같은 체인에 속할 때 까지 반복
			while(isPossible && chainHead[node1] != chainHead[node2])
			{
				// 레벨이 낮은 것(값이 큰것)을 node2로 바꿔서 낮은 레벨을 지속적으로 위로 올린다. 
				if(chainLevel[node1] > chainLevel[node2])
				{
					int tmp = node1;
					node1 = node2;
					node2 = tmp;
				}
				
				boolean notConnected = query(1, 1, N, treeIdx[chainHead[node2]], treeIdx[node2]);
				
				if( !notConnected )				// 노드가 연결되어있는게 맞다면
					node2 = chainParent[node2];	// 다음 체인으로 점프
				else
					isPossible = false;
			}
			
			// 위 코드들을 통해 같은 체인에 속하게 되었을 것이고, 연결 유무를 최종 확인
			if(isPossible)
			{
				if(treeIdx[node1] > treeIdx[node2])
				{
					int tmp = node1;
					node1 = node2;
					node2 = tmp;
				}
				
				boolean notConnected = query(1, 1, N, treeIdx[node1] + 1, treeIdx[node2]);
				if(notConnected)
					isPossible = false;
			}
			
			if(d == 1)// d가 1이면
			{
				int deleteNode = c;	// b와 c를 연결하는 통로가 없으면 c의 부모정점과 c를 연결하는 엣지 제거
				
				if(isPossible)		// b와 c를 연결하는 통로가 있으면 b의 부모 정점과 b연결 엣지 제거
				{
					deleteNode = b;
				}
				
				update(1, 1, N, treeIdx[deleteNode], true);
			}
			
			sb.append(isPossible ? "YES\n" : "NO\n");
		}
		System.out.print(sb);
	}

	static boolean query(int treeNode, int s, int e, int left, int right) {
		
		if(e < left || right < s)
			return false;

		if(left<=s && e<=right)
			return tree[treeNode];
		
		int mid = (s + e) >> 1;
		
		return query(treeNode << 1, s, mid, left, right)
				| query(treeNode << 1 | 1,mid + 1, e, left, right);
	}
	static void update(int treeNode, int s, int e, int targetIdx, boolean value) {
		
		if(e < targetIdx || targetIdx < s)
			return;
		
		if(s == e)
		{
			tree[treeNode] = value;
			return;
		}
		
		int mid = (s + e) >> 1;
		
		update(treeNode << 1, s, mid, targetIdx, value);
		update(treeNode << 1 | 1, mid + 1, e, targetIdx, value);
		
		tree[treeNode] = tree[treeNode << 1] | tree[treeNode << 1 | 1];
	}
	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 child = adNode[nowNode].get(i);
			
			if(child == parentNode)
				continue;
			
			if(i == 0)	// heavy인 경우, 체인 그대로 유지
			{
				chainHead[child]	= chainHead[nowNode];	// heavy는 부모의 값을 그대로 저장
				chainParent[child]	= chainParent[nowNode];	// heavy는 부모의 값을 그대로 저장
				setHLD(child, nowNode, level);
			}
			else		// light인 경우, 새로운 체인 시작
			{
				chainHead[child]	= child;				// head는 자기자신
				chainParent[child]	= nowNode;				// 다음 점프할 노드는 자기의 부모노드로 세팅
				setHLD(child, nowNode, level + 1);			// 레벨을 늘려 새로운 체인 시작
			}
		}
	}
	static void moveHeavyChildToFront(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 child = adNode[nowNode].get(i);
			
			if(child == parentNode)
				continue;
			
			moveHeavyChildToFront(child, nowNode, size);
			
			size[nowNode] += size[child];
			
			if(heavySize < size[child])
			{
				heavySize = size[child];
				heavyIdx = i;
			}
		}
		
		if(adNode[nowNode].size() > 0)
			Collections.swap(adNode[nowNode], 0, heavyIdx);
	}
}

 

 

 

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

[ 문제 요약 ]

  • 트리가 주어지고, 처음엔 모든 노드가 흰색이라 생각한다.
  • 쿼리는 2가지로, 1 i와 2 v가 입력된다.
  • 1이 입력되면 i 번 노드의 색을 반전(흰 →검, 검→흰) 2가 입력되면 1번 노드부터 v 번 정점으로 가는 경로에 존재하는 첫 번째 검정 정점의 번호를 출력

 

[ 테스트 케이스 설명 ]

9			// 노드개수 2<=100,000
1 2		// 간선을 연결하는 두 정점
1 3
2 4
2 9
5 9
7 9
8 9
6 8
8		// 쿼리 개수 1<=100,000
2 3
1 8		// 1일때, 입력된 노드의 색을 반전시킨다. 초기값은 흰색
2 6		// 2일때, 1번에서 v번 정점으로 가는 경로에 존재하는 첫 번째 검정 정점의 번호를 출력
2 7
1 2
2 9
1 2
2 9
// 답
-1
8
-1
2
-1

 

[ 문제 해설 ]

  • 이 문제는 기본적인 HLD 알고리즘으로 풀 수 있습니다. HLD 알고리즘에 관한 설명은 아래 링크 [문제 해설]에서 확인할 수 있습니다.
  • https://kyjdummy.tistory.com/21

문제에서 원하는 출력은, 1번 노드에서 v 노드로 갈 때 첫 번째 노드의 번호입니다.

 

이를 위해 세그먼트 트리는 최솟값을 계산하는 세그먼트 트리를 만듭니다.

 

그리고 흰색일 때는, 세그먼트 트리에 MAX 값을 저장하고, 검정일 때는 세그먼트 트리 인덱스에 해당 노드의 값을 저장해 놓으면 쉽게 최솟값 세그먼트트리로 1번 노드에서 가장 가까운 검정 노드의 번호를 추출할 수 있습니다.

 

1번 노드에 가까워질 수록 세그먼트트리 인덱스 값은 작아질 것이기 때문입니다.

 

이를 위해 v 노드에서 1번 노드까지 바로 찾아갈 수 있도록 HLD 알고리즘은 필수입니다.

 

그리고 입력된 노드 번호를 세그먼트 트리 인덱스로 치환하는 배열을 만들고, 반대로 세그먼트 트리의 인덱스 값을 노드 번호

 

로 치환할 수 있는 배열을 만들어야 합니다.

 

그래야 빠른 출력이 가능합니다.

 

[ 정답 코드 ]

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.StringTokenizer;

class Main{
	
	static final int MAX = 1<<29;
	static int N, Q;
	static int idx;
	static int[] tree;
	static int[] nodeToTreeIndex;	// 노드 번호를 세그먼트 트리 인덱스로 변환하는 배열
	static int[] treeIndexToNode;	// 세그먼트 트리 인덱스를 노드 번호로 변환하는 배열
	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));
		StringBuilder	sb = new StringBuilder();
		StringTokenizer	st;
		
		N				= Integer.parseInt(br.readLine());// 노드개수 2<=100,000
		tree			= new int[N * 4];
		chainHead		= new int[N + 1];
		chainLevel		= new int[N + 1];
		chainParent		= new int[N + 1];
		adNode			= new ArrayList[N + 1];
		nodeToTreeIndex = new int[N + 1];
		treeIndexToNode	= new int[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 n1 = Integer.parseInt(st.nextToken());
			int n2 = Integer.parseInt(st.nextToken());
			adNode[n1].add(n2);
			adNode[n2].add(n1);
		}
		
		Arrays.fill(tree, MAX);
		// 해당 노드에서 heavy노드를 맨 앞으로 옮긴다.
		setSize(1, 0, new int[N + 1]);
		
		chainHead[1] = 1;	// 1번 노드의 헤드는 자기자신
		
		setHLD(1, 0, 1);	// HLD에 필요한 chain정보 및 인덱스들 세팅
		
		Q = Integer.parseInt(br.readLine());
		while(Q-->0)
		{
			st = new StringTokenizer(br.readLine());
			int op = Integer.parseInt(st.nextToken());
			int i = Integer.parseInt(st.nextToken());
			
			if(op == 1)	// 1인 경우 i번 정점 교체
			{
				update(1, 1, N, nodeToTreeIndex[i]);
				continue;
			}
			
			// op가 2인 경우 i번 정점부터 위로 올라오면서 최소 검정을 찾음, 검정은 세그먼트트리에서 MAX보다 작은 값 
			int ans = MAX;
			// 같은 체인까지 만듬
			while(chainHead[1] != chainHead[i])
			{
				ans = Math.min(ans,  query(1, 1, N, nodeToTreeIndex[chainHead[i]], nodeToTreeIndex[i]));
				
				i = chainParent[i];
			}
			// 같은 체인 후 체인안에서 마지막으로 비교하여 정답을 구함
			ans = Math.min(ans, query(1, 1, N, nodeToTreeIndex[1], nodeToTreeIndex[i]));
			// 출력은 노드번호이므로, 세그먼트 트리의 인덱스를 노드번호로 바꾸기 위해 treeIndexToNode 배열을 사용
			sb.append(ans == MAX ? -1 : treeIndexToNode[ans]).append('\n');
		}
		System.out.print(sb);
	}
	
	static void update(int treeNode, int s, int e, int targetIdx) {
		if(e < targetIdx || targetIdx < s)
			return;
		
		if(s == e)
		{
			tree[treeNode] = tree[treeNode] == MAX ? s : MAX;
			return;
		}
		
		int mid = (s + e) >> 1;
		
		update(treeNode << 1, s, mid, targetIdx);
		update(treeNode << 1 | 1, mid + 1, e, targetIdx);
		
		tree[treeNode] = Math.min(tree[treeNode << 1], tree[treeNode << 1 | 1]);
	}
	
	static int query(int treeNode, int s, int e, int left, int right) {
		if(e < left || right < s)
			return MAX;
		
		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 Math.min(l, r);
	}
	
	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) {
		chainLevel[nowNode] = level;
		nodeToTreeIndex[nowNode] = ++idx;	// 노드인덱스를 세그먼트 트리에서 사용할 인덱스로 변경
		treeIndexToNode[idx] = nowNode;		// 세그먼트 트리에서 사용할 인덱스를 노드번호로 변경
		
		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		// 새로운 체인 시작
			{
				chainHead[next] = next;			// 헤드는 자기자신
				chainParent[next] = nowNode;	// 점프할 노드 저장
				setHLD(next, nowNode, level + 1);// 레벨은 + 1
			}
		}
	}
	
}

 

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

BOJ 백준 16453 Linhas de Metrô  (0) 2025.04.28
BOJ 백준 31234 대역폭 관리  (0) 2025.04.27
BOJ 백준 17033 Cow Land  (1) 2025.04.26
BOJ 백준 13309 트리  (0) 2025.04.26
BOJ 백준 13510 트리와 쿼리 1  (0) 2025.04.24

 

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

[ 문제 요약 ]

  • N 개의 노드와 N - 1개의 간선이 주어지며 2가지 명령어가 임의로 주어집니다.
  • 1 i c : i 번 간선의 비용을 c로 변경
  • 2 u v : u에서 v로 가는 단순 경로에 존재하는 간선 비용 중 가장 큰 것을 출력합니다.
  • 2번 입력이 주어질 때마다 두 노드 사이 가장 큰 간선 비용을 한 줄에 하나씩 출력합니다.

[ 테스트 케이스 설명 ]

3	// 정점 수(2<=100,000)
1 2 1	// N-1개 줄에 i번 간선이 연결하는 두 정점 번호 u, v와 비용 w(1<=1,000,000)가 주어짐
2 3 2
3	// 쿼리 개수 Q(1<=100,000)
2 1 2
1 1 3	// 1 i c : i번 간선의 비용을 c로 바꾼다.
2 1 2	// 2 u v : u에서 v로 가는 단순 경로에 존재하는 비용 중 가장 큰 것을 출력한다.
답
1
3

 

[ 문제 해설 ]

이 문제에서 노드가 최대 10만 개이고, 조회하는 쿼리 개수가 10만 개 일 수 있으므로

 

완전 탐색 시 최악의 경우 O(10만^2)으로 시간 초과입니다.

 

이 문제를 풀기 위해서는 u, v 사이 간선을 완전 탐색이 아니라, 한 번에 찾을 수 있어야 합니다.

 

이를 위해 HLD( Heavy -Light Decomposition )과 세그먼트 트리 기법을 이용할 것입니다.

 

HLD는, 트리 위에서 경로 쿼리, 서브 트리 쿼리를 log^2N 시간에 처리할 수 있게 해주는 [트리 분해 기법]입니다.

 

세그먼트 트리를 이용하기 위해서는 선형 자료구조여야 하는데, 트리는 비선형이기 때문에 트리를 여러 개로 분할해 분할된 구간마다 세그먼트 트리를 이용할 수 있게 하는 기법입니다.

 

트리를 분할할 때 아무렇게 하는 게 아니라, 가장 무거운 노드(heavy)를 기준으로 트리를 분할합니다. ( 이렇게 하면 쿼리 시 건너야 할 체인 수를 logN으로 한정 지을 수 있는 수학적 이론이 있으나 길어져 생략합니다. )

 

가장 무거운 노드란 문제에 따라 정의하기 나름이겠으나, 이 문제에서는 자식 노드의 수가 가장 많은 노드를 무거운 노드라고 정의합니다.

 

트리를 분할할 때 분할되는 그룹 단위를 체인이라고 정의합니다. 분할 후 하나의 트리는 논리적인 여러 개의 체인이 되는 겁니다. 이 체인마다 대응되는 세그먼트 트리의 인덱스가 있게 되는 것입니다.

 

입력된 간선 정보를 기반으로 연결 리스트를 만들어 놓고,

 

첫 번째 DFS를 돌면서 자식 노드의 크기를 세고, 자식 노드의 크기가 가장 큰 노드를 알아볼 수 있게 마킹합니다. 여기서 마킹은, 구현에 따라 다르겠지만, 저는 가장 무거운 노드를 연결 리스트상에서 가장 앞으로 옮겼습니다. int형 배열에 별도로 직접 명시해도 되겠으나 구현상 가장 무거운 노드를 앞으로 옮기면 0번째 인덱스가 무거운 노드라는 것을 알 수 있기 때문에 편합니다.

 

그 후 두 번째 DFS를 돌면서 논리적인 체인 정보들을 입력하며, 이때 각 노드들이 세그먼트 트리에 대응될 인덱스도 세팅합니다.

논리적인 체인 정보들을 알아보기 전에, 해당 체인 정보들이 왜 언제 필요한지, 어떻게 응용되는지 그림을 통해 설명드리겠습니다.

 

 

[ 그림 설명 ]

  • 하나의 트리를 여러 개의 체인으로 구분한 그림이며, 각 체인의 색깔은 하나의 체인 그룹이라 생각하면 되고, 각 체인마다 세그먼트 트리의 대응되는 인덱스가 순서대로 있습니다.( 그림은 임의로 막 구분 한 것이라 heavy, Light를 따지지 않음 )
  • 여기서 노란색 3번 노드에서 파란색 2번 노드까지 경로에서 가장 큰 간선의 값을 찾아야 한다고 칩니다.
  • 빠르게 탐색하는 방법은, 노란색 (1,3)까지 최댓값 세그먼트 트리로 빠르게 구하고, 노란색 체인에서 빨간색 체인으로 점프해서 빨간색 (1,2) 사이 최댓값 구하고, 검정 노드도 구하고, 점프해가면서 주황, 파랑도 동일하게 구해줍니다.
  • 여기서 하나 알아야 할 것은, 세그먼트 트리에 저장되는 것은 간선 가중치 값이 저장됩니다. 그러나 노드는 A 노드와 B 노드로 2개인데 간선 1개를 저장해야 돼서 어디에 뭘 저장해야 할지 헷갈리는데, 자식 노드에 간선 정보를 저장하면 문제를 풀 수 있습니다. 즉 노란색 1번과 3번 범위를 세그먼트 트리로 max 값을 구한다는 것은, [3번에서 2번가는 간선 + [2번에서 1번가는 간선] + 노란색 1번에서 빨간색 2번으로 가는 간선] 을 구하는 것과 같습니다.
  • 이렇게 체인 간 이동을 최소로 빠르고, 정확하게 코드로 구현 하기 위해 몇 가지 정보가 필요합니다.
  • 1 ) 처음에 주어지는 트리 노드를 세그먼트 트리에 대응 시킬 인덱스 배열
  • 2 ) 현재 나의 체인에서, 다음 체인으로 바로 점프할 수 있도록 다음 체인의 노드 정보를 담는 배열
  • 3 ) 현재 나의 체인의 head 노드 번호를 담을 배열 : 예를 들어 노란색 4번 노드는 자기 체인의 head인 1번 노드의 번호를 갖고 있어야 합니다. 그래야 세그먼트 트리에 사용할 범위를 바로 알 수 있습니다.
  • 4 ) 해당 체인의 LEVEL 정보를 담을 배열 : LEVEL은 루트가 1이고, 나머지 체인들이 1씩 증가하는 형태를 띠며 LEVEL이 필요한 이유는, 트리 경로 탐색 시 LEVEL이 높은 곳에서부터 낮은 곳으로 올라와야 빠짐없이 정확한 경로 탐색이 가능하기 때문입니다.

 

위 와 같이 체인 정보들을 구했으면, 노드 두 개가 입력되었을 때 해당 노드 사이의 최대 간선을 쉽게 구할 수 있습니다.

 

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

 

[ 정답 코드 ]

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.StringTokenizer;

class Main{
	
	static class Node{
		int node, value;
		Node(int n, int v){
			node = n;
			value = v;
		}
	}
	
	static int N, Q;
	static int idx;			// 입력된 노드 번호를 세그먼트 트리의 인덱스로 전환할 때 증가될 인덱스 값
	static int [] tree;		// 세그먼트 트리
	static int [] treeIdx;		// HLD용 : 입력된 노드 번호 -> 세그먼트 트리 인덱스로 변환할 값
	static int [] chainHead;	// HLD용 : 각 체인 마다의 head 노드, 체인 당 첫번째 노드의 값은 자기 자신이됨
	static int [] chainLevel;	// HLD용 : 노드가 포함된 체인의 깊이(체인 레벨)
	static int [] chainParent;	// HLD용 : 내가 속한 체인의 head 노드의 바로 윗노드, 즉, 다음 체인으로 첨프할 때 도착할 노드
	static int [][] edge;		// 간선 정보 저장
	static ArrayList<Node>[] adNode;// 인접 리스트

	public static void main(String[] args)throws Exception{
		BufferedReader	br = new BufferedReader(new InputStreamReader(System.in));
		StringBuilder	sb = new StringBuilder();
		StringTokenizer	st;
		
		N			= Integer.parseInt(br.readLine());// 정점 수(2<=100,000)
		tree		= new int[N * 4];
		treeIdx		= new int[N + 1];
		chainHead	= new int[N + 1];
		chainLevel	= new int[N + 1];
		chainParent = new int[N + 1];
		edge		= 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 node1 = Integer.parseInt(st.nextToken());
			int node2 = Integer.parseInt(st.nextToken());
			int value = Integer.parseInt(st.nextToken());//비용 w(1<=1,000,000)
			adNode[node1].add(new Node(node2, value));
			adNode[node2].add(new Node(node1, value));
			edge[i] = new int[] {node1, node2};
		}
		
		// 이 함수에서는 단순히 HLD를 구현하기 위해 adNode에서 가장 heavy한 자식만 앞으로 옮기는 역할을 합니다.
		moveHeavyChildToFront(1, 0, new int[N + 1]);
		
		chainHead[1] = 1;	// 1번노드의 헤드는 자기 자신
		
		// 해당 함수에서 HLD를 위한 chain 정보들을 마킹하고, 세그먼트 트리에 간선 가중치를 업데이트 한다.
		setHLD(1, 0, 1);
		
		Q = Integer.parseInt(br.readLine());// 쿼리 개수 Q(1<=100,000)
		while(Q-->0)
		{
			st = new StringTokenizer(br.readLine());
			int op = Integer.parseInt(st.nextToken());
			if(op == 1)// 1 i c : i번 간선의 비용을 j로 바꾼다.
			{
				int i = Integer.parseInt(st.nextToken());
				int c = Integer.parseInt(st.nextToken());
				// 간선 정보는 자식 노드에 저장되어 있으므로, i번째 간선의 2개의 노드를 가져와서
				// 세그먼트 트리 노드의 번호로 변경했을 때 큰 값을 찾아 그 값으로 업데이트한다.( 큰값이 자식노드니까 )
				// setHLD 함수에서 treeIdx배열 세팅 하는 것을 보면, 자식 노드가 무조건 값이 더 크다.
				int segIdx = Math.max(treeIdx[ edge[i][0] ], treeIdx[ edge[i][1] ]);
				
				update(1, 1, N, segIdx, c);
				
				continue;
			}
			// op가 2인 경우 u, v사이의 간선 중 가중치가 가장 큰 값을 찾는다.
			int ans = 0;
			int u = Integer.parseInt(st.nextToken());
			int v = Integer.parseInt(st.nextToken());
			
			// 레벨이 같아질 때까지 레벨이 높은 것을 낮게 만들면서 높이를 맞춘다.
			if(chainLevel[u] > chainLevel[v])	// v노드가 레벨이 더크게 조정
			{
				int tmp = v;
				v = u;
				u = tmp;
			}
			while(chainLevel[u] != chainLevel[v])	// 레벨이 같아질 때 까지 반복
			{
				int head = chainHead[v];
				// 해당 체인의 head부터, 현재 v노드까지 가장 큰 값을 구해와 ans 갱신
				ans = Math.max(ans, query(1, 1, N, treeIdx[head], treeIdx[v]));
				v = chainParent[v];	// 레벨이 높은 v를 다음 체인까지 바로 점프 하도록 만든다.
			}
			
			// 레벨이 같아졌다면 같은 체인에 속하도록 두 노드의 레벨을 지속적으로 낮춘다.
			while(chainHead[v] != chainHead[u])
			{
				ans = Math.max(ans, query(1, 1, N, treeIdx[chainHead[u]], treeIdx[u]));
				ans = Math.max(ans, query(1, 1, N, treeIdx[chainHead[v]], treeIdx[v]));
				u = chainParent[u];	// 다음 체인으로 바로 점프
				v = chainParent[v];	// 다음 체인으로 바로 점프
			}
			
			// 위 연산들을 통해 결국 같은 체인에 속했다면,
            // 마지막으로 해당 두 노드간의 부모 자식 노드를 파악한 후, 그 사이의 가장 큰 값을 다시 비교한다.
			if(treeIdx[u] > treeIdx[v])	// v를 자식노드로 만든다.
			{
				int tmp = v;
				v = u;
				u = tmp;
			}
			
			// treeIdx[u] + 1을 하는 이유 : 
			// 세그먼트 트리에 값을 저장할 때, A가 부모노드이고, B가 자식노드일 때 간선의 가중치를 B위치에 저장했다. 
			// 그러므로 부모인 자기 자신은 빼주어야 정확한 값을 구해올 수 있다.
			ans = Math.max(ans, query(1, 1, N, treeIdx[u] + 1, treeIdx[v]));
			
			sb.append(ans).append('\n');
		}
		System.out.print(sb);
	}
	
	static void setHLD(int nowNode, int parentNode, int level) {
		treeIdx[nowNode] = ++idx;
		chainLevel[nowNode] = level;
		
		for(int i=0; i<adNode[nowNode].size(); i++)
		{
			Node now = adNode[nowNode].get(i);
			
			if(now.node == parentNode)
				continue;
			
			if(i == 0)	// heavy일 경우, 기존 데이터들 그대로 이어짐
			{
				chainHead[now.node] = chainHead[nowNode];
				chainParent[now.node] = chainParent[nowNode];
				setHLD(now.node, nowNode, level);	// heavy는 level이 그대로 이어진다.
			}
			else		// 새로운 체인 시작
			{
				chainHead[now.node] = now.node;			// 해당 체인의 시작은 자기자신
				chainParent[now.node] = nowNode;		// 새 체인 시작시 now.node는 nowNode로 바로 점프할 수 있게 저장
				setHLD(now.node, nowNode, level + 1);	// 새 체인 시작시 레벨 증가
			}
			// now객체의 모든 연산이 끝난 후, 해당 노드 번호를 이용해 세그먼트 트리에 간선 값을 업데이트한다.
			// 부모노드가 A, 자식노드 B라 할 때, 노드는 2개지만 간선은 하나기 때문에 간선 정보는 자식노드에 저장한다.
			// 그래서 현재 함수에서 nowNode(부모노드)에 저장하는것이 아닌, 자식 노드인 now.node에 간선 정보를 저장한다.
			update(1, 1, N, treeIdx[now.node], now.value);
		}
		
	}
	static void moveHeavyChildToFront(int nowNode, int parentNode, int[] size) {
		int heavySize = 0;
		int heavyIdx = 0;
		size[nowNode] = 1;
		for(int i=0; i<adNode[nowNode].size(); i++)
		{
			Node now = adNode[nowNode].get(i);
			
			if(now.node == parentNode)	// 부모노드면 스킵
				continue;
			
			moveHeavyChildToFront(now.node, nowNode, size);
			
			size[nowNode] += size[now.node];	// 자기 자식의 크기를 자기크기에 합산
			
			if(heavySize < size[now.node])	// 자식 노드 크기 중 큰 것의 idx를 갱신해나감
			{
				heavySize = size[now.node];
				heavyIdx = i;
			}
			
		}
		// 0번째 인덱스에 가장 무거은 노드를 놓아 추후 있을 연산들을 단순하게 한다.
		Collections.swap(adNode[nowNode], 0, heavyIdx);
	}
	static void update(int treeNode, int s, int e, int targetIdx, int value) {
		if(e < targetIdx || targetIdx < s)
			return;
		if(s == e)
		{
			tree[treeNode] = value;
			return;
		}
		int mid = (s + e) >> 1;
		
		update(treeNode << 1, s, mid, targetIdx, value);
		update(treeNode << 1 | 1, mid + 1, e, targetIdx, value);
		
		tree[treeNode] = Math.max(tree[treeNode << 1], tree[treeNode << 1 | 1]);
	}
	static int query(int treeNode, int s, int e, int left, int right) {
		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 Math.max(l,r);
	}
}

 

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

BOJ 백준 16453 Linhas de Metrô  (0) 2025.04.28
BOJ 백준 31234 대역폭 관리  (0) 2025.04.27
BOJ 백준 17033 Cow Land  (1) 2025.04.26
BOJ 백준 13309 트리  (0) 2025.04.26
BOJ 백준 13512 트리와 쿼리 3  (0) 2025.04.26

+ Recent posts