자바스크립트 코딩테스트 강좌, 동전 개수의 최솟값 구하기

작성자: 조광형

작성일: 2024년 11월 26일

문제 설명

동전으로 거슬러 줄 수 있는 최소 개수를 계산하는 문제입니다. 여러분은 주어진 동전의 종류와 총액을 기반으로 해서 동전을 최소 개수로 사용하여 총액을 만드는 방법을 찾아야 합니다. 동전의 종류는 무한히 많다고 가정합니다.

예를 들어, 동전의 종류가 {1, 5, 10, 25} 동전이 있고, 만들어야 하는 총액이 63일 때, 여러분은 최소 몇 개의 동전을 사용해야 할지를 찾아야 합니다.

입력

  • coinValues: 동전의 종류를 나타내는 정수 배열 (예: [1, 5, 10, 25])
  • amount: 총액을 나타내는 정수 (예: 63)

출력

  • 최소 동전 개수를 반환합니다. 만약 금액을 만들 수 없는 경우에는 -1을 반환합니다.

문제 접근 방식

이 문제는 동적 프로그래밍(Dynamic Programming) 기법을 사용하여 해결할 수 있습니다. 동적 프로그래밍은 문제를 작은 부분문제로 나누고, 이러한 작은 문제들의 해답을 조합하여 전체 문제의 해결책을 찾아가는 방식입니다. 이 문제를 풀기 위해 다음과 같은 단계를 따르겠습니다.

  1. DP 테이블 초기화: 동전을 사용하여 만들 수 있는 각 금액에 대해 최소 동전 개수를 기록하는 DP 테이블을 만듭니다.
  2. 기본 케이스 설정: DP 테이블의 0 번째 인덱스(0원)는 동전이 필요 없으므로 0으로 설정합니다.
  3. 이전 결과 활용: 각 동전에 대해 가능한 금액을 계산하며 DP 테이블을 갱신합니다.
  4. 결과 반환: lowest number of coins to make the required amount, or -1 if it’s impossible.

자바스크립트 코드 구현


function coinChange(coinValues, amount) {
    // DP 테이블 초기화
    const dp = Array(amount + 1).fill(Infinity);
    dp[0] = 0; // 0원을 만들기 위한 동전의 수는 0

    // 각 동전을 하나씩 확인
    for (let coin of coinValues) {
        for (let j = coin; j <= amount; j++) {
            dp[j] = Math.min(dp[j], dp[j - coin] + 1);
        }
    }

    // 결과 반환
    return dp[amount] === Infinity ? -1 : dp[amount];
}

            

이 코드는 주어진 coinValues 배열과 amount를 가지고 DP 테이블을 작성하고, 최소 동전의 개수를 계산합니다.

동작 과정 설명

위의 코드는 여러 단계로 구성되어 있으며, 이를 통해 동전의 개수를 최소화하는 방법을 살펴보겠습니다.

1단계: DP 테이블 초기화

const dp = Array(amount + 1).fill(Infinity); 이 코드는 일정한 길이의 배열을 생성하며 모든 값을 무한대로 초기화합니다.
이후 dp[0] = 0; 으로 0원을 만드는 데 필요한 동전 개수를 0으로 설정합니다.

2단계: 각 동전으로 금액 갱신

for (let coin of coinValues) 구문으로 각 동전에 대해 반복하며 가능한 금액을 계산합니다.
중첩된 for 문에서 dp[j] = Math.min(dp[j], dp[j - coin] + 1);를 통해 각 금액에 대한 최소 동전 개수를 갱신합니다.

3단계: 결과 반환

마지막으로 return dp[amount] === Infinity ? -1 : dp[amount];를 통해 해당 금액을 만들 수 없는 경우 -1을 반환합니다. 만들 수 있는 경우 최소 동전 개수를 반환합니다.

예제와 테스트 케이스

예제 1

입력: coinValues = [1, 5, 10, 25], amount = 63

출력: 6

해설: 25, 25, 10, 1, 1, 1로 총 6개의 동전을 사용하여 63을 만들 수 있습니다.

예제 2

입력: coinValues = [2], amount = 3

출력: -1

해설: 2만 사용해서는 3을 만들 수 없습니다.

예제 3

입력: coinValues = [1], amount = 0

출력: 0

해설: 0원을 만들기 위해서 필요한 동전은 없습니다.

이 코드의 시간 복잡도와 공간 복잡도

이 알고리즘의 시간 복잡도는 O(n * m)입니다. 여기서 n은 동전의 종류 수, m은 목표 자금입니다. DP 테이블을 작성하고 갱신하는 데 O(m) 작업이 동전 수만큼 반복되므로 이 복잡도를 가집니다.
공간 복잡도는 O(m)입니다. 이는 동전 갯수에 대해 동적으로 생성된 DP 테이블 때문입니다.

마무리

이 글에서는 자바스크립트를 사용하여 동전 개수의 최솟값을 구하는 문제를 동적 프로그래밍을 통해 해결하는 방법을 다루었습니다.
문제를 제공한 뒤 다양한 해법과 코드 구현을 통해 주제를 심층적으로 살폈습니다.
이러한 알고리즘은 실제 면접에서도 자주 출제되므로 잘 익히는 것이 좋습니다.
앞으로의 코딩테스트 준비에 도움이 되길 바랍니다!

자바스크립트 코딩테스트 강좌, 숫자의 합 구하기

본 글에서는 자바스크립트 코딩 테스트에서 자주 출제되는 알고리즘 문제 중 하나인 “숫자의 합 구하기” 문제를 다루겠습니다. 이 문제를 통해 기본적인 알고리즘 구성 능력과 자바스크립트의 데이터 처리 방법에 대해 배워 보도록 하겠습니다.

문제 설명

주어진 숫자 문자열이 있을 때, 문자열에 포함된 숫자들의 합을 구하는 함수를 작성하시오.
예를 들어, 입력 문자열이 “12345”이라면, 1 + 2 + 3 + 4 + 5 = 15를 반환해야 합니다.

입력

  • 길이가 n인 숫자 문자열이 1개 주어집니다 (1 ≤ n ≤ 106)

출력

  • 숫자 문자열에 포함된 모든 숫자의 합을 정수로 반환합니다.

문제 접근 방법

이 문제를 해결하기 위해, 기본적으로 다음과 같은 과정을 거치게 됩니다:

  1. 숫자 문자열을 순회하여 각 문자를 숫자로 변환합니다.
  2. 변환된 숫자를 누적 합산합니다.
  3. 최종 합계를 반환합니다.

코드 구현

자바스크립트에서 이 문제를 해결하는 코드 구현은 다음과 같이 진행할 수 있습니다.


function sumOfDigits(numString) {
    // 기본 변수 초기화
    let total = 0;

    // 문자 순회
    for(let i = 0; i < numString.length; i++) {
        // 각 문자를 숫자로 변환 후 누적 합
        total += parseInt(numString[i]);
    }

    // 최종 합 반환
    return total;
}

// 함수 테스트
const inputString = "12345";
const result = sumOfDigits(inputString);
console.log("입력 문자열:", inputString);
console.log("숫자 합:", result); // 출력: 15

코드 설명

위 코드는 sumOfDigits라는 함수를 정의하고 있습니다. 이 함수는 입력으로 숫자 문자열을 받아 각 문자를 순회하며 정수로 변환한 후, 총합을 계산합니다.

  • let total = 0;: 문자열에서 합계를 저장할 변수를 초기화합니다.
  • for(let i = 0; i < numString.length; i++) { ... }: 입력 문자열의 길이만큼 반복문을 사용하여 각 문자를 순회합니다.
  • total += parseInt(numString[i]);: parseInt를 사용하여 문자열의 각 문자를 정수로 변환하고, 이를 누적하여 합계를 계산합니다.
  • return total;: 누적 합계를 반환합니다.

시간복잡도 분석

이 알고리즘의 시간 복잡도는 O(n)입니다. n은 입력 문자열의 길이를 의미합니다. 문자열을 한 번만 순회하므로, 시간 복잡도는 선형 시간입니다.

공간복잡도 분석

공간 복잡도는 O(1)입니다. 입력 문자열 외에는 추가적으로 변수 하나만을 사용하기 때문입니다.

변형 문제

위의 문제가 기본 형태라면, 변형된 문제로는 “음수와 양수를 포함한 숫자들로 이루어진 배열에서 양수의 합을 구하라”라는 문제가 있을 수 있습니다.
이러한 문제는 기본 알고리즘을 변경하거나 추가 조건을 고려해야 하므로, 코드를 약간 수정해야 합니다.

영지식형 문제 변형 코드 예시


// 변형 문제: 짝수 인덱스의 숫자 합 구하기
function sumEvenIndexedDigits(numString) {
    let total = 0;

    // 짝수 인덱스에 해당하는 숫자만 합산
    for(let i = 0; i < numString.length; i += 2) {
        total += parseInt(numString[i]);
    }
    
    return total;
}

// 코드 테스트
const inputString = "123456"; // 예: 1과 3과 5를 더함
console.log("짝수 인덱스 합:", sumEvenIndexedDigits(inputString)); // 출력: 12

결론

“숫자의 합 구하기” 문제는 자바스크립트의 기본적인 문법과 알고리즘을 익히는 데 매우 유용합니다.
이 문제를 통해 문자열 처리, 반복문, 조건문 등의 기초적인 개념을 학습할 수 있습니다.
다양한 변형 문제를 시도해보면서 알고리즘 이해를 더욱 깊이 있게 해보시기 바랍니다.

다음 시간에는 더 복잡한 문제를 다루어 보도록 하겠습니다. 이 강좌를 통해 여러분이 자바스크립트 코딩 테스트에서 사용할 수 있는 다양한 기술을 학습하고, 효과적으로 문제를 해결하는 데 도움이 되길 바랍니다.

자바스크립트 코딩테스트 강좌, 주몽의 명령

안녕하세요! 이번 포스팅에서는 자바스크립트로 코딩테스트를 준비하는 여러분들을 위해 ‘주몽의 명령’이라는 주제를 가지고 알고리즘 문제를 풀어보겠습니다. 이 과정에서 문제를 이해하고, 접근하는 방법, 코드 구현 및 시간 복잡도 분석까지 자세히 설명할 것입니다.

문제 설명

주몽은 한겨울에도 불구하고 자신의 부하들에게 명령을 내렸습니다. 그의 명령은 다음과 같은 형태로 주어집니다:

  • 이진 트리가 주어진다.
  • 각 노드는 0부터 n까지의 정수를 담고 있다.
  • 주몽이 명령한 숫자 x가 주어졌을 때, 이진 트리의 모든 경로에서 x의 개수를 세어야 한다.

이 문제에서 우리는 트리의 경로가 무엇인지, 그리고 어떻게 탐색할 것인지를 명확히 이해해야 합니다. 경로란 루트에서 리프 노드까지 이동하는 모든 노드의 집합을 의미합니다.

입력 형식

{
    "root": {
        "value": 1,
        "left": {
            "value": 2,
            "left": null,
            "right": {
                "value": 3,
                "left": null,
                "right": null
            }
        },
        "right": {
            "value": 4,
            "left": null,
            "right": null
        }
    },
    "x": 1
}

출력 형식

주어진 이진 트리에서 숫자 x의 개수를 포함하는 모든 경로의 x 개수의 합을 반환한다.

{
    "result": 2
}

문제 접근법

이 문제를 풀기 위해서는 DFS(Depth-First Search) 알고리즘을 사용할 것입니다. 이진 트리를 깊이 우선으로 탐색하면서 경로를 따라 `x`를 세어 나가는 방식입니다.

1단계: 트리 탐색

우선 트리를 탐색할 수 있는 재귀 함수를 작성합니다. 이 함수는 현재 노드, 현재 경로에 포함된 x의 개수, 그리고 결과를 저장할 변수를 인자로 받습니다.

2단계: 경로 저장 및 조건 확인

노드가 리프 노드인지 확인하고, 리프 노드에 도달했을 때 경로에 포함된 x의 개수를 결과 변수에 더합니다. 그리고 현재 노드가 리프가 아닐 경우 왼쪽 및 오른쪽 자식 노드를 재귀적으로 탐색합니다.

3단계: 시간 복잡도 분석

이 알고리즘의 시간 복잡도는 O(n)입니다. 트리의 모든 노드를 한 번씩 방문하기 때문입니다.

코드 구현


function countXPaths(root, x) {
    let totalPathsCount = 0;

    function dfs(node, currentCount) {
        if (node === null) {
            return;
        }

        // 현재 노드의 값을 확인
        let newCount = currentCount + (node.value === x ? 1 : 0);

        // 리프 노드인지 확인
        if (node.left === null && node.right === null) {
            totalPathsCount += newCount;
            return;
        }

        // 왼쪽과 오른쪽 자식 노드 탐색
        dfs(node.left, newCount);
        dfs(node.right, newCount);
    }

    dfs(root, 0);
    return { result: totalPathsCount };
}

테스트 케이스

이제 다양한 테스트 케이스를 만들어 우리가 작성한 코드가 예상대로 작동하는지 확인해보겠습니다.


// 1번 테스트 케이스
const testCase1 = {
    root: {
        value: 1,
        left: {
            value: 2,
            left: null,
            right: {
                value: 1,
                left: null,
                right: null
            }
        },
        right: {
            value: 4,
            left: null,
            right: null
        }
    },
    x: 1
};
console.log(countXPaths(testCase1.root, testCase1.x)); // 출력: { result: 2 }

// 2번 테스트 케이스
const testCase2 = {
    root: {
        value: 2,
        left: {
            value: 1,
            left: null,
            right: null
        },
        right: {
            value: 1,
            left: {
                value: 3,
                left: null,
                right: null
            },
            right: null
        }
    },
    x: 1
};
console.log(countXPaths(testCase2.root, testCase2.x)); // 출력: { result: 3 }

// 3번 테스트 케이스
const testCase3 = {
    root: {
        value: 5,
        left: {
            value: 1,
            left: {
                value: 1,
                left: null,
                right: null
            },
            right: {
                value: 2,
                left: null,
                right: null
            }
        },
        right: {
            value: 7,
            left: null,
            right: null
        }
    },
    x: 1
};
console.log(countXPaths(testCase3.root, testCase3.x)); // 출력: { result: 2 }

결론

이번 포스팅에서는 주몽의 명령이라는 알고리즘 문제를 통해 깊이 우선 탐색과 재귀함수의 개념을 이해하고 구현해보았습니다. 다양한 테스트 케이스를 통해 코드의 신뢰성을 높일 수 있었으며, 각 단계별로 문제를 해결하는 방법을 체계적으로 정리했습니다.

코딩 테스트를 준비하는 과정에서 문제를 접근하는 사고방식과 구현 능력은 매우 중요합니다. 지속적으로 연습하면서 이러한 문제들을 해결하는 데에 익숙해지시길 바랍니다.

자바스크립트 코딩테스트 강좌, 외판원의 순회 경로 짜기

안녕하세요! 이번 강좌에서는 코딩테스트에서 자주 출제되는 문제 중 하나인 외판원의 순회 문제(Traveling Salesman Problem, TSP)를 다루어 보겠습니다. 이 문제는 그래프와 동적 프로그래밍(Dynamic Programming)의 중요한 개념을 이해하는 데 도움이 됩니다.

1. 문제 설명

외판원의 순회 문제는 주어진 도시들 사이를 방문하며, 모든 도시를 한 번씩 방문한 후 출발 도시로 돌아오는 경로 중 최소 비용을 찾는 문제입니다. 이 문제는 일반적으로 각 도시 간의 이동 비용이 주어지는 거리 행렬로 표현되며, 외판원은 모든 도시를 반드시 한번씩 방문해야 합니다.

2. 문제의 수학적 표현

도시의 개수를 N이라고 할 때, 도시 간의 거리 행렬은 cost[i][j] 형태로 정의됩니다. 여기서 cost[i][j]는 도시 i에서 도시 j로 이동하는 비용을 나타냅니다. 외판원이 방문해야 할 경로는 다음과 같이 표현할 수 있습니다:

min(cost[0][1] + cost[1][2] + ... + cost[N-1][0])

3. 문제 해결 접근법

외판원의 순회 문제는 NP-hard 문제에 속하므로, 완전 탐색(Brute Force 방식)으로 해결하기에는 시간 복잡도가 매우 높습니다. 따라서 동적 프로그래밍(Dynamic Programming)을 사용하여 더 효율적으로 문제를 해결할 수 있습니다.

이 문제를 동적 프로그래밍 접근법으로 해결하기 위해 비트마스킹을 사용할 것입니다. 비트마스킹은 각 도시를 비트로 표현하여 방문 여부를 쉽게 체크할 수 있도록 도와줍니다. 아래의 알고리즘 단계로 문제를 접근해보겠습니다.

3.1 비트마스킹을 통한 상태 표현

방문한 도시의 상태를 비트마스크로 표현합니다. 예를 들어, 도시가 4개일 경우:

  • 0000: 아무 도시도 방문하지 않음
  • 0001: 도시 0 방문
  • 0010: 도시 1 방문
  • 0011: 도시 0과 도시 1 방문
  • …이와 같은 방식으로 모든 조합을 표현할 수 있습니다.

3.2 동적 프로그래밍 테이블 정의

DP 테이블 dp[mask][i]mask에 해당하는 도시들을 방문하고, 도시 i에서 출발했을 때의 최소 비용을 저장합니다. 초기 상태에서 mask는 1로 설정되어 있으며, 모든 다른 상태는 무한대(INFINITY)로 초기화합니다.

4. 알고리즘 구현

이제 우리가 작성한 알고리즘을 자바스크립트로 구현해 보겠습니다.

function tsp(cost) {
    const N = cost.length;
    const INF = Number.MAX_SAFE_INTEGER;
    const dp = new Array(1 << N).fill(null).map(() => new Array(N).fill(INF));
    
    // Starting point
    dp[1][0] = 0;

    for (let mask = 0; mask < (1 << N); mask++) {
        for (let u = 0; u < N; u++) {
            if (dp[mask][u] === INF) continue; // Skip if u is not visited

            // Visit all other cities not in the current mask
            for (let v = 0; v < N; v++) {
                if (mask & (1 << v)) continue; // Skip if v is already visited

                const nextMask = mask | (1 << v);
                dp[nextMask][v] = Math.min(dp[nextMask][v], dp[mask][u] + cost[u][v]);
            }
        }
    }

    let ans = INF;
    for (let i = 1; i < N; i++) {
        ans = Math.min(ans, dp[(1 << N) - 1][i] + cost[i][0]);
    }

    return ans;
}

5. 시간 복잡도

이 알고리즘의 시간 복잡도는 O(N^2 * 2^N)입니다. 비트마스킹을 통해 상태를 나타내는 방법과 DP 테이블 업데이트 과정이 결합되어 있기 때문에, 도시 수가 증가하면 상당한 연산 시간이 소요됩니다. 고로 이 알고리즘은 N이 20 이하일 때에만 실용적입니다.

6. 테스트 및 예제

이제 알고리즘을 테스트하기 위해 몇 가지 예제를 들어보겠습니다. 다음은 도시 간의 비용 행렬입니다:

const costMatrix = [
    [0, 10, 15, 20],
    [10, 0, 35, 25],
    [15, 35, 0, 30],
    [20, 25, 30, 0]
];
console.log(tsp(costMatrix)); // Expected output: 80

7. 결론

외판원의 순회 문제를 다루면서 동적 프로그래밍과 비트마스킹을 활용한 경로 탐색 기법을 이해할 수 있었습니다. 이 문제는 단순한 이동 경로 문제처럼 보일 수 있지만, 인터넷 기업의 경로 최적화 문제 등 여러 분야에 응용될 수 있는 중요한 알고리즘 문제입니다.

이번 강좌가 자바스크립트를 활용한 알고리즘 문제 해결에 도움이 되었기를 바라며, 추가적인 연습을 통하여 더 많은 문제를 풀이하시면 좋겠습니다. 감사합니다!

자바스크립트 코딩테스트 강좌, 구간 곱 구하기

코딩 인터뷰에서 종종 주어지는 문제 중 하나가 배열의 특정 구간(서브배열)의 곱을 구하는 문제입니다. 본 강좌에서는 이를 구간 곱 구하기 문제로 정의하고, 이를 해결하는 방법을 자세히 설명하겠습니다.

문제 정의

배열 A와 쿼리 리스트가 주어질 때, 각 쿼리는 구간 [L, R]을 포함하며, 우리는 A[L]에서 A[R]까지의 곱을 계산해야 합니다. 주어진 배열 A와 쿼리가 다음과 같다고 가정합시다:

A = [2, 3, 4, 5, 6]
쿼리 = [(1, 3), (0, 2), (2, 4)] // 인덱스는 0부터 시작

각 쿼리의 결과를 출력해야 합니다. 예를 들어, 쿼리 (1, 3)의 경우, A[1] * A[2] * A[3] = 3 * 4 * 5 = 60이 됩니다.

문제 이해

이 문제를 해결하기 위해서는 몇 가지 문제의 조건을 이해해야 합니다:

  • 배열 A의 크기와 각 쿼리의 구간 (L, R)의 유효성 검증
  • 구간의 곱을 빠르게 구하는 방법
  • 배열의 각 요소가 주어질 때, 여러 번의 곱을 계산하는 비효율성 문제를 어떻게 해결할 것인가

해결 전략

구간 곱을 효율적으로 계산하기 위해 프리픽스 곱 (prefix product)를 활용할 수 있습니다. 이 방법은 초기 배열 A로부터 새로운 배열 P(A)를 생성하여 각 인덱스 i에 대해 A의 0번 인덱스부터 i번 인덱스까지의 곱을 저장합니다. 그러면 쿼리 (L, R)의 곱은 다음과 같이 계산할 수 있습니다.

구간 곱 Q(L, R) = P(R) / P(L - 1)

여기서 P(i)는 인덱스 i까지의 곱을 의미하며, P(0)은 A[0]입니다.

구현

이제 위의 전략을 바탕으로 자바스크립트 코드를 구현해 보겠습니다:


function productRange(arr, queries) {
    // 배열의 크기
    const n = arr.length;

    // 프리픽스 곱 배열을 초기화
    const prefixProduct = new Array(n);
    prefixProduct[0] = arr[0];

    // 프리픽스 곱을 계산
    for (let i = 1; i < n; i++) {
        prefixProduct[i] = prefixProduct[i - 1] * arr[i];
    }

    // 결과 배열 초기화
    const result = [];

    // 쿼리 처리
    queries.forEach(([L, R]) => {
        if (L === 0) {
            result.push(prefixProduct[R]);
        } else {
            result.push(prefixProduct[R] / prefixProduct[L - 1]);
        }
    });

    return result;
}

// 테스트
const A = [2, 3, 4, 5, 6];
const queries = [[1, 3], [0, 2], [2, 4]];
console.log(productRange(A, queries)); // 결과 기대: [60, 24, 120]

분석

위 코드의 시간 복잡도는 O(N + Q)입니다. 여기서 N은 배열의 크기, Q는 쿼리의 수입니다. 프리픽스 곱을 계산하는 데 O(N)의 시간이 소요되고, 각 쿼리를 처리하는 데 O(1)의 시간이 소요되기 때문입니다.

이 방식은 구간 곱 문제를 효율적으로 해결할 수 있게 해 주며, 다양한 조건에서의 쿼리 처리에도 유용합니다.

대안적 방법

만약 배열에 변경이 가해질 경우, 업데이트하거나 동적으로 길어지는 쿼리에 대한 효율적인 데이터 구조가 필요할 수 있습니다. 이 경우 세그먼트 트리와 같은 자료 구조를 활용할 수 있습니다. 이를 통해 업데이트와 쿼리 처리 모두 O(log N) 시간 내에 할 수 있습니다.

결론

이번 강좌에서는 자바스크립트로 구간 곱을 계산하는 문제를 다루어 보았습니다. 배열의 프리픽스 곱을 활용하여 효율적으로 문제를 해결할 수 있음을 보여주었고, 필요할 경우 더 복잡한 구조로 확장할 수 있음을 언급했습니다.

여러분도 이러한 기법을 활용하여 자신만의 코딩 문제를 해결해 보세요!