알고리즘/위상정렬

[BOJ] 백준 2623 - 음악프로그램 풀이

송승현(SSH) 2023. 3. 8. 10:48

1. 문제

https://www.acmicpc.net/problem/2623

 

2623번: 음악프로그램

첫째 줄에는 가수의 수 N과 보조 PD의 수 M이 주어진다. 가수는 번호 1, 2,…,N 으로 표시한다. 둘째 줄부터 각 보조 PD가 정한 순서들이 한 줄에 하나씩 나온다. 각 줄의 맨 앞에는 보조 PD가 담당한

www.acmicpc.net



2. 위상 정렬

  • 비순환 방향 그래프에서 정점을 선형으로 정렬한 것을 의미
  • 위상 정렬은 순서가 정해져 있는 작업들을 차례대로 수행해야 할 때, 그 순서를 결정해주는 알고리즘
  • 위상 정렬이 성립하기 위해서는 반드시 그래프의 순환이 존재하지 않아야 한다.
  • 즉, 그래프가 비순환 방향 그래프(Directed Aclic Graph)여야 한다.


3. 위상 정렬 구현 방법(BFS)

  • 차수
    • 정점에 연결된 간선 수
  • 진출 차수
    • 자신의 정점에서 타 정점으로 나가는 간선 수
  • 진입 차수
    • 타 정점에서 자신의 정점으로 들어오는 간선 수
  • 구현 방식
    • 진입 차수가 0인 노드(시작점)를 큐에 모두 넣는다.
      1. 진입 차수가 0이라는 얘기는 나를 가리키는 정점이 없다는 얘기!!!
      2. BFS 돌리기 전에 처음부터 진입 차수가 0인 노드가 없다면 처음부터 사이클이 생성되어 있다는 것.
    • 큐에서 진입 차수가 0인 노드를 꺼내어 자신과 인접한 노드의 간선을 제거한다.
    • 간선 제거 후 진입 차수가 0이 된 노드를 큐에 넣는다.
    • 큐가 공백이 될 때까지 2번과 3번의 과정을 반복한다.
  • 노드 제거 전

  • 노드 제거 후



4. 풀이

  • 주요 자료구조
    • List<Integer>[] list : 인접 리스트
    • List<Integer> singList : 출연 순서 리스트
    • int[] inDegree : 정점들의 진입 차수를 담은 배열
  • 입력으로 주어지는 PD들이 정한 가수의 순서들을 기반으로 인접리스트를 만들고 inDegree와 signList를 초기화한다.
  • 입력으로 인접리스트를 초기화하고 진입 차수를 계산하여 inDegree를 초기화한다.
    • 인접리스트는 주어지는 입력을 2개씩 잡아서 from, to 노드라고 생각하고 입력한다
    • 예를 들어 1, 4, 3의 경우 1 -> 4, 4 -> 3의 식으로 생각하고 진입 차수는 inDegree[1] += 0, inDegree[4] += 1, inDegree[3] += 0 이런 식으로 계산하며 모든 입력에 대해 계산한다.
  • 이후 위상 정렬을 위한 BFS 함수인 orderSingerBFS()를 실행한다.
  • 함수에서는 위의 구현 방식에 맞게 로직을 구성한다.
    • 처음에 진입 차수가 0인 노드를 큐에 넣는다.
    • 큐가 빌 때까지 인접리스트를 확인하고 진입 차수를 하나씩 줄여가면서 확인한다.
    • 큐에서 꺼냈다는 것은 진입 차수가 0인 노드라는 것이고 이는 정답 순서에 들어가는 노드이다. 따라서 poll 시킨 노드를 singList에 넣는다.
    • 인접리스트를 만들었으므로 현재 탐색하는 정점(큐에서 꺼낸 정점)과 연결된 정점이 있고 degree를 -1 시켰을 때 0이라면 큐에 넣을 수 있으므로 큐에 넣는다.
  • 탐색을 마쳤다면 이제 사이클이 만들어졌는지 확인해야한다.
    • 우리가 정렬한 노드들은 singList에 들어가 있다.
    • 만약 사이클이 발생하지 않고 모든 정점들을 탐색하여 정렬했다면 singList에는 처음 입력을 받은 정점의 개수만큼 노드가 들어가 있을 것이다.
    • 따라서 처음 입력받은 노드의 개수와 singList에 들어가있는 원소의 개수가 같다면 사이클이 발생하지 않았다는 것이므로 이 리스트를 그대로 출력한다.
    • 만약 이 수가 다르다면 0을 출력한다.
  • 이를 그림으로 간단하게 그리면 다음과 같다.



5. 소스코드

import java.io.*;
import java.util.*;
import java.lang.*;
import java.security.Signature;
import java.sql.Array;

public class Main {
	
	static List<Integer>[] list; // 인접리스트
	static int N,M; // 가수의 수 : N, 보조 PD 수 : M
	static List<Integer> singList; // 출연 순서 리스트
	static int[] inDegree; // 정점들의 진입 차수를 담은 배열
	
	public static void main(String[] args) throws IOException {
	    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
	    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
	    
	    StringTokenizer st = new StringTokenizer(br.readLine());
	    N = Integer.parseInt(st.nextToken());
	    M = Integer.parseInt(st.nextToken());
	    
	    // 인접 리스트 초기화
	    list = new ArrayList[N + 1];
	    for (int i = 1; i <= N; i++) {
	    	list[i] = new ArrayList<Integer>();
	    }
	    
	    // 진입차수 입력 배열 초기화
	    inDegree = new int[N + 1];
	    
	    // singList 초기화
	    singList = new ArrayList<Integer>();
	    
	    // pd들이 정한 순서 입력
	    for (int i = 0; i < M; i++) {
	    	st = new StringTokenizer(br.readLine());
	    	int cnt = Integer.parseInt(st.nextToken());
	    	
	    	// 입력을 위한 임시 배열
	    	ArrayList<Integer> temp = new ArrayList<>();
	    	for (int j = 0; j < cnt; j++) {
	    		temp.add(Integer.parseInt(st.nextToken()));
	    	}
	    	
	    	for (int j = 0; j < temp.size() - 1; j++) {
	    		list[temp.get(j)].add(temp.get(j + 1));
	    		inDegree[temp.get(j + 1)] += 1;
	    	}
	    }
	    
	    // 위상 정렬을 위한 BFS함수
	    orderSingerBFS();
	    
	    // 위상 정렬 후 singList에 들어있는 노드의 개수가 전체 가수의 수와 같다면 정렬이 된 것
	    if (singList.size() == N) {
	    	for (int i = 0; i < singList.size(); i++) {
	    		bw.write(singList.get(i) + "\n");
	    	}
	    } else {
	    	bw.write("0");
	    }
	    
	    bw.flush();
	    bw.close();
	    br.close();
	}
	
	// 위상 정렬을 위한 BFS함수
	private static void orderSingerBFS() {
		Queue<Integer> q = new ArrayDeque<Integer>();
		
		// 처음에 진입 차수가 0인 노드를 q에 넣는다.
		for (int i = 1; i <= N; i++) {
			if (inDegree[i] == 0) {
				q.offer(i);
			}
		}
		
		// 큐가 빌 때까지 인접리스트를 확인하고 진입 차수를 하나씩 줄여가면서 확인한다.
		while (!q.isEmpty()) {
			// 큐에서 꺼냈다는 것은 진입 차수가 0인 노드라는 것이고 이는 정답 순서에 들어가는 노드이다.
			int poll = q.poll();
			singList.add(poll);
			
			// 인접한 정점을 확인
			for (int i = 0; i < list[poll].size(); i++) {
				// 연결된 정점이 있고 degree를 -1 시켰을 때 0이라면
				if (list[poll].get(i) != null && --inDegree[list[poll].get(i)] == 0) {
					q.add(list[poll].get(i));
				}
			}
		}
	}
}



6. 배운점

  • 위상 정렬에 대한 기본적인 개념을 학습했다.
  • 결국 알고리즘을 잘한다는 것은 알고리즘의 개념과 작성 방법을 많이 알고, 문제 상황이 주어졌을 때 이런 알고리즘을 사용하면 효율적이겠다는 것을 빠르게 떠올리는 것이 전부인것 같다.(물론 반례도 존재하지만....)


7. 점수

70/100