자바스크립트 코딩테스트 강좌, 임계 경로 구하기

문제 설명

여러분은 다양한 작업이 연결되어 있는 프로젝트 관리 시스템을 관리하고 있습니다. 각 작업은 특정 시간 동안 수행되어야 하며, 어떤 작업은 다른 작업의 완료 후에만 시작할 수 있습니다. 이러한 관계를 파악하고, 프로젝트가 완료되는 데 필요한 최소 시간을 계산하는 알고리즘을 구현하세요.

프로젝트는 다음과 같은 정보로 구성됩니다:

  • n : 작업의 개수
  • dependencies : 각 작업 간의 종속 관계를 나타내는 배열
  • times : 각 작업을 수행하는 데 필요한 시간의 배열

함수의 형식은 다음과 같습니다:

function criticalPath(n, dependencies, times) {
    // 여기에 코드를 작성하세요.
}
    

입력 예시

n = 5
dependencies = [[1, 0], [2, 1], [3, 1], [3, 2], [4, 3]]
times = [3, 2, 5, 1, 2]
입력: criticalPath(n, dependencies, times)
    

출력 예시

출력: 11
    

문제 풀이 접근법

이 문제를 해결하기 위해서는 다음과 같은 단계를 거쳐야 합니다:

1. 그래프 모델링

작업과 그 간의 종속 관계를 그래프 형태로 변환하여 표현합니다. 각 작업을 정점으로, 종속 관계를 간선으로 표현할 수 있습니다.

2. 위상 정렬 (Topological Sort)

주어진 그래프의 위상 정렬을 통해 작업의 수행 순서를 정합니다. 위상 정렬은 방향 그래프의 모든 정점을 방문하는 선형 배열을 찾는 과정입니다.

3. 최장 경로 (Longest Path) 계산

위상 정렬을 이용하여 각 작업의 시작 시점을 계산하고, 최종적으로 모든 작업이 완료되는 데 걸리는 최소 시간을 구합니다.

구현 코드

다음은 위의 접근법을 구현한 자바스크립트 코드입니다:

function criticalPath(n, dependencies, times) {
    const adjList = Array.from({length: n}, () => []);
    const inDegree = Array(n).fill(0);
    
    // 1. 그래프와 진입 차수 계산
    for (const [next, prev] of dependencies) {
        adjList[prev].push(next);
        inDegree[next]++;
    }
    
    // 2. 위상 정렬을 위한 큐 생성
    const queue = [];
    const timeToComplete = Array(n).fill(0);
    
    for (let i = 0; i < n; i++) {
        timeToComplete[i] = times[i];
        if (inDegree[i] === 0) {
            queue.push(i);
        }
    }
    
    // 3. 최장 경로 계산
    let maxTime = 0;

    while (queue.length) {
        const current = queue.shift();
        maxTime = Math.max(maxTime, timeToComplete[current]);

        for (const neighbor of adjList[current]) {
            timeToComplete[neighbor] = Math.max(timeToComplete[neighbor], timeToComplete[current] + times[neighbor]);
            inDegree[neighbor]--;
            if (inDegree[neighbor] === 0) {
                queue.push(neighbor);
            }
        }
    }
    
    return maxTime;
}
    

코드 설명

이제 각 코드의 부분을 살펴보겠습니다:

1. 그래프 구성 및 진입 차수 계산

우선 입력으로 주어진 작업의 종속 관계를 기반으로 인접 리스트를 생성하고, 각 정점의 진입 차수를 계산합니다. 진입 차수가 0인 작업은 바로 시작할 수 있는 작업이므로 큐에 추가합니다.

2. 위상 정렬 및 최장 경로 계산

큐에서 작업을 하나씩 꺼내어 그 작업에 연결된 후속 작업들의 최장 완료 시간을 업데이트합니다. 만약 후속 작업의 진입 차수가 0이 된다면 다시 큐에 추가합니다. 모든 작업을 처리한 후 가장 긴 시간을 기록하면 그게 임계 경로입니다.

시간 복잡도 분석

이 알고리즘은 그래프의 정점과 간선을 각각 한 번씩 탐색하므로 시간 복잡도는 O(V + E) 입니다. 여기서 V는 작업의 개수, E는 작업 간의 종속 관계의 개수입니다.

최종 생각

임계 경로를 구하는 문제는 프로젝트 관리 및 일정 계획에서 중요한 요소로, 실제 산업에서도 많이 사용됩니다. 이 문제를 통해 그래프와 위상 정렬의 개념을 이해하고, 자바스크립트로 복잡한 문제를 해결하는 능력을 키울 수 있습니다.

추가 연습 문제

이제 여러분의 실력을 테스트하기 위해 다음과 같은 문제들을 연습해보세요:

  1. 작업 간의 종속 관계가 다를 때, 임계 경로의 변경을 추적하는 알고리즘을 구현하십시오.
  2. 작업의 시간뿐만 아니라 비용을 고려하여 최적의 경로를 찾는 문제가 있다. 이 문제를 해결하기 위한 접근법은 무엇인가요?
  3. 그래프가 다른 구조를 가질 때(예: 비순환 방향 그래프) 위상 정렬을 어떻게 응용할 수 있을까요?

참고 자료

임계 경로 문제를 더 자세히 알고 싶다면 아래 링크를 참고하세요:

자바스크립트 코딩테스트 강좌, 절댓값 힙 구현하기

이 강좌에서는 자바스크립트로 절댓값 힙(Absolute Value Heap)을 구현하는 방법에 대해 배워보겠습니다. 절댓값 힙은 두 가지 기준으로 정렬된 힙 구조로, 주로 절댓값을 기준으로 데이터를 정렬합니다. 예를 들어, 절댓값이 같은 경우에는 실제 값에 따라 정렬됩니다.

문제 설명

절댓값 힙을 구성하는 방법과 이를 통해 주어진 작업을 수행하는 알고리즘 문제를 해결해 보겠습니다. 문제는 다음과 같습니다.

정수로 이루어진 배열이 주어질 때, 절댓값 힙을 이용하여 가장 작은 절댓값을 가진 수를 삭제하고 그 값을 반환하는 작업을 진행하세요. 절댓값이 동일한 경우에는 더 작은 값을 우선하여 삭제합니다.

입력 형식


["I 5", "I -5", "I 3", "I -2", "I 0", "I 4", "D 1", "D 1"]
    

출력 형식


[0, -2, 3, 4]
    

문제 풀이 과정

절댓값 힙을 구현하기 위해, 자바스크립트의 배열을 사용할 수 있습니다. 배열을 선언하고, 이를 기반으로 삽입 및 삭제 작업을 수행하는 메서드를 구현합니다.

1단계: 힙 클래스 정의

우선 힙의 기본 구조를 설계합니다. 힙은 항상 특정한 규칙에 따라 부모 노드와 자식 노드가 관계를 맺어야 합니다. 절댓값이 작은 순서로 정렬되며, 절댓값이 같을 경우 원래 값이 작은 순서로 정렬됩니다.


class AbsoluteValueHeap {
    constructor() {
        this.heap = [];
    }

    insert(value) {
        this.heap.push(value);
        this.bubbleUp(this.heap.length - 1);
    }

    bubbleUp(index) {
        const element = this.heap[index];
        while (index > 0) {
            const parentIndex = Math.floor((index - 1) / 2);
            const parent = this.heap[parentIndex];
            if (this.isCorrectOrder(element, parent)) break;
            this.heap[index] = parent;
            index = parentIndex;
        }
        this.heap[index] = element;
    }

    isCorrectOrder(child, parent) {
        if (Math.abs(child) < Math.abs(parent)) return true;
        if (Math.abs(child) > Math.abs(parent)) return false;
        return child < parent;
    }

    delete() {
        if (this.heap.length === 0) return null;
        const min = this.heap[0];
        const end = this.heap.pop();
        if (this.heap.length > 0) {
            this.heap[0] = end;
            this.bubbleDown(0);
        }
        return min;
    }

    bubbleDown(index) {
        const element = this.heap[index];
        const length = this.heap.length;
        while (true) {
            let leftChildIndex = 2 * index + 1;
            let rightChildIndex = 2 * index + 2;
            let leftChild, rightChild;
            let swap = null;

            if (leftChildIndex < length) {
                leftChild = this.heap[leftChildIndex];
                if (!this.isCorrectOrder(element, leftChild)) {
                    swap = leftChildIndex;
                }
            }

            if (rightChildIndex < length) {
                rightChild = this.heap[rightChildIndex];
                if (
                    (swap === null && !this.isCorrectOrder(element, rightChild)) ||
                    (swap !== null && !this.isCorrectOrder(leftChild, rightChild))
                ) {
                    swap = rightChildIndex;
                }
            }

            if (swap === null) break;
            this.heap[index] = this.heap[swap];
            index = swap;
        }
        this.heap[index] = element;
    }

    peek() {
        return this.heap[0] || null;
    }
}
    

2단계: 작업 처리 함수 만들기

이제 절댓값 힙을 기반으로 다양한 명령어를 처리하는 함수를 만들겠습니다. 명령어는 ‘I’는 삽입, ‘D’는 삭제를 나타냅니다.


function processCommands(commands) {
    const heap = new AbsoluteValueHeap();
    const results = [];

    for (const command of commands) {
        const [action, value] = command.split(' ');
        const num = parseInt(value);

        if (action === 'I') {
            heap.insert(num);
        } else if (action === 'D' && num === 1) {
            const deleted = heap.delete();
            results.push(deleted !== null ? deleted : 0);
        } else if (action === 'D' && num === -1) {
            // Min heap으로 구현하기 때문에 삭제할 필요 없음
            const deleted = heap.delete();
            results.push(deleted !== null ? deleted : 0);
        }
    }
    return results;
}
    

3단계: 전체 코드 요약

이제까지 만들어진 코드를 모두 결합해 전체 코드를 정리해보겠습니다.


class AbsoluteValueHeap {
    // 이전에 정의한 코드를 활용합니다.
}

function processCommands(commands) {
    // 이전에 정의한 코드를 활용합니다.
}

// 테스트 예제 실행
const commands = ["I 5", "I -5", "I 3", "I -2", "I 0", "I 4", "D 1", "D 1"];
console.log(processCommands(commands)); // [0, -2, 3, 4]
    

결론

이번 강좌에서는 자바스크립트로 절댓값 힙을 구현하여 주어진 문제를 해결하는 과정을 살펴보았습니다. 알고리즘의 이해를 돕기 위해 기본적인 힙 정렬 개념과 절댓값을 기준으로 하는 힙의 동작 원리를 설명하였습니다. 이 강좌를 바탕으로 더 발전된 자료구조와 알고리즘 문제 해결 능력을 키워나가길 바랍니다.

자바스크립트 코딩테스트 강좌, 세일즈맨의 고민

작성일:

작성자: 코딩전문가

문제 설명

한 세일즈맨이 다양한 도시들을 방문해 상품을 판매하고 있습니다. 이 세일즈맨은 각 도시에서 판매할 수 있는 상품의 가격과 각 도시 간의 이동 비용을 알고 있습니다. 세일즈맨은 목표를 설정하고 이 목표를 달성하는 가장 효율적인 경로를 찾아야 합니다. 즉, 세일즈맨은 각 도시를 한 번만 방문하고, 가능한 한 최대의 수익을 얻으면서 돌아오는 경로를 찾아야 합니다.

입력

입력으로는 도시의 수 n, 각 도시의 판매 가격 prices 배열, 도시 간 이동 비용 costs 2차원 배열이 주어집니다. 가격은 prices[i]로 i번째 도시의 상품 가격을 나타내며, 이동 비용은 costs[i][j]로 i번째 도시에서 j번째 도시로 가는 비용을 나타냅니다.

출력

세일즈맨이 천천히 돌아올 수 있는 최대 이익을 반환해야 합니다.

제한사항

  • 2 ≤ n ≤ 10
  • 0 ≤ prices[i] ≤ 1000
  • 0 ≤ costs[i][j] ≤ 1000

문제 해결 접근법

이 문제는 ‘외판원 문제’와 유사하여, 백트래킹이나 동적 계획법을 사용하여 해결할 수 있습니다. 근본적으로, 세일즈맨이 모든 도시를 방문하고 돌아오는 경로를 최적화하기 위해 가능한 모든 조합을 시도해야 합니다.

1단계: 문제 이해하기

세일즈맨은 모든 도시를 방문해야 하므로, 모든 도시 간의 경로를 탐색하면서 각 경로에서의 아이템 판매 수익과 이동 비용을 고려해야 합니다. 목표는 수익과 비용을 계산하여 최적의 경로를 선택하는 것입니다.

2단계: 알고리즘 설계

이 문제를 해결하기 위해 다음의 단계를 따릅니다:

  • 모든 도시를 시작 도시로 가정하고, 각 도시를 방문하여 가능한 모든 경로를 탐색합니다.
  • 각 경로의 판매 수익과 이동 비용을 계산하여 최적의 수익을 갱신합니다.
  • 모든 도시를 방문한 후 원래 도시로 돌아오는 비용도 포함하여 계산합니다.

3단계: 구현

이제 구현 단계로 넘어가겠습니다. 자바스크립트를 이용하여 세일즈맨의 최대 이익을 찾는 코드를 작성해 보겠습니다.


function maxProfit(n, prices, costs) {
    let maxProfit = -Infinity;

    function backtrack(currentCity, visited, currentProfit) {
        // 모든 도시를 방문했으면 집으로 돌아갑니다.
        if (visited.length === n) {
            const returnCost = costs[currentCity][0]; // 다시 시작 도시로 돌아가는 비용
            const totalProfit = currentProfit - returnCost; // 총 수익 계산
            maxProfit = Math.max(maxProfit, totalProfit);
            return;
        }

        // 각 도시를 방문합니다.
        for (let nextCity = 0; nextCity < n; nextCity++) {
            if (!visited.includes(nextCity)) {
                visited.push(nextCity); // 도시 방문 기록
                const nextProfit = currentProfit + prices[nextCity]; // 다음 도시 수익 계산
                const travelCost = costs[currentCity][nextCity]; // 이동 비용
                backtrack(nextCity, visited, nextProfit - travelCost); // 재귀 호출
                visited.pop(); // 방문 기록 삭제(백트래킹)
            }
        }
    }

    // 0번째 도시에서 시작
    backtrack(0, [0], prices[0]);

    return maxProfit;
}

// 예제 사용
const n = 4;
const prices = [100, 70, 90, 40];
const costs = [
    [0, 10, 15, 20],
    [10, 0, 35, 25],
    [15, 35, 0, 30],
    [20, 25, 30, 0]
];

console.log(maxProfit(n, prices, costs));
        

4단계: 코드 설명

위의 코드에서 정의한 maxProfit 함수는 다음을 수행합니다:

  • currentCity: 현재 도시를 추적합니다.
  • visited: 현재까지 방문한 도시를 추적합니다.
  • currentProfit: 현재까지의 누적 수익을 추적합니다.

그리고, 우리는 재귀적으로 각 도시를 탐색합니다. 모든 도시를 방문한 후에는 집으로 돌아오는 비용을 계산하여 총 이익을 갱신합니다.

예제 테스트

코드를 실행하면 maxProfit 함수가 최대 이익을 반환합니다. 다양한 입력값으로 실험해보면서 알고리즘의 성능을 확인하는 것이 좋습니다.

결론

이번 강좌에서는 세일즈맨 문제에 대해 알아봤습니다. 코딩테스트에서 자주 나오는 이론적 배경과 구현 방법을 익히는 것이 중요합니다. 다양한 경로를 탐색하며 최적의 이익을 수치적으로 계산하는 과정을 통해 자바스크립트의 강력한 기능을 활용하는 방법을 배웠습니다.

다음 시간에는 다른 알고리즘 문제를 다뤄보겠습니다. 질문이나 피드백이 있다면 댓글로 남겨주세요!

자바스크립트 코딩테스트 강좌, 다각형의 면적 구하기

이번 강좌에서는 코딩테스트에서 자주 출제되는 문제 중 하나인 “다각형의 면적 구하기” 문제를 다루어 보겠습니다. 다각형의 면적을 구하는 알고리즘을 자바스크립트로 구현하는 방법에 대해 깊이 있는 학습을 진행할 것입니다.

문제 설명

주어진 다각형의 꼭지점 좌표가 주어졌을 때, 이 다각형의 면적을 계산하는 함수를 작성하시오. 다각형의 꼭지점은 시계 방향 또는 반시계 방향으로 정렬되어 있으며, 꼭지점의 좌표는 평면 좌표계에서 정수로 표현됩니다.

입력

  • 다각형의 꼭지점 수 n (3 ≤ n ≤ 1000)
  • n개의 꼭지점의 좌표 (x1, y1), (x2, y2), ..., (xn, yn)

출력

다각형의 면적을 소수점 아래 두 자리까지 반올림하여 출력한다.

문제 풀이 과정

다각형의 면적을 구하는 방법으로는 다음과 같은 여러 가지가 있습니다. 여기서는 가장 일반적인 “슈뢰더의 공식(또는 다각형 면적 공식)”을 사용하겠습니다. 이 공식을 사용하면 다각형의 면적을 간단하게 구할 수 있습니다.

슈뢰더의 공식

주어진 꼭지점 (x1, y1), (x2, y2), ..., (xn, yn)에 대해서 면적 A는 다음과 같이 계산됩니다:

A = (1/2) * | Σ (xi * yi+1 - yi * xi+1) | 

여기서 i+1은 모듈러 연산을 사용하여 in에 도달할 경우 다시 1로 돌아가게 설정합니다. 이 공식은 다각형의 모든 변의 기여도를 고려하여 면적을 계산합니다.

자바스크립트 코드 구현

위의 공식을 코딩으로 구현해 보겠습니다. 아래에는 자바스크립트로 작성한 코드가 있습니다.


function calculatePolygonArea(vertices) {
    let n = vertices.length;
    let area = 0;

    for (let i = 0; i < n; i++) {
        let x1 = vertices[i][0];
        let y1 = vertices[i][1];
        let x2 = vertices[(i + 1) % n][0];
        let y2 = vertices[(i + 1) % n][1];

        area += (x1 * y2) - (y1 * x2);
    }

    return Math.abs(area / 2).toFixed(2);
}

// 예시
let vertices = [[0, 0], [4, 0], [4, 3], [0, 4]];
console.log(calculatePolygonArea(vertices)); // 12.00

코드 설명

  • calculatePolygonArea 함수는 다각형의 꼭지점 좌표 배열 vertices를 입력받습니다.
  • 다각형의 꼭지점 개수 n를 구합니다.
  • 초기 면적 area를 0으로 설정한 다음, 모든 꼭지점에 대해 면적을 계산합니다.
  • 현재 꼭지점 (xi, yi)와 다음 꼭지점 (xi+1, yi+1)를 사용하여 면적에 기여도를 더합니다.
  • Modulus 연산을 통해 마지막 꼭지점과 첫 번째 꼭지점을 연결하여 면적 계산을 완성합니다.
  • 계산된 면적을 정수로 반올림하여 반환합니다.

테스트 케이스

코드를 확인했다면 다음과 같은 테스트 케이스를 추가해 보겠습니다.


let testVertices1 = [[0, 0], [0, 2], [2, 2], [2, 0]]; //사각형
let testVertices2 = [[0, 0], [4, 0], [4, 3], [0, 4]]; // 불규칙 다각형

console.log(calculatePolygonArea(testVertices1)); // 4.00
console.log(calculatePolygonArea(testVertices2)); // 12.00

정리

이번 강좌에서는 다각형의 면적을 구하는 방법에 대한 이론과 함께 자바스크립트 구현 예시를 살펴보았습니다. 다각형의 면적을 구하는 공식을 이해하고, 이를 실제 코드로 구현하는 과정이 코딩테스트에서 도움이 되리라 생각합니다.

다각형의 면적 구하기 문제는 실제 코딩테스트에서도 자주 출제되므로, 기본적인 이론과 문제 풀이 과정을 확실히 숙지해 두시기 바랍니다. 다음 강좌에서는 다른 알고리즘 문제를 다룰 예정이니 많은 관심 부탁드립니다.

자바스크립트 코딩테스트 강좌, 디버깅은 왜 중요할까

코딩 테스트는 소프트웨어 엔지니어로서의 역량을 테스트하는 중요한 방법 중 하나입니다. 특히 자바스크립트는 웹 개발에서 가장 널리 사용되는 언어 중 하나이며, 다양한 알고리즘 문제를 해결하는 데 있어 자주 사용됩니다. 이번 포스트에서는 자바스크립트로 풀 수 있는 알고리즘 문제를 제시하고, 그 해결 과정에서 디버깅의 중요성을 강조하고자 합니다.

문제 설명

문제: 두 수의 합

주어진 배열에서 두 수를 찾아서 그 합이 특정 값(target)이 되는 경우, 해당 두 수의 인덱스를 반환하는 문제입니다. 배열은 항상 두 개의 해답이 존재한다고 가정합니다.

함수 시그니처

function twoSum(nums: number[], target: number): number[] {
        // Your code here
    }

예제 입력 및 출력

  • 입력: nums = [2, 7, 11, 15], target = 9
  • 출력: [0, 1]
  • 입력: nums = [3, 2, 4], target = 6
  • 출력: [1, 2]
  • 입력: nums = [3, 3], target = 6
  • 출력: [0, 1]

해결方案

이 문제를 해결하기 위해서는 배열을 반복하면서 각 요소에 대해 나머지 수가 배열 내에 존재하는지를 확인해야 합니다. 그러나 이 방법은 최악의 경우 O(n^2)의 시간 복잡도를 가지므로 비효율적입니다. 따라서 해시맵(또는 객체)을 사용하여 보다 효율적인 O(n)의 시간을 구현할 수 있습니다.

1단계: 문제 분석

주어진 배열이 [2, 7, 11, 15]이고, target이 9일 경우, 다음과 같은 과정을 통해 해결할 수 있습니다:

  • 2를 보고, 9에서 2를 뺀 값인 7이 해시맵에 있는지 확인합니다.
  • 7이 없으므로, 2를 해시맵에 추가합니다.
  • 7을 보고, 9 – 7 = 2가 해시맵에 있는지 확인합니다.
  • 2가 존재하므로, 우리는 [0, 1]의 인덱스를 반환합니다.

2단계: 코드 작성

function twoSum(nums, target) {
        const map = new Map();
        
        for (let i = 0; i < nums.length; i++) {
            const complement = target - nums[i];
            
            if (map.has(complement)) {
                return [map.get(complement), i];
            }
            map.set(nums[i], i);
        }
        
        throw new Error("No two sum solution");
    }

3단계: 디버깅 과정

코드를 작성한 후에는 반드시 디버깅 과정을 거쳐야 합니다. 다음은 코드 디버깅 시 주의해야 할 사항입니다:

  • 예외 처리: 입력 배열이 비어 있거나 두 수를 찾을 수 없는 경우, 적절한 에러 메시지를 반환하도록 합니다.
  • 변수 확인: map 객체가 올바르게 작동하고 있는지 확인하기 위해 중간 결과를 콘솔에 출력하여 확인해봅니다.
  • 성능 검토: 특히 큰 입력 데이터에 대해 성능을 고려하여 테스트해보아야 합니다.

디버깅 중요성

디버깅은 프로그래밍의 핵심 과정 중 하나입니다. 디버깅을 통해 우리는 코드의 문제점을 발견하고 수정함으로써, 더 나은 품질의 소프트웨어를 개발할 수 있습니다. 다음의 이유로 디버깅은 특히 중요합니다:

  1. 문제 해결 능력 향상: 디버깅 과정은 다양한 문제를 분석하고 해결하는 방법을 배우는 기회를 제공합니다.
  2. 코드 가독성 향상: 문제를 찾고 수정하는 과정에서 코드의 가독성을 높이는 방법을 배우게 됩니다.
  3. 프로젝트 품질 향상: 오류를 사전에 찾아내어 수정하는 과정은 최종 제품의 품질을 높여줍니다.
  4. 팀 협업의 기반: 디버깅 경험은 팀원 간의 협업을 증진시키고, 서로의 코드를 이해하는 데 도움을 줍니다.

결론

이번 포스팅에서는 간단한 알고리즘 문제를 통해 자바스크립트 코딩 테스트의 중요성과 디버깅의 필요성을 강조하였습니다. 문제 해결 뿐만 아니라, 그 과정에서 발생할 수 있는 오류를 찾아내고 수정하는 것이 더 나은 개발자로 성장하는 데 매우 중요합니다. 다음에도 더 다양한 주제로 찾아뵙겠습니다.