본문 바로가기
코딩 테스트/백준

5719 거의 최단 경로 (자바스크립트)

by 위그든씨 2025. 6. 30.

요즘 많은 자동차에서는 GPS 네비게이션 장비가 설치되어 있다. 네비게이션은 사용자가 입력한 출발점과 도착점 사이의 최단 경로를 검색해 준다. 하지만, 교통 상황을 고려하지 않고 최단 경로를 검색하는 경우에는 극심한 교통 정체를 경험할 수 있다.

상근이는 오직 자기 자신만 사용 가능한 네비게이션을 만들고 있다. 이 네비게이션은 절대로 최단 경로를 찾아주지 않는다. 항상 거의 최단 경로를 찾아준다.

거의 최단 경로란 최단 경로에 포함되지 않는 도로로만 이루어진 경로 중 가장 짧은 것을 말한다. 

예를 들어, 도로 지도가 아래와 같을 때를 생각해보자. 원은 장소를 의미하고, 선은 단방향 도로를 나타낸다. 시작점은 S, 도착점은 D로 표시되어 있다. 굵은 선은 최단 경로를 나타낸다. (아래 그림에 최단 경로는 두 개가 있다)거의 최단 경로는 점선으로 표시된 경로이다. 이 경로는 최단 경로에 포함되지 않은 도로로 이루어진 경로 중 가장 짧은 경로이다. 거의 최단 경로는 여러 개 존재할 수도 있다. 예를 들어, 아래 그림의 길이가 3인 도로의 길이가 1이라면, 거의 최단 경로는 두 개가 된다. 또, 거의 최단 경로가 없는 경우도 있다.

=====

문제 풀이

우선 다익스트라 방식으로 출발 지점부터 도착 지점까지의 최단 경로를 구한 뒤 그 방식으로 이동했을 경우 지나쳤던 경로들을 false 처리 해주고 false 경로는 패스하면서 다시 한번 다익스트라 방식으로 최단 거리를 찾으면 된다. 

처음 다익스트라 방식으로 경로를 탐색할 때 특정 지점까지 동일한 이동거리로 오는 경우가 있을 수 있다. 기록 된 distances[next] 와 값이 같다고 해서 그것 또한 힙에 넣어주면 시간 초과가 발생할 것 같았다. 때문에 distances의 각 노드에는 가중치를 뜻하는 weight과 거기 까지 온 이전 노드의 값을 기록해줄 previous를 Set으로 지정해줬다. 특정 지점까지 이동했을 때 더 작은 가중치로 갱신 됐다면 previous는 clear로 이전 기록을 치우고 지금의 이전 노드를 추가해주면 된다. 만약 기록된 weight 과 동일한 가중치로 방문했다면 previous에 기록만 해주고 힙에는 추가 안해주면 된다.

아래는 힙 구현 부분

class Heap {
    heapq;
    constructor() {
        this.heapq = [undefined];
    }
    isEmpty() {
        return this.heapq.length === 1;
    }
    swap(src, des) {
        [this.heapq[src], this.heapq[des]] = [this.heapq[des], this.heapq[src]];
    }
    push(arr) {
        this.heapq.push(arr);
        let cur = this.heapq.length - 1;
        while (cur > 1) {
            let parent = Math.floor(cur / 2);
            if (this.heapq[parent][0] > this.heapq[cur][0]) {
                this.swap(cur, parent);
                cur = parent;
            } else {
                break;
            }
        }
    }
    pop() {
        if (this.isEmpty()) return undefined;
        const root = this.heapq[1];
        const last = this.heapq.pop();
        if (this.isEmpty()) return last;
        this.heapq[1] = last;
        let cur = 1;
        while (cur * 2 < this.heapq.length) {
            let smallest = cur * 2;

            const right = cur * 2 + 1;
            if (
                right < this.heapq.length &&
                this.heapq[smallest][0] > this.heapq[right][0]
            ) {
                smallest = right;
            }

            if (this.heapq[cur][0] > this.heapq[smallest][0]) {
                this.swap(cur, smallest);
                cur = smallest;
            } else {
                break;
            }
        }
        return root;
    }
}

입력값을 받아올 재량으로 만든 함수

const input = require('fs')
    .readFileSync(process.platform === 'linux' ? '/dev/stdin' : './input.txt')
    .toString()
    .trim()
    .split('\n');

const LEN = input.length;

let idx = 0;
const init = () => {
    const [n, m] = input[idx++].split(' ').map(Number);
    const [s, e] = input[idx++].split(' ').map(Number);
    const arr = input.slice(idx, idx + m).map((s) => s.split(' ').map(Number));
    idx += m;
    return {
        n,
        m,
        s,
        e,
        arr,
    };
};

아래는 위에서 설명한 로직을 코드로 구현한 부분이다.

도착 지점까지 동일한 최단 거리로 여러 경로가 있더라도 그 최단 거리 이전에 처리 됐으므로 cur이 도착지점과 같다면 반복문을 종료해줘도 무방하다.

const { n, m, s, e, arr } = init();
const gr = Array.from({ length: n }, () => []);
for (const [s, e, v] of arr) {
    gr[s].push([e, v]);
}
const distances = Array.from({ length: n }, () => ({
    weight: Infinity,
    previousNode: new Set(),
}));
	// 거의 최단 경로를 구할 때 방문하면 안되는 경로를 피하기 위해 선언된 배열
const routes = Array.from({ length: n }, () =>
    Array.from({ length: n }, () => true)
);
const hq = new Heap();
distances[s].previousNode.add(-1);
distances[s].weight = 0;
hq.push([0, s]);

while (!hq.isEmpty()) {
    const [totalDistance, cur] = hq.pop();
    if (cur !== e && totalDistance > distances[e]) {
        break;
    }
    if (cur === e) {
        break;
    }
    for (const [next, value] of gr[cur]) {
        const nextDis = totalDistance + value;
        	// 동일 가중치라면 방문 경로에 추가
        if (distances[next].weight === nextDis) {
            distances[next].previousNode.add(cur);
            continue;
        }
        if (distances[next].weight < nextDis) continue;
        distances[next].weight = nextDis;
        distances[next].previousNode.clear();	// 갱신되는 더 작은 가중치라면 이전 방문 치우고 새로이 기록
        distances[next].previousNode.add(cur);
        hq.push([nextDis, next]);
    }
}

이제 거의 최단 경로를 구하기 위해 방문해서는 안되는 경로들을 기록하는 로직을 시작한다.

이는 도착지점부터 시작하여 previous들을 방문하면서 출발 지점까지 반복문을 돌린다.

const q = [e];
const visited = Array.from({ length: n }, () => false);
while (q.length) {
    const cur = q.shift();
    if (cur === s) continue;
    for (const p of distances[cur].previousNode) {
        routes[p][cur] = false;
        if (visited[p]) continue;	// 동일한 지점을 또 방문할 필요 없으니 처리
        visited[p] = true;
        q.push(p);
    }
}

이제 위에서 구한 routes 경로들은 피하면서 최단 경로를 구하면 된다.

const a = new Heap();
a.push([0, s]);

const dd = Array.from({ length: n }, () => Infinity);
dd[s] = 0;
while (!a.isEmpty()) {
    const [total, cur] = a.pop();
    if (cur === e) break;
    for (const [next, v] of gr[cur]) {
        const nv = v + total;
        if (routes[cur][next] === false) continue;
        if (dd[next] <= nv) continue;

        dd[next] = nv;
        a.push([nv, next]);
    }
}

if (dd[e] === Infinity) {
    answers.push(-1);
} else {
    answers.push(dd[e]);
}

정답 코드

class Heap {
    heapq;
    constructor() {
        this.heapq = [undefined];
    }
    isEmpty() {
        return this.heapq.length === 1;
    }
    swap(src, des) {
        [this.heapq[src], this.heapq[des]] = [this.heapq[des], this.heapq[src]];
    }
    push(arr) {
        this.heapq.push(arr);
        let cur = this.heapq.length - 1;
        while (cur > 1) {
            let parent = Math.floor(cur / 2);
            if (this.heapq[parent][0] > this.heapq[cur][0]) {
                this.swap(cur, parent);
                cur = parent;
            } else {
                break;
            }
        }
    }
    pop() {
        if (this.isEmpty()) return undefined;
        const root = this.heapq[1];
        const last = this.heapq.pop();
        if (this.isEmpty()) return last;
        this.heapq[1] = last;
        let cur = 1;
        while (cur * 2 < this.heapq.length) {
            let smallest = cur * 2;

            const right = cur * 2 + 1;
            if (
                right < this.heapq.length &&
                this.heapq[smallest][0] > this.heapq[right][0]
            ) {
                smallest = right;
            }

            if (this.heapq[cur][0] > this.heapq[smallest][0]) {
                this.swap(cur, smallest);
                cur = smallest;
            } else {
                break;
            }
        }
        return root;
    }
}

const input = require('fs')
    .readFileSync(process.platform === 'linux' ? '/dev/stdin' : './input.txt')
    .toString()
    .trim()
    .split('\n');

const LEN = input.length;

let idx = 0;
const init = () => {
    const [n, m] = input[idx++].split(' ').map(Number);
    const [s, e] = input[idx++].split(' ').map(Number);
    const arr = input.slice(idx, idx + m).map((s) => s.split(' ').map(Number));
    idx += m;
    return {
        n,
        m,
        s,
        e,
        arr,
    };
};
const answers = [];
while (idx < LEN - 1) {
const { n, m, s, e, arr } = init();
const gr = Array.from({ length: n }, () => []);
for (const [s, e, v] of arr) {
    gr[s].push([e, v]);
}
const distances = Array.from({ length: n }, () => ({
    weight: Infinity,
    previousNode: new Set(),
}));
const routes = Array.from({ length: n }, () =>
    Array.from({ length: n }, () => true)
);
const hq = new Heap();
distances[s].previousNode.add(-1);
distances[s].weight = 0;
hq.push([0, s]);

while (!hq.isEmpty()) {
    const [totalDistance, cur] = hq.pop();
    if (cur !== e && totalDistance > distances[e]) {
        break;
    }
    if (cur === e) {
        break;
    }
    for (const [next, value] of gr[cur]) {
        const nextDis = totalDistance + value;
        if (distances[next].weight === nextDis) {
            distances[next].previousNode.add(cur);
            continue;
        }
        if (distances[next].weight < nextDis) continue;
        distances[next].weight = nextDis;
        distances[next].previousNode.clear();
        distances[next].previousNode.add(cur);
        hq.push([nextDis, next]);
    }
}

const q = [e];
const visited = Array.from({ length: n }, () => false);
while (q.length) {
    const cur = q.shift();
    if (cur === s) continue;
    for (const p of distances[cur].previousNode) {
        routes[p][cur] = false;
        if (visited[p]) continue;
        visited[p] = true;
        q.push(p);
    }
}

const a = new Heap();
a.push([0, s]);

const dd = Array.from({ length: n }, () => Infinity);
dd[s] = 0;
while (!a.isEmpty()) {
    const [total, cur] = a.pop();
    if (cur === e) break;
    for (const [next, v] of gr[cur]) {
        const nv = v + total;
        if (routes[cur][next] === false) continue;
        if (dd[next] <= nv) continue;

        dd[next] = nv;
        a.push([nv, next]);
    }
}

if (dd[e] === Infinity) {
    answers.push(-1);
} else {
    answers.push(dd[e]);
}
}

console.log(answers.join('\n'));

'코딩 테스트 > 백준' 카테고리의 다른 글

1079 마피아 (자바스크립트)  (0) 2025.07.04
22870 산책 (자바스크립트)  (0) 2025.06.30
2504 괄호의 값  (0) 2025.06.30
3109 빵집 (자바스크립트)  (0) 2025.06.23
17182 우주 탐사선 (자바스크립트)  (0) 2025.06.22