
주어진 방향 그래프에서 순서를 찾는 문제이다. 한 작업이 끝나야 다른 작업을 시작할 수 있다는 의존성에 대한 조건이 존재하며 사이클이 없는 방향 그래프(DAG, Directed Acyclic Graph) 형태로 주어져 있다.
SWEA상에서 높은 난이도로 책정되어 있지만 위상정렬 알고리즘에 대해 알고 있으면 쉽게 구현이 가능하다.
위상정렬 알고리즘의 프로세스는 다음과 같다.
1. 정의 :
2. 탐색 :
3. 처리 :
4. 반복 :
이러한 접근 방식은 칸 알고리즘(Kahn's Algorithm)으로 알려져 있으며 BFS와 유사한 방식으로 동작한다.
List<Integer>[] adjList): 그래프의 연결 관계를 표현한다. adjList[u]는 작업 u가 완료되어야 시작할 수 있는 후속 작업들의 리스트를 저장한다.int[] inDegree): 각 작업에 대해 필요한 선행 작업 수를 저장한다. inDegree[v]는 작업 v를 시작하기 전에 완료되어야 하는 작업의 개수이다.Queue<Integer>): 현재 즉시 수행 가능한 작업들(진입 차수가 0인 정점)을 보관하는 데 사용된다. FIFO(선입선출) 구조가 여러 유효한 순서 중 자연스럽게 하나를 선택하게 해준다.V+1 크기의 인접 리스트와 진입 차수 배열을 생성한다. (정점 번호가 1부터 시작하므로)E개의 간선 정보를 입력받아 u v에 대해, adjList[u]에 v를 추가하고 inDegree[v]의 값을 1 증가시킨다.1부터 V까지 모든 정점을 순회하며 inDegree가 0인 정점을 큐에 모두 삽입(offer)한다. 이들이 선행 작업 없이 가장 먼저 시작할 수 있는 작업들이다.cur를 하나 꺼내(poll) 결과 리스트에 추가한다.adjList[cur]를 통해 cur 작업이 끝나야 시작할 수 있는 모든 후속 작업 next를 확인한다.next 작업의 진입 차수(inDegree[next])를 1 감소시킨다.inDegree[next]가 0이 되면 next 작업의 모든 선행 작업이 완료되었다는 의미이므로 next를 큐에 삽입한다.E개를 한 번씩 순회하므로 V개를 한 번씩 순회하므로 package SWEA.D6;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
/**
* 작업순서 (위상정렬)
* V 개 작업 어떤 작업은 특정 작업이 끝나여 시작 가능 -> 선행 관계
*
* 작업과 선행관계 그래프
* 선행 관계는 방향성을 가진 간선으로 표현
* 사이클 존재 x
*
* 일을 끝낼 수 있는 작업 순서 찾기
*
* 모든 정점의 진입차수 기록
* 0부터 단계별 bfs 탐색
*/
public class D6_1267 {
static int V, E;
static List<List<Integer>> graph;
static int[] indegree;
static int[] result;
static StringBuilder sb;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
for (int tc = 1; tc <= 10; tc++) {
StringTokenizer st = new StringTokenizer(br.readLine());
V = Integer.parseInt(st.nextToken());
E = Integer.parseInt(st.nextToken());
indegree = new int[V + 1];
graph = new ArrayList<>();
// 그래프 초기화
for (int i = 0; i <= V; i++) {
graph.add(new ArrayList<>());
}
st = new StringTokenizer(br.readLine());
for (int i = 0; i < E; i++) {
int s = Integer.parseInt(st.nextToken());
int e = Integer.parseInt(st.nextToken());
// 그래프 연결 및 진입차수 기록
graph.get(s).add(e);
indegree[e]++;
}
sb = new StringBuilder();
sb.append("#").append(tc).append(" ");
// 단계별 bfs
bfs();
System.out.println(sb.toString());
}
}
public static void bfs() {
ArrayDeque<Integer> queue = new ArrayDeque<>();
int t = 1;
// 진입차수가 0개인 노드들을 입력
for (int i = 1; i <= V; i++) {
if (indegree[i] == 0) {
queue.offer(i);
sb.append(i).append(" ");
}
}
while(!queue.isEmpty()) {
t++;
int size = queue.size();
for (int i = 0; i < size; i++) {
int cur = queue.poll();
for (int e: graph.get(cur)) {
indegree[e]--;
if (indegree[e] == 0) {
sb.append(e).append(" ");
queue.offer(e);
}
}
}
}
}
}
위 문제는 위상 정렬이라는 알고리즘의 개념과 구현 방법을 정확히 알고 있는지 확인하는 좋은 예제이다.
특히 작업의 선행 관계를 그래프로 모델링하고 진입차수라는 핵심적인 아이디어를 사용하여 문제를 해결하는 과정이 중요했던 것 같다.
위상 정렬은 빌드 시스템의 의존성 관리 등 다양한 실세계 문제들에 대한 기본 모델이 되기 때문에 이해해 두는 것이 좋다고 생각한다.