본문 바로가기

알고리즘 Algorithm/BOJ 백준 (초급~중급)

[BOJ 백준] 교수님은 기다리지 않는다(3830) Java

링크 : https://www.acmicpc.net/problem/3830

문제 설명 :

더보기

상근이는 매일 아침 실험실로 출근해서 샘플의 무게를 재는 일을 하고 있다. 상근이는 두 샘플을 고른 뒤, 저울을 이용해서 무게의 차이를 잰다.

교수님의 마음에 들기 위해서 매일 아침부터 무게를 재고 있지만, 가끔 교수님이 실험실에 들어와서 상근이에게 어떤 두 샘플의 무게의 차이를 물어보기도 한다. 이때, 상근이는 지금까지 잰 결과를 바탕으로 대답을 할 수도 있고, 못 할 수도 있다.

상근이는 결과를 출근한 첫 날부터 공책에 적어 두었다. 하지만, 엄청난 양의 무게가 적혀있기 때문에, 교수님의 질문에 재빨리 대답할 수가 없었다. 이런 상근이를 위해서 프로그램을 만들려고 한다.

상근이가 실험실에서 한 일이 순서대로 주어진다. 어떤 두 샘플의 무게의 차이를 구할 수 있는지 없는지를 알아내는 프로그램을 작성하시오.


입력 :

더보기

입력은 여러 개의 테스트 케이스로 이루어져 있다.

각 테스트 케이스의 첫째 줄에는 샘플의 종류의 개수 N (2 ≤ N ≤ 100,000)과 상근이가 실험실에서 한 일의 수 M (1 ≤ M ≤ 100,000)이 주어진다. 샘플은 1번부터 N번까지 번호가 매겨져 있다. 다음 M개 줄에는 상근이가 실험실에서 한 일이 주어진다.

상근이가 무게를 쟀다면, ! a b w와 같은 형식으로 주어진다. 이 뜻은 b가 a보다 w그램 무겁다는 뜻이다. (a ≠ b) w는 1,000,000을 넘지 않는 음이 아닌 정수이다. 모든 측정은 정확하고, 일관성을 유지한다.

교수님의 질문은 ? a b와 같은 형식으로 주어진다. 이 뜻은 b가 a보다 얼마나 무거운지를 출력하라는 뜻이다.

마지막 줄에는 0이 두 개 주어진다.


출력 :

더보기

교수님의 질문 (? a b)이 입력으로 들어올 때 마다, 지금까지 측정한 결과를 바탕으로 a와 b의 무게 차이를 계산할 수 있다면, b가 a보다 얼마나 무거운지를 출력한다. 무게의 차이의 절댓값이 1,000,000을 넘지 않는다. 만약, 측정한 결과를 바탕으로 무게의 차이를 계산할 수 없다면, "UNKNOWN"을 출력한다.


예제 입력 :

더보기

2 2
! 1 2 1
? 1 2
2 2
! 1 2 1
? 2 1
4 7
! 1 2 100
? 2 3
! 2 3 100
? 2 3
? 1 3
! 4 3 150
? 4 1
0 0


예제 출력 :

더보기

1
-1
UNKNOWN
100
200
-50


접근법 :

1) 어떻게 풀 것인가?
N이 10만으로 적지 않은 편이고, 실시간으로 간선이 추가되며 샘플들의 관계가 변화한다.
샘플의 관계가 고정되어있다면 LCA로 간선간의 거리를 구하는데 용이할 것 같으나,

간선이 추가될 경우 재구성이 필요해 시간초과가 예상된다.

 

다시 문제로 돌아와 답을 구하려면 필요한 것은 2개이다.

① 2개 정점의 거리 차이를 측정할 수 있는가?

② 그 거리가 얼마인가?

 

①은 비교적 간단하다.

union find 를 사용한다면 2개 정점이 같은 그룹인지 확인하는 방식으로 구할 수 있다.

 

②의 경우 기존 union find를 활용해서 구할 수 있다.

 

예제를 그림으로 표현해보자.

먼저 dist[ i ] 배열을 만들어 루트 노드와 i 와의 거리를 배열로 만든다고 했을때,

 

위 그래프에서 루트 노드가 3이라고 가정하면, 

dist[ 1 ] = 200;        parent[1]  = 3;

dist[ 2 ] = 100;        parent[1]  = 3;

dist[ 3 ] = 0;           parent[1]  = 3;

dist[ 4 ] = 150;        parent[1]  = 3;

이라고 한다면, 1~4 같은 그룹내의 모든 노드간의 상대 거리를 구할 수 있다. 

 

그러기위해 find 함수는

1. dist[ id ] 에 자신의 바로 앞선 dist[ parent[id] ] 를 더해주고, 

2. return 을 할때 parent[ id ] 를 갱신시켜주면 된다.

	static int find(int id) {
		// 1. root인 경우 기존 union find 동일
		if (parent[id] == id)
			return id;
		// 2. root가 아닌 경우 root와의 거리를 구해서 갱신
		int pId = find(parent[id]);
		dist[id] += dist[parent[id]];
		return parent[id] = pId;
	}

 

union 함수는 아래 그림을 코드로 구현하면 다음과 같다.

 

pa + dist[a] = a;

a + w = b;

b = pb + dist[b];

 

pa + dist[a] = b - w;

pa + dist[a] = pb + dist[b] - w;

 

pa + (dist[a] – dist[b] + w) = pb;

 

	static void union(int a, int b, long diff) {
		int pa = find(a);
		int pb = find(b);

		// 이미 같은 그룹이면 거리 계산이 되어있으므로 종료
		if (pa == pb)
			return;

		// parent를 변경하고, 무게 차이를 갱신
		dist[pb] = dist[a] - dist[b] + diff;
		parent[pb] = pa;

		return;
	}

 

전체 코드는 아래 참조

 

2) 시간복잡도
유니온파인드의 일반적인 시간복잡도 O(logN)이라 가정하면 O(M logN) 이다.

테스트 케이스의 개수가 공개되어있지는 않으나 통과에는 무리가 없다.
(Java 기준 - 1,632ms)

3) 공간복잡도
M이 100만으로 크지 않아 고려하지 않음

4) 풀면서 놓쳤던점
특별히 없음.

5) 이 문제를 통해 얻어갈 것
유니온 파인드 활용법

Java 코드 :

import java.io.*;
import java.util.*;

// 3830 교수님은 기다리지 않는다
public class Main {

	static int N, M;
	static int[] parent;
	static long[] dist;
	static String type;
	static int a, b, w;

	public static void main(String[] args) throws Exception {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

		StringBuilder sb = new StringBuilder();

		StringTokenizer st;
		st = new StringTokenizer(br.readLine());
		N = Integer.parseInt(st.nextToken());
		M = Integer.parseInt(st.nextToken());

		// N, M이 0, 0 이면 종료
		for (; N != 0 && M != 0;) {
			// Union Find를 위한 parent 초기화
			parent = new int[N + 1];
			for (int i = 1; i <= N; i++) {
				parent[i] = i;
			}
			dist = new long[N + 1];

			for (int i = 1; i <= M; i++) {
				st = new StringTokenizer(br.readLine());
				type = st.nextToken();

				// 1. 무게를 쟀을 경우 - 판단 가능한 그룹으로 union
				if (type.equals("!")) {
					a = Integer.parseInt(st.nextToken());
					b = Integer.parseInt(st.nextToken());
					w = Integer.parseInt(st.nextToken());

					union(a, b, w);
				}
				// 2. 무게를 판단하는 경우
				else {
					a = Integer.parseInt(st.nextToken());
					b = Integer.parseInt(st.nextToken());

					// 2-1. 무게를 판단할수 없는 경우 UNKNOWN
					if (find(a) != find(b)) {
						sb.append("UNKNOWN\n");
					}
					// 2-2. 무게를 판단할 수 있는 경우
					else {
						sb.append((dist[b] - dist[a]) + "\n");
					}
				}
			}

			// 다음 테스트 케이스의 N, M 입력받기
			st = new StringTokenizer(br.readLine());
			N = Integer.parseInt(st.nextToken());
			M = Integer.parseInt(st.nextToken());
		}

		bw.write(sb.toString());

		bw.flush();
		bw.close();
		br.close();
	}

	static int find(int id) {
		// 1. root인 경우 기존 union find 동일
		if (parent[id] == id)
			return id;
		// 2. root가 아닌 경우 root와의 거리를 구해서 갱신
		int pId = find(parent[id]);
		dist[id] += dist[parent[id]];
		return parent[id] = pId;
	}

	static void union(int a, int b, long diff) {
		int pa = find(a);
		int pb = find(b);

		// 이미 같은 그룹이면 거리 계산이 되어있으므로 종료
		if (pa == pb)
			return;

		// parent를 변경하고, 무게 차이를 갱신
		dist[pb] = dist[a] - dist[b] + diff;
		parent[pb] = pa;

		return;
	}
}

 

 

+ Union Find를 몰랐을때 구현한 

C 코드 : 

// 3830 교수님은 기다리지 않는다 - 영준이의 무게측정 유사문제
#if 1
#pragma warning (disable : 4996)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int N, M;
struct st {
	int value, flag;
};

int main(void)
{
	register int i, j;
	int tmp1, tmp2, judge, flag, tmp_flag, tmp_value;
	char tmp;
	struct st *p;
	flag = 1;
	for (; ; )
	{
		scanf("%d %d", &N, &M);
		if (!N) break;
		p = (struct st *) calloc(N+2, sizeof(struct st));
		for (i = 1; i <= M; i++)
		{
			scanf(" %c", &tmp);
			if (tmp == '!')
			{
				scanf("%d %d %d", &tmp1, &tmp2, &judge);
				if (p[tmp1].flag == 0 && p[tmp2].flag == 0)
				{
					p[tmp1].value = p[tmp1].flag = p[tmp2].flag = flag;
					p[tmp2].value = flag + judge;
					flag++;
				}
				else if (p[tmp1].flag && !p[tmp2].flag)
				{
					p[tmp2].value = p[tmp1].value + judge;
					p[tmp2].flag = p[tmp1].flag;
				}
				else if (p[tmp2].flag && !p[tmp1].flag)
				{
					p[tmp1].value = p[tmp2].value - judge;
					p[tmp1].flag = p[tmp2].flag;
				}
				else if (p[tmp1].flag == p[tmp2].flag) continue;
				else
				{
					tmp_flag = p[tmp2].flag;
					tmp_value = p[tmp2].value;
					p[tmp2].value = p[tmp1].value + judge; 
					p[tmp2].flag = p[tmp1].flag;
					for (j = 1; j <= N; j++)
					{
						if (p[j].flag == tmp_flag)
						{
							p[j].value = p[j].value - tmp_value + p[tmp2].value;
							p[j].flag = p[tmp2].flag;
						}
					}
				}
			}
			else
			{
				scanf("%d %d", &tmp1, &tmp2);
				if (p[tmp1].flag == p[tmp2].flag && p[tmp1].flag)
				{
					printf("%d\n", p[tmp2].value - p[tmp1].value);
				}
				else printf("UNKNOWN\n");
			}
		}
	}
	return 0;
}
#endif

 

반응형