위상정렬은 들어보기만 했고 아예 모르는 알고리즘이다! 위상정렬은 대체 뭘까?
아하 그러니까 방향 그래프에서 출발 정점이 도착 정점보다 앞에 오는 정렬이다.
쉽게 설명하자면 선수 과목을 생각해보면 된다. 특정 과목을 이수해야만 수강 할 수 있는 과목이 있다.
이 과목들을 위상정렬 했을 시, 선수 과목을 지킨 정렬이 위상정렬이다.
다만 위상정렬은 사이클이 존재하면 할 수 없다. 또한 위상정렬된 결과는 여러개일 수 있다.
이름만 들었을땐 어려워 보였지만, 생각한 것 보다 쉽다. 다만 효율적으로 구현해야한다.
위 사항을 지키면 다음과 갇타.
이를 코드로 옮겨보자.
//1~n까지 정점
const adj = [...];
const indegrees = [...]; //각 정점 indegree
const q = [];
const result = [];
for(let i = 1; i<=n; i++){
if(indegrees[i] === 0) q.push(i);
}
while(q.length){
const cur = q.shift();
result.push(cur);
for(const next of adj[cur]){
indegress[next]--;
if(indegrees[next] === 0) q.push(next);
}
}
if(result.length !== n) return console.log('사이클 존재');
return result;
그래프에서의 BFS,DFS처럼 시간복잡도는 O(V+E)다. 왜 그럴까?
=> 각 정점은 큐에 한 번 들어가고 indegree감소연산은 각 간선에대해 한번 발생한다.
보자마자 위상정렬을 떠올릴 수 있어야 한다. 작은 학생이 큰 학생보다 앞에 있어야 한다는 건, 방향그래프를 뜻한다.
const input = require("fs")
.readFileSync("example.txt")
.toString()
.trim()
.split("\n")
.map((el) => el.trim().split(" ").map(Number));
const [n, m] = input[0];
const adj = {};
const deg = Array(n + 1).fill(0);
const q = [];
const result = [];
for (let i = 1; i <= n; i++) {
adj[i] = [];
}
for (let i = 1; i <= m; i++) {
const [short, long] = input[i];
adj[short].push(long);
deg[long]++;
}
for (let i = 1; i <= n; i++) {
if (deg[i] === 0) q.push(i);
}
while (q.length) {
const cur = q.shift();
result.push(cur);
for (const next of adj[cur]) {
deg[next]--;
if (deg[next] === 0) q.push(next);
}
}
console.log(result.join(" "));
해결!
최소 신장트리를 알아보기전에 신장트리가 무엇인지 부터 알아야 한다.
MST를 구할때 사용하는 대표적인 알고리즘이다. 하지만 Union Find알고리즘을 알아야 이 알고리즘을 익힐 수 있다.
그래서 Union Find알고리즘 부터 알아보자!
유니온파인드 사진의 출처는 여기입니다!
서울 => 강릉은 갈 수 있지만, 서울 => 부산 은 갈 수 없다. 왜?
=> 이어져 있지 않고든
이렇게 복잡한 노선도라면 직관적으로 판별 불가능하다. 이때 사용하는게 Union find(상호 배타 집합 찾기)다.
먼저 모든 노드를 초기화(홀로 떼어버림)한다. 이때, 부모는 자기 자신이다.
만약 1-2노드를 연결하고 4-5-6노드를 연결하고싶다면 아래처럼 하면 된다.
이런식으로 연결 한 뒤, 부모를 나타내는 값을 배열에 적어준다.
하지만 이렇게 바로 윗 부모를 배열에 적어주면 가끔 문제가 발생한다.
이렇게 극단적인 트리가 존재한다면...부모까지 O(n)만큼의 시간이 걸릴 것이다.
따라서 바로 윗 부모가 아닌, 루트를 기록해둔다면 판단하기 쉬울 것이다.
요래말이다.
위 그림을 이제 코드로 옮겨보자!
//초기화 과정. 자기 자신이 부모다.
const init = (N) => {
return Array(N)
.fill(0)
.map((value, idx) => idx);
};
//부모를 탐색하는 과정.
const findParent = (x, parent) => {
if (parent[x] === x) { //배열 인덱스와 값이 같으면 루트다
return x;
};
return parent[x] = findParent(parent[x], parent); //배열의 값을 인덱스로 갖는 값을 리턴
};
const union = (A, B, parent) => {
const rootA = findParent(A, parent);
const rootB = findParent(B, parent); //각자 루트노드를 가진다.
rootA < rootB ? (parent[rootB] = rootA) : (parent[rootA] = rootB);
};
const isDiffParent = (parent, x, y) => {
return findParent(parent,x) === findParent(parent,y)
}
이제 크루스칼 알고리즘을 배워보자
const e,v //간선갯수, 정점갯수
const edge = Array({length:e}, () => [비용, 정점1, 정점2]);
const parent = []
edge.sort((a,b) => a[0] - b[0]) //제일 비용이 낮은 간선순으로 정렬;
let cnt = 0;
for(let i = 0; i< e; i++){
const [cost, a, b] = edge[i];
if(!isDiffParent(parent,a,b)) continue;
console.log(a,b);
cnt++;
if(cnt === v-1) break;
}
MST를 찾는 문제다. 당연히 크루스칼을 사용하면 쉽게 풀린다.
function solution(n, costs) {
const init = (n) => Array(n).fill(0).map((el,idx) => idx);
const parent = init(n+1);
const find = (x,parent) => {
if(parent[x] === x) return x;
return parent[x] = find(parent[x], parent);
}
const union = (a,b,parent) => {
const rootA = find(a,parent);
const rootB = find(b,parent);
rootA < rootB ? (parent[rootB] = rootA) : (parent[rootA] = rootB);
}
const isSameParent = (parent,x,y) => find(x,parent) === find(y,parent);
const e = costs.length;
costs.sort((a,b) => a[2] - b[2]);
let cnt = 0;
let result = 0;
for(let i = 0; i < e; i++){
const [start,to,cost] = costs[i];
if(isSameParent(parent,start,to)) continue;
union(start,to,parent);
cnt++;
result += cost;
if(cnt === n-1) break;
}
return result;
}
하지만 의문이 하나 남아있었다. 어차피 그리디하게 짧은 비용의 간선만 추가하면 되는데, 그러면 conneceted
라는 배열을 하나 선언해서 union-find
없이 사용할수 있지 않을까?
바로 실험해보았다.
function solution(n, costs) {
const connected = Array(n+1).fill(0)
costs.sort((a,b) => a[2] - b[2]);
let cnt = 0;
let result = 0;
for(let i = 0; i < costs.length; i++){
const [start,to,cost] = costs[i];
if(connected[start] && connected[to]) continue;
connected[start] = 1;
connected[to] = 1;
cnt++;
result += cost;
if(cnt === n-1) break;
}
return result;
}
곰곰히 생각해보면, 두번째 코드는 cycle을 고려하지 않는다. 결국 cycle이 형성되면 최소 스패닝트리를 만족하지 않는다.
궁금증 해결!