
강철부대의 각 부대원이 여러 지역에 뿔뿔이 흩어져 특수 임무를 수행 중입니다. 지도에서 강철부대가 위치한 지역을 포함한 각 지역은 유일한 번호로 구분되며, 두 지역 간의 길을 통과하는 데 걸리는 시간은 모두 1로 동일합니다. 임무를 수행한 각 부대원은 지도 정보를 이용하여 최단시간에 부대로 복귀하고자 합니다. 다만 적군의 방해로 인해, 임무의 시작 때와 다르게 되돌아오는 경로가 없어져 복귀가 불가능한 부대원도 있을 수 있습니다.
강철부대가 위치한 지역을 포함한 총지역의 수 n, 두 지역을 왕복할 수 있는 길 정보를 담은 2차원 정수 배열 roads, 각 부대원이 위치한 서로 다른 지역들을 나타내는 정수 배열 sources, 강철부대의 지역 destination이 주어졌을 때, 주어진 sources의 원소 순서대로 강철부대로 복귀할 수 있는 최단시간을 담은 배열을 return하는 solution 함수를 완성해주세요. 복귀가 불가능한 경우 해당 부대원의 최단시간은 -1입니다.
n개의 노드가 있다.roads에 연결관계가 있다.sources에서 destination 까지 가는 최단 거리를 구해라| n | roads | sources | destination | result |
|---|---|---|---|---|
| 3 | [[1, 2], [2, 3]] | [2, 3] | 1 | [1, 2] |
| 5 | [[1, 2], [1, 4], [2, 4], [2, 5], [4, 5]] | [1, 3, 5] | 5 | [2, -1, 0] |
위의 문제는 가중치를 포함한 최소 거리를 구하는 문제이다.
이런 문제의 경우 가중치가 모두 양수라면 다익스트라(Dijkstra) 알고리즘일 가능성이 크다.
따라서 다익스트라(Dijkstra)를 이용해서 풀 예정이고 풀이를 보기 전에
다익스트라(Dijkstra) 알고리즘은 기본적으로 BFS(너비 우선 탐색)를 기반으로 구현을 한다.
여기서 구현 방식에 따라서 2가지 방식으로 나뉘는데 그것은 아래에서 자세히 설명하겠다.
다익스트라(Dijkstra) 구현 방식은
두가지 방식이 있다.
우선 순차 탐색을 이용하여 거리 테이블을 매번 확인 하는 방식인데 우리가 필요한게 2가지 있다.
우선 특정 지점부터 해당 노드까지 가는 거리를 담는 배열
ex) [Infinity, 0(시작 노드), 2번 노드까지의 거리, ....]
연결 관계가 나와 있는 인접 리스트
인접 리스트, 거리 배열 생성
// n+1을 하는 이유는 노드 번호로 배열을 사용하기 위해
const trees = new Array(n + 1).fill().map(_ => []);
for (let i = 0; i < roads.length; i++) {
trees[roads[i][0]].push(roads[i][1]);
trees[roads[i][1]].push(roads[i][0]);
}
// 거리배열은 거리가 최대 값이 되도록 초기화
// 거리에 최대 값이 있으면 굳이 Infinity로 설정 안해도됨
let visited = new Array(n + 1).fill(Infinity);
앞서 말했듯 다익스트라(Dijkstra) 알고리즘은 BFS를 기반으로 구현하기 때문에
우리가 알고 있는 익숙한 BFS느낌의 함수가 있다.
BFS의 로직은 다음과 같은 순서로 진행이 된다.
shortest 배열에 시작 지점을 0으로 변경trees배열에 있는 연결 되어있는 노드를 검사shortest 값이 현재 지점의 shortest값 +1 보다 크면 값 변경BFS문
const BFS = (goal) => {
shortest[goal] = 0;
let queue = [goal];
while (queue.length) {
let now = queue.shift();
for (let i = 0; i < trees[now].length; i++) {
// now 노드와 연결된 다음 노드의 visited 배열 값 비교
if (shortest[now] + 1 < shortest[trees[now][i]]) {
shortest[trees[now][i]] = shortest[now] + 1;
queue.push(trees[now][i]);
}
}
}
};
위의 과정을 거치면 거리 배열에 시작 지점은 0으로, 다른 지점의 값은 최소값 혹은 가는것이 불가능하면 Infinity 값이 들어가 있다.
문제 요구 사항에서 불가능할 경우 -1을 리턴해주라고 했기 때문에 해당 과정을 추가하면 된다.
전체 코드
function solution(n, roads, sources, destination) {
const trees = new Array(n + 1).fill().map(_ => []);
let answer = [];
for (let i = 0; i < roads.length; i++) {
trees[roads[i][0]].push(roads[i][1]);
trees[roads[i][1]].push(roads[i][0]);
}
let shortest = new Array(n + 1).fill(Infinity);
const BFS = (goal) => {
shortest[goal] = 0;
let queue = [goal];
while (queue.length) {
let now = queue.shift();
for (let i = 0; i < trees[now].length; i++) {
if (shortest[now] + 1 < shortest[trees[now][i]]) {
shortest[trees[now][i]] = shortest[now] + 1;
queue.push(trees[now][i]);
}
}
}
};
BFS(destination);
for (const army of sources) {
if (shortest[army] === Infinity) {
answer.push(-1);
}else{
answer.push(shortest[army]);
}
}
return answer;
}
그럼 이제 우선 순위 큐(Priority Queue)를 이용하여 풀어보자.
전에 있었던 백준 문제 에서도 말했지만
JavaScript에서는 우선순위 큐를 라이브러리로 제공하지 않아서 우리가 직접 구현을 해야한다.
따라서 전에 만들었던 우선순위 큐 구현 코드를 조금 수정하여 사용할 예정이다.
그런데 그 전에 우리는 우선 순위 큐에 {node: ?, cost: ?} 형태로 넣어줄 예정이고 cost를 기준으로 MinHeap을 만들 것이다.
그리고 만약 cost가 같을 경우 node가 더 작은 값을 우선으로 배치할 예정이다.
수정한 MinHeap을 이용한 우선순위 큐 구현 코드
class MinHeap {
constructor() {
this.heap = [null];
}
insert(item) {
// 트리의 마지막 위치부터 순서대로 계산
let current = this.heap.length;
while (current > 1) {
const parent = Math.floor(current / 2);
// cost 비교 후 위치 변경
if (this.heap[parent].cost > item.cost) {
this.heap[current] = this.heap[parent];
current = parent;
// 만약 cost 가 같을 경우 node를 기준으로 위치 변경
} else if (this.heap[parent].cost === item.cost) {
if (this.heap[parent].node > item.node) {
this.heap[current] = this.heap[parent];
current = parent;
} else break;
} else break;
}
//current 값이 위의 반복문을 통해 알맞은 위치로 왔다. 따라서 핻당 위치에 값 배치
this.heap[current] = item;
}
remove() {
let min = this.heap[1];
if (this.heap.length > 2) {
// 우선순위 큐에서 최상단을 제거하기 때문에 최상단 제거
// 마지막 위치의 노드에 있는 값을 최상단으로 옮김
this.heap[1] = this.heap[this.heap.length - 1];
this.heap.splice(this.heap.length - 1);
// 최상단부터 아래로 내려가며 비교
let current = 1;
let leftChild = current * 2;
let rightChild = current * 2 + 1;
// 이진 트리이기 때문에 왼쪽 자식이 있는 것으로 자식 노드가 있는지 판단
while (this.heap[leftChild]) {
let compareItem = leftChild;
// 오른쪽 자식이 있을 경우, 왼쪽 자식과 오른쪽 자식 노드 비교
// 비교 후 compareItem에 내가 비교할 노드 저장
if (this.heap[rightChild] && this.heap[compareItem].cost > this.heap[rightChild].cost) {
compareItem = rightChild;
} else if (this.heap[rightChild] && this.heap[compareItem].cost === this.heap[rightChild].cost) {
if (this.heap[compareItem].node > this.heap[rightChild].node) {
compareItem = rightChild;
}
}
// 위의 과정으로 더 작은 자식 노드가 compareItem에 들어간다.
// 따라서 compareItem과 부모 노드를 비교
// 만약 자식 노드가 더 클 경우 위치 변경
if (this.heap[current].cost > this.heap[compareItem].cost) {
[this.heap[current], this.heap[compareItem]] = [this.heap[compareItem], this.heap[current]];
current = compareItem;
} else if (this.heap[current].cost === this.heap[compareItem].cost) {
if (this.heap[current].node > this.heap[compareItem].node) {
[this.heap[current], this.heap[compareItem]] = [this.heap[compareItem], this.heap[current]];
// 위치 변경후 해당 위치에서 다시 반복문 진행
current = compareItem;
} else break;
} else break;
// current 값이 바뀌었기 때문에 자식 노드도 바꿔줌
leftChild = current * 2;
rightChild = current * 2 + 1;
}
} else if (this.heap.length === 2) {
this.heap.splice(1, 1);
} else {
return null;
}
return min;
}
getMin() {
return this.heap[1];
}
getSize() {
return this.heap.length - 1;
}
getHeap() {
return this.heap;
}
}
이제 우선순위 큐가 구현이 되었으니 로직을 순서대로 생각해보자.
우리는 우선순위 큐에 cost가 작은 값들이 들어올 예정이다.
여기서 cost는 해당 node까지 가는 거리를 나타내는 것
그럼 table에 거리를 기록한 shortest 배열이 필요하고, 해당 노드를 이미 거쳤는지 확인할 visited 배열도 필요하다.
그리고 트리의 연결 관계를 저장했던 trees 변수에도 heap의 구조에 맞게 {node: ? , cost: ?} 형태로 저장을 해야한다.
변수 생성 코드
let answer = [];
let list = Array.from({length: n + 1}, () => []);
for (let i = 0; i < roads.length; i++) {
const [start, end] = roads[i];
list[start].push({node: end, cost: 1});
list[end].push({node: start, cost: 1});
}
let shortest = Array.from({length: n + 1}, () => Infinity);
const visited = new Array(n + 1).fill(false);
const priorityQueue = new MinHeap();
그 외의 코드는 BFS와 똑같지만, BFS에서 반복문을 돌 때 사용하는 Queue 대신 우선순위 큐 (Priority Queue)를 사용한다고 생각하면 된다.
BFS 기반의 로직 코드
// 시작 지점 값 초기화
priorityQueue.insert({node: destination, cost: 0});
shortest[destination] = 0;
// 우선순위 큐를 이용한 BFS기반의 코드
while (priorityQueue.getSize()) {
const TopOfPrioriyQueue = priorityQueue.remove();
const now = TopOfPrioriyQueue.node;
// 현 위치 값의 visited를 체크
if (!visited[now]) {
visited[now] = true;
// 반복문으로 연결되어 있는 노드를 돌며 최단거리 갱신
// 최단 거리 갱신 후 다음 노드와 cost를 증가 시켜주고 우선 순위큐에 삽입
for (const node of list[now]) {
const pathNodeCost = node.cost;
const fullCost = TopOfPrioriyQueue.cost + pathNodeCost;
shortest[node.node] = Math.min(shortest[node.node], fullCost);
priorityQueue.insert({node: node.node, cost: fullCost});
}
}
}
전체 코드
class MinHeap {
constructor() {
this.heap = [null];
}
insert(item) {
// 트리의 마지막 위치부터 순서대로 계산
let current = this.heap.length;
while (current > 1) {
const parent = Math.floor(current / 2);
// cost 비교 후 위치 변경
if (this.heap[parent].cost > item.cost) {
this.heap[current] = this.heap[parent];
current = parent;
// 만약 cost 가 같을 경우 node를 기준으로 위치 변경
} else if (this.heap[parent].cost === item.cost) {
if (this.heap[parent].node > item.node) {
this.heap[current] = this.heap[parent];
current = parent;
} else break;
} else break;
}
//current 값이 위의 반복문을 통해 알맞은 위치로 왔다. 따라서 핻당 위치에 값 배치
this.heap[current] = item;
}
remove() {
let min = this.heap[1];
if (this.heap.length > 2) {
// 우선순위 큐에서 최상단을 제거하기 때문에 최상단 제거
// 마지막 위치의 노드에 있는 값을 최상단으로 옮김
this.heap[1] = this.heap[this.heap.length - 1];
this.heap.splice(this.heap.length - 1);
// 최상단부터 아래로 내려가며 비교
let current = 1;
let leftChild = current * 2;
let rightChild = current * 2 + 1;
// 이진 트리이기 때문에 왼쪽 자식이 있는 것으로 자식 노드가 있는지 판단
while (this.heap[leftChild]) {
let compareItem = leftChild;
// 오른쪽 자식이 있을 경우, 왼쪽 자식과 오른쪽 자식 노드 비교
// 비교 후 compareItem에 내가 비교할 노드 저장
if (this.heap[rightChild] && this.heap[compareItem].cost > this.heap[rightChild].cost) {
compareItem = rightChild;
} else if (this.heap[rightChild] && this.heap[compareItem].cost === this.heap[rightChild].cost) {
if (this.heap[compareItem].node > this.heap[rightChild].node) {
compareItem = rightChild;
}
}
// 위의 과정으로 더 작은 자식 노드가 compareItem에 들어간다.
// 따라서 compareItem과 부모 노드를 비교
// 만약 자식 노드가 더 클 경우 위치 변경
if (this.heap[current].cost > this.heap[compareItem].cost) {
[this.heap[current], this.heap[compareItem]] = [this.heap[compareItem], this.heap[current]];
current = compareItem;
} else if (this.heap[current].cost === this.heap[compareItem].cost) {
if (this.heap[current].node > this.heap[compareItem].node) {
[this.heap[current], this.heap[compareItem]] = [this.heap[compareItem], this.heap[current]];
// 위치 변경후 해당 위치에서 다시 반복문 진행
current = compareItem;
} else break;
} else break;
// current 값이 바뀌었기 때문에 자식 노드도 바꿔줌
leftChild = current * 2;
rightChild = current * 2 + 1;
}
} else if (this.heap.length === 2) {
this.heap.splice(1, 1);
} else {
return null;
}
return min;
}
getMin() {
return this.heap[1];
}
getSize() {
return this.heap.length - 1;
}
getHeap() {
return this.heap;
}
}
function solution(n, roads, sources, destination) {
let answer = [];
let list = Array.from({length: n + 1}, () => []);
for (let i = 0; i < roads.length; i++) {
const [start, end] = roads[i];
list[start].push({node: end, cost: 1});
list[end].push({node: start, cost: 1});
}
let shortest = Array.from({length: n + 1}, () => Infinity);
console.log(list);
const priorityQueue = new MinHeap();
const visited = new Array(n + 1).fill(false);
// 시작 지점 값 초기화
priorityQueue.insert({node: destination, cost: 0});
shortest[destination] = 0;
// 우선순위 큐를 이용한 BFS기반의 코드
while (priorityQueue.getSize()) {
const TopOfPrioriyQueue = priorityQueue.remove();
const now = TopOfPrioriyQueue.node;
// 현 위치 값의 visited를 체크
if (!visited[now]) {
visited[now] = true;
// 반복문으로 연결되어 있는 노드를 돌며 최단거리 갱신
// 최단 거리 갱신 후 다음 노드와 cost를 증가 시켜주고 우선 순위큐에 삽입
for (const node of list[now]) {
const pathNodeCost = node.cost;
const fullCost = TopOfPrioriyQueue.cost + pathNodeCost;
shortest[node.node] = Math.min(shortest[node.node], fullCost);
priorityQueue.insert({node: node.node, cost: fullCost});
}
}
}
// 출력을 위해 answer 배열에 삽입
for (let i = 0; i < sources.length; i++) {
if (shortest[sources[i]] === Infinity) {
answer.push(-1);
} else {
answer.push(shortest[sources[i]]);
}
}
return answer;
}