[문제][알고스팟] EDITORWARS (에디터 전쟁)

2018. 5. 27. 19:23[개발] 지식/알고리즘 문제풀이

문제 정보

문제

에디터 전쟁은 가장 유명한 자유 소프트웨어 텍스트 편집기인 vi와 Emacs 중 어느 쪽이 더 우월한가를 놓고 인터넷에서 자주 벌어지는 논쟁을 말합니다. 이 논쟁에 참여하는 사람들은 서로 자신이 사용하는 편집기의 장점을 찬양하고 (“vi는 동작도 빠르고, 빠른 편집을 가능하게 한다고”, “Emacs는 LISP을 통해 확장 가능하다고”) 다른 편집기를 헐뜯곤 (“vi-vi-vi는 666이잖아! vi는 악마의 편집기야”, “Emacs는 좋은 운영 체제지. 좋은 편집기가 없는 것만 빼면 완벽해”) 합니다.

모든 회원들이 vi 혹은 Emacs를 사용하는 프로그래밍 동호회에서 연말 파티를 개최하려 합니다. 서로 다른 편집기를 사용하는 사람들이 파티에 함께 참가하면 싸움이 나기 때문에 vi를 사용하는 사람들만 오는 파티, Emacs를 사용하는 사람들만 오는 파티를 따로 하기로 했습니다. 이를 위해 지금까지 모든 회원들이 쓴 댓글을 모아 이들을 두 종류로 분류했습니다.

  1. 상호 인정: 이 유형의 댓글은 댓글을 쓴 사람과 원글을 쓴 사람이 같은 편집기를 쓴다는 사실을 의미합니다. 예로 “아이고 이런 편집기를 쓰시다니 뭘 아는 분이네” 혹은 “역시 편집기는 xxx가 짱이죠” 등이 있겠지요.
  2. 상호 비방: 이 유형의 댓글은 댓글을 쓴 사람과 원글을 쓴 사람이 다른 편집기를 쓴다는 사실을 의미합니다. 예로 “그런 편집기 쓰면 손가락에 마비가 올 것 같네요” 혹은 “훠어이 악마의 편집기는 물러가라” 등이 있겠지요.

이것만으로는 물론 각 사용자가 어떤 편집기를 쓰는지는 알 수 없지만, 우선 서둘러 장소를 예약해야 하기 때문에 이 정보만으로 파티에 올 수 있는 최대 인원을 알아야 합니다. 댓글 정보가 주어질 때 이 댓글 정보 중 모순되는 것은 없는지 확인하고, 모순되는 것이 없을 때 한 파티의 가능한 최대 인원을 계산하는 프로그램을 작성하세요.

입력

입력의 첫 줄에는 테스트 케이스의 수 C (1≤C≤50)가 주어집니다. 각 테스트 케이스의 첫 줄에는 회원의 수 N (1≤N≤10000)과 분석한 댓글의 개수 M (1≤M≤100000)이 주어집니다. 각 회원은 0에서 N - 1 범위의 숫자로 표현됩니다.

그 후 M줄에 하나씩 각 댓글의 정보가 주어집니다. 각 댓글은 상호 인정, 혹은 상호 비방 댓글입니다. 상호 인정 댓글은 “ACK a b”(0≤a, b<N) 형태로 주어지며 상호 비방 댓글은 “DIS a b” 형태로 주어집니다.

출력

각 테스트 케이스마다 한 줄을 출력합니다.

  • ᆞ댓글에 주어진 정보 중 모순이 없는 경우 한 파티에 올 수 있는 사람의 최대 수를 “MAX PARTY SIZE IS x”의 형태로 출력합니다.
  • ᆞ댓글들에 주어진 정보 중 모순이 있는 경우, 모순이 처음으로 발생하는 댓글이 몇 번인지를 “CONTRADICTION AT i” 형태로 출력합니다. 댓글의 번호는 입력에 주어진 순서대로 1부터 시작한다고 합시다.

예제 입력

4
4 4
ACK 0 1
ACK 1 2
DIS 1 3
ACK 2 0
100 4
ACK 0 1
ACK 1 2
ACK 2 3
ACK 3 4
3 3
ACK 0 1
ACK 1 2
DIS 2 0
8 6
DIS 0 1
ACK 1 2
ACK 1 4
DIS 4 3
DIS 5 6
ACK 5 7

예제 출력

MAX PARTY SIZE IS 3
MAX PARTY SIZE IS 100
CONTRADICTION AT 3
MAX PARTY SIZE IS 5




난이도 '중'이고, Union-Find 문제라는걸 알면서도 헤맸다.
그리고 못풀고 답안을 보며 이해했다. ㅜㅜ
언제쯤 혼자 풀 수 있을까

아래는 삽질한 나의 첫번째 코드이다.
Union-Find를 적용했지만, 상응되는 적대집합과의 쌍 관리를 하지 못했다. 
(해야한다는 생각이 들었지만 어떻게 해야할지 감이 안왔다.)

Relation이라는 클래스를 만들어 입력값을 관리했고, 이를 다시 돌면서 Union-Find를 돌렸다.
지금보면 굳이 새로운 클래스를 만들 필요는 없었지만, 당시에는 코딩과 동시에 생각하는 과정이었으므로 그럴 수 있었겠다 싶다.
이 코드에서 해결하지 못한건 위에서 말했듯 적대 집합과의 쌍관리를 하지 못한것이다. 그리고 그로인해 각각의 컴포넌트 별 최대 집합 크기를 구하지 못했다.

내 첫번째 코드

package apss;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.StringTokenizer;

// 알고스팟
// ID : EDITORWARS
// 알고리즘 : Union-Find
// 난이도 : 중
// 상태 : 푸는중

public class EditorWars {

	public static class Relation {
		public int idx;
		public int u;
		public int v;
		public int type;

		public Relation(int idx, int u, int v, int type) {
			this.idx = idx;
			this.u = u;
			this.v = v;
			this.type = type;
		}

	}

	public static int N, M;
	public static int[] parent, rank;
	public static Relation relation[];
	public static boolean isShow[];

	public static void main(String[] args) throws NumberFormatException, IOException {
		// TODO Auto-generated method stub

		System.setIn(new FileInputStream("D:\\sample_input.txt"));
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

		int T = Integer.parseInt(br.readLine());

		for (int tc = 1; tc <= T; tc++) {

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

			parent = new int[N];
			rank = new int[N];
			relation = new Relation[M];
			isShow = new boolean[N];

			UnionFind();

			// TODO String 타입의 크기비교가 가능한지 알아볼것
			for (int i = 0; i < M; i++) {
				st = new StringTokenizer(br.readLine());
				int type = "ACK".equals(st.nextToken()) ? 0 : 1;
				int u = Integer.parseInt(st.nextToken());
				int v = Integer.parseInt(st.nextToken());
				
				isShow[u] =true;
				isShow[v] = true;

				relation[i] = new Relation(i+1, u, v, type);
			}

			// ACK -> DIS 순으로 오름차순 정렬
			Arrays.sort(relation, new Comparator<Relation>() {

				@Override
				public int compare(Relation a, Relation b) {
					// TODO Auto-generated method stub
					return a.type - b.type;
				}

			});

			boolean isPossible = true;
			int impossibleIdx = -1;
			for (int i = 0; i < relation.length; i++) {

				if (relation[i].type == 0) { // 같은 그룹일때 합친다

					merge(relation[i].u, relation[i].v);

				} else { // 이제부터 다른그룹이여야 한다

					int uParent = find(relation[i].u);
					int vParent = find(relation[i].v);

					// 다른그룹이여야하는데, 그룹이 같다?
					if (uParent == vParent) { // 모순 -> 빠져나간다
						isPossible = false;
						impossibleIdx = relation[i].idx;
						break;
					}

				}
			}

			bw.flush();
			if (!isPossible) { // 모순일경우
				bw.write("CONTRADICTION AT " + impossibleIdx + "\n");
			} else { // 가능한 최대 인원은?
				
				int showCnt = 0;
				int cnt[] = new int[N];
				for (int i = 0; i < N; i++) {
					cnt[parent[i]]++;
					if(isShow[i]) {
						showCnt++;
					}
				}
				
				int max = 0;
				for(int i=0; i<cnt.length; i++) {
					if(cnt[i] > 1) {max += cnt[i];}
//					max = Math.max(max, cnt[i]);
				}

				bw.write("MAX PARTY SIZE IS  " + (max+(N-showCnt)) + "\n");
			}

		}
		bw.close();

	}

	public static void UnionFind() {
		for (int i = 0; i < N; i++) {
			parent[i] = i;
		}
	}

	public static int find(int u) {
		if (u == parent[u])
			return u;
		return find(parent[u]);
	}

	public static boolean merge(int u, int v) {
		u = find(u);
		v = find(v);

		if (u == v)
			return false;

		if (rank[u] > rank[v]) {
			parent[v] = u;
		} else {
			parent[u] = v;
		}

		if (rank[u] == rank[v])
			++rank[v];

		return true;

	}

}


문제에서는 결국 두 개의 집합밖에 나올 수 없다.
핵심은 호의적인 개체들을 하나의 집합으로 Union(이하 merge) 하고, 동시에 적대적인 개체들을 merge한다.

모든 개체들을 merge 하면 결국 1개의 집합과 그 집합의 적대집합으로 나누어질것이다. 혹은 적대집합이 없을 수도 있다(공집합).
어쨌든 쌍으로 이루어진 각각의 컴포넌트에 대해(컴포넌트는 루트로 분별한다) 둘 중 더 큰 사이즈를 합치면 가능한 최대 집합의 크기가 나오게 된다. 중복된 쌍을 계산함을 방지하기 위해서 for문 안에 enemy의 번호가 현재 탐색중인 개체(node) 번호보다 큰 경우는 패스함에 유의한다.

정답 코드

package apss;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Arrays;
import java.util.StringTokenizer;

// 알고스팟
// ID : EDITORWARS
// 알고리즘 : Union-Find
// 난이도 : 중
// 상태 : 풀이완료(답안)

public class EditorWars_Solution {

	public static int N, M;
	public static int[] parent, rank, size, enemies;

	public static void main(String[] args) throws NumberFormatException, IOException {
		// TODO Auto-generated method stub

		System.setIn(new FileInputStream("C://dev/sample_input.txt"));
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

		int T = Integer.parseInt(br.readLine());

		for (int tc = 1; tc <= T; tc++) {

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

			parent = new int[N];
			rank = new int[N];
			size = new int[N];
			enemies = new int[N];
			
			// 초기화
			UnionFind();

			boolean isPossible = true;
			int impsIdx = -1;
			// TODO String 타입의 크기비교가 가능한지 알아볼것
			for (int i = 0; i < M; i++) {
				st = new StringTokenizer(br.readLine());
				int type = "ACK".equals(st.nextToken()) ? 0 : 1;
				int u = Integer.parseInt(st.nextToken());
				int v = Integer.parseInt(st.nextToken());
				
				if(!isPossible){
					continue;
				}
				
				if(type == 0){	// ACK
					isPossible = ack(u,v);
				}else{
					isPossible = dis(u,v);
				}
				
				if(!isPossible){
					impsIdx = i+1;
				}
			}
			
			bw.flush();
			if(isPossible){
				bw.write("MAX PARTY SIZE IS " + maxParty() + "\n");
			}else{
				bw.write("CONTRADICTION AT " + impsIdx + "\n");
			}
			
		}
		bw.close();

	}

	public static void UnionFind() {
		
		Arrays.fill(enemies, -1);
		Arrays.fill(size, 1);
		
		for (int i = 0; i < N; i++) {
			parent[i] = i;
		}
	}

	public static int find(int u) {
		if (u == parent[u])
			return u;
		return find(parent[u]);
	}

	public static int merge(int u, int v) {
		
		int rtVal = 0;
		
		// u나 v가 공집합인 경우 나머지 하나를 반환한다.
		if(u == -1 || v == -1) return Math.max(u,v);
		
		u = find(u);
		v = find(v);

		// 이미 둘이 같은 트리에 속한 경우
		if (u == v)
			return u;

		if (rank[u] > rank[v]) { // v를 u에 편입시킨다
			parent[v] = u;
			size[u] += size[v];
			rtVal = u;
		} else { // u를 v에 편입시킨다
			parent[u] = v;
			size[v] += size[u];
			rtVal = v;
		}

		if (rank[u] == rank[v])
			++rank[v];

		return rtVal;

	}
	
	public static boolean dis(int u, int v){
		
		u = find(u);
		v = find(v);
		
		// 같은 집합에 속해 있으면 모순!
		if(u == v) return false;
		
		// 적의 적은 나의 동지
		int a = merge(u, enemies[v]);
		int b = merge(v, enemies[u]);
		
		enemies[a] = b;
		enemies[b] = a;
		
		return true;
		
	}
	
	public static boolean ack(int u, int v){
		
		u = find(u);
		v = find(v);
		
		// 두 집합이 서로 적대 관계라면 모순!
		if(enemies[u] == v) return false;
		
		// 동지의 적은 나의 적
		int a = merge(u, v);
		int b = merge(enemies[u], enemies[v]);
		enemies[a] = b;
		if(b != -1) enemies[b] = a;
		
		return true;
		
	}
	
	public static int maxParty(){
		int ret = 0;
		for(int node=0; node<N; node++){
			if(parent[node] == node){ // 루트일경우
				int enemy = enemies[node];
				
				// 같은 모임쌍을 두 번 세지 않기 위해, enemy < node인 경우만 센다.
				// enemy == -1인 경우도 정확히 한 번씩 세게 된다.
				if(enemy > node) continue;
				
				int mySize = size[node];
				int enemySize = (enemy == -1 ? 0 : size[enemy]);
				
				// 두 집합 중 큰 집합을 더한다.
				ret += Math.max(mySize, enemySize);
				
			}
		}
		return ret;
	}

}



회고

1. 끝까지 고민하지 못했다. 해결방안이 무엇인지 알고있었음에도 그걸 해결하기를 겁냈다. 또는 귀찮아했다.
2. 항상 입력값을 끝까지 받아야 다음 입력값을 제대로 받을 수 있음을 잊지 말아야한다. 이런 사소한 실수 때문에 많은 시간을 허비했다.
(break를 사용해 버렸다. 입력을 끝까지 안받았는데도.. 결국 런타임 에러의 원인이 뭔지 계속 생각해야했다)
3. 답안 풀이를 보면 항상 몇가지 조건을 만들고 그 조건에 따라 프로그래밍하는 깔끔함을 볼 수 있다. 물론 책이니까 결국 풀어버린 코드를 가지고 깔끔하게 정리할 수 있었겠지만, 실제로 문제를 풀기시작하면 저런 조건을 만드는게 힘들다. 직관적으로 와닿지 않는다고 할까? 이건 아직 내가 느끼지 못한 영역일지, 원래 그렇게 될 수 없는건지 항상 의문이다.
4. 회고과정이 필요하다는 생각이 들었다. 그래서 이런 섹션을 이번부터 추가했다. 회고하지 않으면 똑같은 실수를 반복할것 같아서이다.

<