분할정복(Divide&Conquer)이란?

분할정복(Divide & Conquer) 알고리즘은 문제를 해결하는 일반적인 방법 중 하나로, 주어진 문제를 더 작고 동일한 유형의 하위 문제로 나눈 다음, 이 하위 문제들을 해결하여 원래 문제의 해답을 구하는 방식입니다. 이 방법은 여러 가지 알고리즘 설계 기법 중에서도 특히 강력하고 널리 사용됩니다.

분할정복 알고리즘은 세 가지 주요 단계로 구성됩니다:

  1. 분할(Divide): 문제를 더 작고 독립적인 하위 문제로 나눕니다. 이 단계에서는 주어진 문제를 나누는 기준과 방법을 결정합니다.
  2. 정복(Conquer): 나눈 하위 문제들을 재귀적으로 해결합니다. 이 단계에서는 하위 문제를 해결하기 위해 다시 분할정복 알고리즘을 적용하거나, 더 이상 나눌 수 없을 만큼 작은 경우 직접 해결합니다.
  3. 결합(Combine): 하위 문제의 해답들을 결합하여 원래 문제의 해답을 만듭니다. 이 단계에서는 하위 문제들의 해답을 이용해 전체 문제의 해답을 도출하는 방법을 정의합니다.

 

DP와의 공통점 및 차이점

공통점

  • 큰 문제를 더 작은 하위 문제로 나누어 해결한다.

차이점

  • 분할 정복 : 하위 문제가 서로 독립적
  • DP : 하위 문제들이 중복 되는 경우

 

알고리즘을 공부 할 수록, 서로 비슷한 알고리즘이 많다는 것을 알게 된다. 비슷하지만 다른 알고리즘들을 확실히 이해하고 각 문제에 적합한 알고리즘을 찾는 능력을 키우는 것이 중요해진다.

 

문제 요구사항

재귀적인 패턴으로 별을 찍어 보자. N이 3의 거듭제곱(3, 9, 27, ...)이라고 할 때, 크기 N의 패턴은 N×N 정사각형 모양이다.

크기 3의 패턴은 가운데에 공백이 있고, 가운데를 제외한 모든 칸에 별이 하나씩 있는 패턴이다.

***
* *
***

N이 3보다 클 경우, 크기 N의 패턴은 공백으로 채워진 가운데의 (N/3)×(N/3) 정사각형을 크기 N/3의 패턴으로 둘러싼 형태이다. 예를 들어 크기 27의 패턴은 예제 출력 1과 같다.

 

입력

첫째 줄에 N이 주어진다. N은 3의 거듭제곱이다. 즉 어떤 정수 k에 대해 N=3^k이며, 이때 1 ≤ k < 8이다.

 

출력

첫째 줄부터 N번째 줄까지 별을 출력한다.

 

입출력 예

입력 출력
27

 

문제 이해하기

문제 설명이 자세하지 않아 문제를 이해하는데 시간이 걸렸다.

 

이해한 결과, 3의 제곱수인 N이 주어질 때, N은 다음과 같이 구성된다.

  • N이 1일 때
*
  • N이 3일 때 : 가운데에 크기가 3/3 = 1*1인 공백을 두고 3*3의 패턴이 완성된다.
***
* *
***
  • N이 9일 때 : N이 3일 때의 패턴을 반복한다. 단, 가운데에 크기가 9/3 = 3*3인 공백을 두고 패턴을 그린다.

  • N이 27일 때 : N이 9일 때의 패턴을 반복한다. 단, 가운데에 크기가 27/3 = 9*9인 공백을 두고 패턴을 그린다.

즉, N이 커질수록 N/3에서 사용한 패턴을 재사용하여 3X3의 패턴을 만드는데, 중앙은 빈 형태로 그리게 되는 것이다.

그럼 이 문제는 하위 패턴을 재사용한다는 점에서 DP가 아닐까 싶긴하지만, 그림이 아닌 문자열을 하나씩 찍어나가는 것이기에, DP보다는 분할정복이 더 적합하다고 볼 수 있다.

 

성공 코드

function solution(N) {
    // 2차원 배열 생성 후 각 자리의 문자 저장 -> 마지막에 join
    let pattern = Array.from({ length: N }, () => Array(N).fill(' '));

    function fillPattern(x, y, size) {
        if (size === 1) {
            pattern[x][y] = '*';
            return;
        }

        let newSize = size / 3;
        for (let i = 0; i < 3; i++) {
            for (let j = 0; j < 3; j++) {
                if (i === 1 && j === 1) continue; // 가운데 비우기
                fillPattern(x + i * newSize, y + j * newSize, newSize);
            }
        }
    }

    fillPattern(0, 0, N);

    for (let i = 0; i < N; i++) {
        console.log(pattern[i].join(''));
    }
}

solution(N[0]);
  1. 문자열을 담을 2차원 배열을 생성한다 -> N*N 배열
  2. fillPattern 함수를 재귀적으로 호출하며 배열을 채운다
    • fillPattern(0, 0, 9)
      1. fillPattern(0, 0, 3) -> fillPattern(0, 0, 1) -> [0, 0] 자리에 *
      2. 반복하여 [ 0~2, 0~2 ] 자리에 3X3 패턴 생성
      3. fillPattern(0, 3, 3) -> fillPattern(0, 3, 1) -> [0, 3] 자리에 *
      4. 반복하여 [ 0~2, 3~5 ] 자리에 3X3 패턴 생성
      5. 위의 과정 반복하며 한 패턴씩 생성하여 9X9 패턴 완성
    • 위의 9X9 패턴 생성 반복
  3. 마지막에 배열을 join함으로서 문자열 형태로 출력

 

지피티의 도움을 받아 문제를 풀었다. 처음에는 단순 패턴 반복이라 생각하였는데, 너무나도 인적 사고였고.. 한 줄씩 출력하며 패턴을 완성해야하는 프로세스를 구현하기가 어려웠다. 컴퓨터적 사고를 통해 알고리즘을 구현해내는 것을 더 연습해야 할 듯 하다.

문제 요구사항

수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오.

예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {1020, 10, 30, 20, 50} 이고, 길이는 4이다.

 

입력

첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000)이 주어진다.

둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (1 ≤ Ai ≤ 1,000)

 

출력

첫째 줄에 수열 A의 가장 긴 증가하는 부분 수열의 길이를 출력한다.

 

입출력 예

입력 출력
6
10 20 10 30 20 50
4
  • 처음에는 문제가 잘 이해되지 않았다
  • 배열 [10, 20, 10, 30, 20, 50] 으로 만들 수 있는 부분 배열 중, 숫자가 연속적으로 커지는 배열의 최대 길이를 구한다
  • 예를 들어, [10], [10, 20], [10, 20, 30] 은 연속적으로 숫자가 커지는 배열이지만,[10, 20, 10], [10, 30, 20] 은 연속적으로 커지지 않기 때문에 답이 될 수 없다
  • 위의 입출력 예시에서는 [10, 20, 30, 50] 배열이 연속적으로 이어지는 부분 배열 중 길이가 가장 긴 배열이라 답이 4가 된다

 

첫번째 시도 - 문제 이해를 잘못함

처음에는 배열 내 앞의 숫자보다 커지는 수의 개수를 말하는 줄 알고 코드를 잘못 작성하였다. 당연히 오답 !

 

두번째 시도 - 여전히 문제 이해를 잘못함

이번에는 배열 내 앞의 숫자보다 커지는 수를 새로운 배열로 생각 했을 때, 그 배열의 최대 길이인 줄 알고 아래 코드를 작성했다.

function solution(A) {
    let n = +A[0]
    let nums = A[1].split(' ').map(Number);
    let maxLength = 0;

    for(let i = 1; i < n; i++){
        let cnt = 1;
        let increaseSet = new Set();
        for(let j = 0; j < i; j++){
            if(nums[j] < nums[i]) {
                if(!increaseSet.has(nums[j])){
                    increaseSet.add(nums[j]);
                    cnt++;
                }
            }
        }
        if(cnt > maxLength) maxLength = cnt;
    }

    console.log(maxLength);
}

당연히 오답 !

 

성공 코드

지피티의 도움으로 문제를 완전히 이해하고 dp를 적용시켜 문제를 풀어보았다.

  1. '연속적'으로 증가하는 부분 배열을 구하기 위해, 이중 for문을 사용한다.
  2. 첫번째 for문은 1번 인덱스부터 마지막 인덱스까지, 그 안의 두번째 for문은 0번째 인덱스부터 i-1번째 인덱스까지 돈다.
  3. 모든 인덱스는 요소 1개(본인)를 가지고 있기에, 배열의 길이와 같은 배열을 생성하여 모든 값을 1로 채운다. → dp배열
  4. for문에서 0부터 i-1까지 돌며 nums[i]보다 작은 값이 있으면 이 인덱스의 dp 값과 현재 dp 값을 비교하여 더 큰값을 할당한다.
    1. dp 배열은 모두 1로 채워져있고, 앞의 값부터 계산하게 된다.
    2. 예를 들어 i = 1, j = 0 일 때, nums[0]이 nums[1]보다 작은 값이라면 dp[1]에는 1 + 1인 2가 저장된다.
    3. 다음으로, i = 2, j = 1일 때, nums[1]이 nums[2]보다 작은 값이라면 dp[2]에는 dp[1] + 1인 3이 저장된다.
    4. 이 때 Math.max 함수를 사용하는 이유
      • nums[ j ]가 nums[ i ]보다 작은 값일 때, dp[ i ]에 값이 할당 되는데, 만약 i = 4라고 가정하자.
      • dp[2] = 3, dp[3] = 1 인 상태에서, nums[2], nums[3]이 nums[4] 보다 작은 값이라면 for문은 순차적으로 돌아 먼저 dp[4]dp[2]+1 = 4가 된다. 또 for문을 돌아 j가 3이 되었을 때, dp[3] = 1, dp[4] = 4이기에 이 때는 dp[4]가 4가 되어야한다.
      • 즉, 연속적으로 커지는 배열의 길이 중 최댓값을 구해야하기 때문에 Math.max 함수를 사용해야한다.
  5. 따라서 본 문제의 핵심은 누적값 재사용이라는 것을 알 수 있다.
function solution(A) {
    let n = +A[0]
    let nums = A[1].split(' ').map(Number);
    let dp = Array(n).fill(1);

    for (let i = 1; i < n; i++) {
        for (let j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }

    let maxLength = Math.max(...dp);

    console.log(maxLength);
}

solution(A);

 

아직 혼자 생각해내기에는 어려움이 많은 것 같다. 공부를 더 해야겠다...🥲

DP란?

DP = Dynamic Programming (동적 계획법)

복잡한 문제를 간단한 여러 개의 문제로 해결하는 알고리즘 설계 기법

각 하위 문제의 해결을 계산한 뒤, 그 해결책을 저장하여 후에 같은 하위 문제가 나왔을 경우 그것을 간단하게 해결할 수 있다.

DP 적용 조건

  1. Overlapping Subproblems(중복되는 하위 문제)
    • DP는 기본적으로 문제를 나누고 그 문제의 결과 값을 재활용해서 전체 답을 구한다.
    • 그래서 동일한 작은 문제들이 반복하여 나타나는 경우에 사용이 가능하다.
  2. Optimal Substructure(최적 부분 구조)
    • 부분 문제의 최적 결과 값을 사용해 전체 문제의 최적 결과를 낼 수 있는 경우를 의미한다.
    • 그래서 특정 문제의 정답은 문제의 크기에 상관없이 항상 동일하다.

문제 요구사항

어떤 동물원에 가로로 두칸 세로로 N칸인 아래와 같은 우리가 있다.

이 동물원에는 사자들이 살고 있는데 사자들을 우리에 가둘 때, 가로로도 세로로도 붙어 있게 배치할 수는 없다.

이 동물원 조련사는 사자들의 배치 문제 때문에 골머리를 앓고 있다.

동물원 조련사의 머리가 아프지 않도록 우리가 2*N 배열에 사자를 배치하는 경우의 수가 몇 가지인지를 알아내는 프로그램을 작성해 주도록 하자.

사자를 한 마리도 배치하지 않는 경우도 하나의 경우의 수로 친다고 가정한다.

입력

첫째 줄에 우리의 크기 N(1≤N≤100,000)이 주어진다.

출력

첫째 줄에 사자를 배치하는 경우의 수를 9901로 나눈 나머지를 출력하여라.

입출력 예

N
return
4
41
  • 사자가 0마리 일 때 경우의 수 1
  • 사자가 1마리 일 때 경우의 수 8
  • 사자가 2마리 일 때 경우의 수 18
  • 사자가 3마리 일 때 경우의 수 12
  • 사자가 4마리 일 때 경우의 수 2

첫번째 시도 - 구현 실패

일단은 생각나는 대로 코드를 작성해보았다.

  1. 사자가 0마리 일 때에는 경우의 수가 항상 1이다
  2. 사자가 1마리 일 때의 경우의 수는 전체 칸 수 이다
  3. 사자가 2마리 일 때의 경우의 수는 1부터 N-1개의 홀수에 각각 2를 곱한뒤 더한다

하지만 이 이후로 알고리즘을 생각해내기가 어려웠다.

(N=3의 경우의 수) + (N=4만 가능한 경우의 수)가 답 인것 같은데, 그 로직이 떠오르지 않아 골머리를 앓았다.

혼자 2시간 쯤 고민을 하고 결국 GPT의 도움을 받았다.

성공 코드

  1. 사자가 있는 우리를 배열로 나타내는게 아니라, i번째 줄에 사자가 없는 경우, 1마리씩 있는 경우를 배열로 나타낸다
    • dp[i][0] = i번째 줄에 사자가 없는 경우
    • dp[i][1] = i번째 줄 첫번째 열에 사자가 있는 경우
    • dp[i][2] = i번째 줄 두번째 열에 사자가 있는 경우
  2. 배열의 크기가 (N+1) * 3 인 배열을 만든다
    • 사실 배열의 크기가 Nx3 이어도 되는데, 가독성을 위해 0번째 행은 제외하고 생각한다.
  3. dp[1][0], dp[1][1], dp[1][2] 는 모두 1이다
    • 1번째 줄에 사자가 없을 경우 1가지
    • 1번째 줄 첫번째 열에 사자가 있을 경우 1가지
    • 1번째 줄 두번째 열에 사자가 있을 경우 1가지
  4. i번째 줄의 경우의 수는 아래와 같이 구한다
    • dp[i][0] = (dp[i-1][0] + dp[i-1][1] + dp[i-1][2])
      • i번째 줄에 사자가 없다면, i-1번째 줄에는 사자가 없어도 되고 있어도 되기에 모든 경우의 수를 더한다
    • dp[i][1] = (dp[i-1][0] + dp[i-1][2])
      • i번째 줄 첫번째 열에 사자가 있다면, i-1번째 줄에는 사자가 없거나, 두번째 열에 있을 수 있다
    • dp[i][2] = (dp[i-1][0] + dp[i-1][1])
      • i번째 줄 두번째 열에 사자가 있다면, i-1번째 줄에는 사자가 없거나, 첫번째 열에 있을 수 있다

위의 내용을 정리하면 아래 그림과 같다.

하위 → 상위 식으로 값이 누적되어 최종적으로는 N번째 줄의 모든 값을 더 하면 답이 된다.

function solution(N) {
    const mod = 9901;
    const dp = Array.from({ length: N + 1 }, () => Array(3).fill(0));
    
    // 초기값 설정
    dp[1][0] = 1;
    dp[1][1] = 1;
    dp[1][2] = 1;
    
    for (let i = 2; i <= N; i++) {
        dp[i][0] = (dp[i-1][0] + dp[i-1][1] + dp[i-1][2]) % mod;
        dp[i][1] = (dp[i-1][0] + dp[i-1][2]) % mod;
        dp[i][2] = (dp[i-1][0] + dp[i-1][1]) % mod;
    }
    
    const result = (dp[N][0] + dp[N][1] + dp[N][2]) % mod;
    console.log(result);
}

처음 지피티의 코드를 보고 이해하기가 쉽지 않았다.

이해를 한 뒤에는, 이런식으로도 문제를 풀 수 있구나하는 생각에 뒤통수를 맞은 느낌이었다.

마냥 사자의 수와 우리에 얽매여 더 효율적인 프로세스를 생각하지 못한것이 안타깝다..🥲

문제 요구사항

프로그래머스 재귀 문제 Lv.1 소수 만들기

주어진 숫자 중 3개의 수를 더했을 때 소수가 되는 경우의 개수를 구하려고 합니다.

숫자들이 들어있는 배열 nums가 매개변수로 주어질 때, nums에 있는 숫자들 중 서로 다른 3개를 골라 더했을 때 소수가 되는 경우의 개수를 return 하도록 solution 함수를 완성해주세요.

제한 사항

  • nums에 들어있는 숫자의 개수는 3개 이상 50개 이하입니다.
  • nums의 각 원소는 1 이상 1,000 이하의 자연수이며, 중복된 숫자가 들어있지 않습니다.

입출력 예

nums
return
[1,2,3,4]
1
[1,2,7,6,4]
4

성공 코드

보통 3중 for문을 이용해서 푸는데, 그 방법 보다는 재귀로 풀고싶어서 여러 가지 시도를 해보았다.

도저히 재귀로 풀 방법이 생각나지 않아서 GPT의 도움을 받아서 풀었다.

코드별 설명

1. 주어진 숫자가 소수인지 아닌지 구하는 함수

function isPrime(num) {
        if (num <= 1) return false;
        for (let i = 2; i * i <= num; i++) {
            if (num % i === 0) return false;
        }
        return true;
    }

약수에는 짝이 있기 때문에, 특정 수의 제곱이 num보다 작을 때까지 비교하며 특정 수가 약수인지 아닌지 찾는다.

Ex) 64 -> 64의 약수는 1 x 64, 2 x 32, 8 x 8 이므로 32까지 가지 않아도 2에서 약수가 있음이 검출된다.

2. 재귀 함수

// 조합을 재귀적으로 생성하고, 그 합이 소수인 경우를 계산하는 함수
    function findCombinations(index, selected, sum) {
        if (selected.length === 3) {
            if (isPrime(sum)) count++;
            return;
        }

        // 현재 전달 받은 index + 1, 현재 전달받은 배열에 현재 인덱스 값 추가해서 함수 호출
        // 전달한 배열의 길이가 3이면 return 되어 i가 더해짐
        for (let i = index; i < nums.length; i++) {
            findCombinations(i + 1, [...selected, nums[i]], sum + nums[i]);
        }
    }

index, selected, sum 세 개의 매개변수를 갖는다.

index -> 전체 nums의 인덱스 중 현재 가르키고 있는 인덱스

seleted -> 선택된 숫자들이 들어있는 배열

sum -> seleted 배열에 있는 숫자들의 합

seleted 안에 숫자가 3개라면 3개 숫자 합이 소수인지 아닌지 판별한다.

안에 들어있는 숫자가 3개가 아니라면, 다음 인덱스의 값을 추가하여 또 다시 함수를 호출한다.

이를 그림으로 정리해보면 아래 그림과 같다.

전체 코드

function solution(nums) {
    let count = 0;

    // 주어진 수가 소수인지 판별하는 함수
    function isPrime(num) {
        if (num <= 1) return false;
        for (let i = 2; i * i <= num; i++) {
            if (num % i === 0) return false;
        }
        return true;
    }

    // 조합을 재귀적으로 생성하고, 그 합이 소수인 경우를 계산하는 함수
    function findCombinations(index, selected, sum) {
        if (selected.length === 3) {
            if (isPrime(sum)) count++;
            return;
        }

        // 현재 전달 받은 index + 1, 현재 전달받은 배열에 현재 인덱스 값 추가해서 함수 호출
        // 전달한 배열의 길이가 3이면 return 되어 i가 더해짐
        for (let i = index; i < nums.length; i++) {
            findCombinations(i + 1, [...selected, nums[i]], sum + nums[i]);
        }
    }

    // 첫 번째 인덱스부터 조합 생성 시작
    findCombinations(0, [], 0);

    return count;
}

재귀를 생각해내기가 어려운 문제인 것 같다.

이해하고 나면 그렇게 어려운 알고리즘은 아니지만, 인적 사고로 바로 생각해내기에는 복잡함이 있다.

또한, 오히려 재귀를 쓰면 시간 초과가 되는 문제들이 있기 때문에 주어진 문제에 맞추어 알고리즘을 잘 선택해야할 것 같다.

특히, 프로그래머스의 모음 사전과 같은 문제는 재귀를 사용하기가 더욱 어려운 문제였고,

피보나치 수 문제는 재귀를 사용하면 시간 초과, for문을 사용해야 통과되는 경우가 있었다.

문제 요구사항

프로그래머스 Greedy Lv.2 큰 수 만들기

어떤 숫자에서 k개의 수를 제거했을 때 얻을 수 있는 가장 큰 숫자를 구하려 합니다.

예를 들어, 숫자 1924에서 수 두 개를 제거하면 [19, 12, 14, 92, 94, 24] 를 만들 수 있습니다. 이 중 가장 큰 숫자는 94 입니다.

문자열 형식으로 숫자 number와 제거할 수의 개수 k가 solution 함수의 매개변수로 주어집니다. number에서 k 개의 수를 제거했을 때 만들 수 있는 수 중 가장 큰 숫자를 문자열 형태로 return 하도록 solution 함수를 완성하세요.

제한 조건

  • number는 2자리 이상, 1,000,000자리 이하인 숫자입니다.
  • k는 1 이상 number의 자릿수 미만인 자연수입니다.

입출력 예

number
k
return
"1924"
2
"94"
"1231234"
3
"3234"
"4177252841"
4
"775841"
"3879781299"
2
"89781299"
"9998888123"
4
"999888

- 아래 2개의 테스트 케이스는 다른 분들이 제시한 테스트 케이스이다.

첫번째 시도 - 테스트 4번, 8번, 10번 실패

일단은 생각나는 대로 코드를 작성해보았다.

  1. number를 배열로 바꾼다. -> array
  2. 시작 인덱스를 정한다. -> startIndex
    • 한자리 수 중 최댓값은 9이므로 배열 앞자리에 9가 있으면 건너뛰고 시작 인덱스 설정.
  3. startIndex부터 k + 1만큼 배열 앞부분을 잘라서 따로 저장한다. -> frontPart
    • k만큼 자르게되면 k가 1일 때 비교값이 사라지기 때문에 k+1만큼 자른다.
    • 만약, frontPart의 마지막 값이 array의 마지막 값이라면, frontPart 부분을 모두 잘라내고 while문 밖으로 나간다.
  4. frontPart내에서 최댓값을 찾는다. -> max
  5. array에서 frontPart 부분을 돌며 max보다 작은 값을 삭제하기 위해 deleteSet에 저장한다.
    1. deleteSet에 값을 저장할 때 마다 k를 감소시키고, k가 0이 되었다면 for문 밖으로 나간다.
    2. max와 같은 값과 만나면 for문 밖으로 나간다.
  6. while문 마지막에서 filter를 이용해 deleteSet에 저장된 인덱스에 해당하는 값을 삭제한다.
  7. while문이 끝난 뒤 array의 길이가 결과 길이보다 길면 뒤에서부터 값을 삭제한다.
function solution(number, k) {
    // number를 배열로 바꾼다
    let array = number.split('');

    // 시작 인덱스를 정한다.
    let startIndex = 0;
    // 초기 max값은 9로 정한다. 배열의 가장 앞부분에 9가 있으면 건너뛰기 위해서.
    let max = 9;

    while(k > 0){
        // max값보다 작은 값이 처음 나오는 곳을 시작 인덱스로 정한다. -> 999, 888 등 높은 숫자가 이어지는 것 건너뛰기
        for(let i = startIndex; i < array.length; i++){
            if(array[i] < max){
                startIndex = i;
                break;
            }
        }

        // startIndex부터 k + 1만큼 배열 앞부분을 잘라서 따로 저장한다.
        let frontPart = array.slice(startIndex, startIndex+k+1).map(Number);

        // 만약, frontPart의 마지막 값이 array의 마지막 값이라면, frontPart 부분을 모두 잘라내고 while문 밖으로 나간다.
        if(startIndex+k >= array.length){
            array = array.slice(0, startIndex);
            break;
        }

        // frontPart 내에서 최댓값을 찾는다.
        max = Math.max(...frontPart);

        // array에서 frontPart 부분을 돌며 max보다 작은 값을 삭제하기 위해 deleteSet에 저장한다.
        let deleteSet = new Set();
        for(let i = startIndex; i < startIndex + frontPart.length; i++){
            if(array[i] != max) { 
                // deleteSet에 값을 저장할 때 마다 k를 감소시키고, k가 0이 되었다면 for문 밖으로 나간다.
                deleteSet.add(i);
                k--;
                if(k == 0) break;
            }
            else{ 
                // max와 같은 값과 만나면 for문 밖으로 나간다.
                break;
            }
        }

        // while문 마지막에서 filter를 이용해 deleteSet에 저장된 인덱스에 해당하는 값을 삭제한다.
        array = array.filter((_, index) => !deleteSet.has(index));
    }

    // while문이 끝난 뒤 array의 길이가 결과 길이보다 길면 뒤에서부터 값을 삭제한다.
    while(array.length > number.length - k){
        array.pop();
    }
    
    return array.join('');
}

주어진 테스트케이스에서는 모두 통과를 하였지만, 막상 제출을 하니 4번은 실패, 8번은 시간 초과, 10번은 런타임 에러가 떴다.

그래서 질의응답 창을 보던 중 Stack을 사용하면 간단하다는 힌트를 보고 아래 코드를 구현했다.

두번째 시도 - "Stack"이라는 힌트를 얻어 성공 !

  1. number를 배열로 바꾼다. -> array
  2. 결과 값이 되어야하는 길이를 저장해둔다. -> finalLength
  3. array의 가장 첫 숫자를 넣은 stack을 생성한다.
    • 가장 첫 숫자를 stack에 넣었기 때문에 시작 인덱스를 1로 정한다. -> index
  4. index가 array를 벗어나지 않는 동안 while문을 돌린다.
    1. stack이 비어있거나, stack의 맨 위의 값이 현재 값보다 크면 현재 값을 stack에 넣는다. 값을 넣었기때문에 index는 다음으로 넘어간다.
    2. stack의 맨 위의 값이 현재 값보다 작으면
      1. stack의 맨 위의 값을 뺀다. 값을 뺐기 때문에 k를 감소시킨다.
        • index를 그대로 두는 이유 : 다시 while문이 돌아 왔을 때 또 맨 위의 값이 현재 값보다 작으면 빼야한다.
      2. 만약 k가 0이라서 값을 더 이상 빼지 않아도 된다면 그냥 값을 넣고 index를 다음으로 넘긴다.
  5. while문이 모두 끝나서 array의 마지막 값까지 넣었다면 stack 길이를 finalLength와 맞춘다.
    • "9998888123"의 경우 마지막 3을 가리킬 때 1,2만 삭제하게 된다. 그렇게 되면 k가 0이 되기 전 while문이 끝난다.
function solution(number, k) {
    let array = number.split('');
    let finalLength = array.length - k;
    let stack = [array[0]];
    let index = 1;

    while(index < array.length){

        if((stack.length == 0 || stack[stack.length - 1] >= array[index])){
            stack.push(array[index]);
            index++;
        }
        else {
            if(k !== 0){
                stack.pop();
                k--;
            }
            else {
                stack.push(array[index]);
                index++;
            }
        }
    }
    while(stack.length > finalLength){
                stack.pop();
    }

    return stack.join('');
}

힌트를 얻어 코드를 작성하니 생각보다 간단하게 해결되었다. 처음에 너무 어렵게 생각한걸까...

Greedy 알고리즘은 너무 어렵다..🥲

문제 요구사항

H-Index는 과학자의 생산성과 영향력을 나타내는 지표입니다.

어느 과학자의 H-Index를 나타내는 값인 h를 구하려고 합니다. 위키백과에 따르면, H-Index는 다음과 같이 구합니다.

어떤 과학자가 발표한 논문 n편 중, h번 이상 인용된 논문이 h편 이상이고 나머지 논문이 h번 이하 인용되었다면 h의 최댓값이 이 과학자의 H-Index입니다.

어떤 과학자가 발표한 논문의 인용 횟수를 담은 배열 citations가 매개변수로 주어질 때, 이 과학자의 H-Index를 return 하도록 solution 함수를 작성해주세요.

 

제한사항

  • 과학자가 발표한 논문의 수는 1편 이상 1,000편 이하입니다.
  • 논문별 인용 횟수는 0회 이상 10,000회 이하입니다.

입출력 예

citations
return
[3, 0, 6, 1, 5]
3

 

입출력 예 설명

이 과학자가 발표한 논문의 수는 5편이고, 그중 3편의 논문은 3회 이상 인용되었습니다. 그리고 나머지 2편의 논문은 3회 이하 인용되었기 때문에 이 과학자의 H-Index는 3입니다.

------------------------------------------------------------

일단 이 문제의 큰 문제점은 이해하기가 어렵다는 것이다.

프로그래머스의 질의응답 게시판을 봐도 문제 이해가 안된다는 글이 대부분이다.

그래서 문제를 이해한 바탕으로 다시 설명해보자면,

"나머지 논문이 h번 이하 인용되었다면" 이 문구를 무시한다.

어떤 과학자가 발표한 논문 n편 중, h번 이상 인용된 논문이 h편 이상이면 h의 최댓값이 이 과학자의 H-Index입니다.

즉, 논문이 인용된 횟수가 a번이라고 하면, a번보다 많이 인용된 논문의 개수가 a보다 크거나 같으면 된다.

여기서 중요한 점은, 입력값에 존재하지 않는 인용 횟수h값이 될 수 있다는 것이다.

위의 입출력 예시로 다시 설명해보면

  • 0 -> 0번 이상 인용된 논문은 0, 1, 3, 5, 6 총 5개이다. 0 < 5 이므로 0은 H-Index의 후보값이 된다.
  • 1 -> 1번 이상 인용된 논문은 1, 3, 5, 6 총 4개이다. 1 < 4 이므로 1은 H-Index의 후보값이 된다.
  • 2 -> 2번 이상 인용된 논문은 3, 5, 6 총 3개이다. 2 < 3 이므로 2는 H-Index의 후보값이 된다.
  • 3 -> 3번 이상 인용된 논문은 3, 5, 6 총 3개이다. 3 = 3 이므로 3은 H-Index의 후보값이 된다.
  • 4 -> 4번 이상 인용된 논문은 5, 6 총 2개이다. 4 > 2이므로 4는 H-Index의 후보값이 될 수 없다.
  • 5 -> 5번 이상 인용된 논문은 5, 6 총 2개이다. 5 > 2이므로 5는 H-Index의 후보값이 될 수 없다.
  • 6 -> 6번 이상 인용된 논문은 6 총 1개이다. 6 > 1이므로 6은 H-Index의 후보값이 될 수 없다.

그럼 후보값 0, 1, 3 중에 가장 큰 값인 3이 H-Index가 된다.

다른 예시를 들어보면, [1, 10, 11] 이라는 세 값이 있을 때

  • 1번 이상 인용된 논문은 1, 10, 11로 3개이다. 1 < 3이므로 1은 H-Index의 후보값이 된다.
  • 2번 이상 인용된 논문은 10, 11로 2개이다. 2 = 2이므로 2은 H-Index의 후보값이 된다.
  • 3번 이상 인용된 논문은 10, 11로 2개이다. 3 > 2이므로 3은 H-Index의 후보값이 될 수 없다.

그럼 우리는 세번의 시도 만으로도 답을 알 수 있다.

2가 H-Index 후보값이지만, 3이 H-Index 후보값이 아니라면, 3보다 큰 수 중에서는 H-Index의 후보값이 될 수 있는 값이 없다.

따라서 답은 2이다.

위의 방법으로 문제를 이해하고 문제를 풀어보았다.

코드

function solution(citations) {
    citations.sort((a, b) => b - a); // 내림차순 정렬

    let sValue = citations[0]; // H-Index는 최댓값이므로, 논문이 가장 많이 인용된 횟수를 시작 값으로 정한다.

    while(true){               // 최악의 경우, 논문이 인용된 횟수 중 최댓값 x citations 배열 길이만큼 돌아야하므로 while문 사용
        let cnt = 0;
        
        for (let i = 0; i < citations.length; i++) {
            if (citations[i] >= sValue) { // citations 배열을 훑으며 sValue보다 크거나 같은 값을 카운트
                cnt = cnt + 1;
            }
        }
        if(cnt >= sValue){     // sValue보다 크거나 같은 값이 sValue보다 많으면 그 값을 H-Index로 정한다.
            return sValue;
        }
        else{
            sValue--;          // 아니라면 기존 sValue에서 1을 뺀 값을 sValue로 정한다.

            if(sValue < 0){    // sValue가 음수 값이라면 citations 배열의 길이를 return한다.
                return citations.length;
            }
        }
        
    }
}

console.log(solution([3, 0, 6, 1, 5])); // return 3
console.log(solution([1, 10, 11])); // return 2
console.log(solution([0, 0, 0, 0, 0])); // return 0

while true 문을 사용하여, 메모리 복잡도가 걱정되긴하지만 이런 식으로 풀이하였다.

아마 더 간단한 풀이도 있을텐데, 문제 이해를 위하여 이렇게 늘여서 작성하였다.

덱이란 ?

 덱(Deque) : 양쪽 끝에서 추가 및 제거가 가능한 자료구조로, 스택과 큐의 일반화된 형태

 

문제 요구사항

백준 28279번 덱 2

정수를 저장하는 덱을 구현한 다음, 입력으로 주어지는 명령을 처리하는 프로그램을 작성하시오.

명령은 총 여덟 가지이다.

-------------------------------------------------------------------------------

1 X: 정수 X를 덱의 앞에 넣는다. (1 ≤ X ≤ 100,000)

2 X: 정수 X를 덱의 뒤에 넣는다. (1 ≤ X ≤ 100,000)

3: 덱에 정수가 있다면 맨 앞의 정수를 빼고 출력한다. 없다면 -1을 대신 출력한다.

4: 덱에 정수가 있다면 맨 뒤의 정수를 빼고 출력한다. 없다면 -1을 대신 출력한다.

5: 덱에 들어있는 정수의 개수를 출력한다.

6: 덱이 비어있으면 1, 아니면 0을 출력한다.

7: 덱에 정수가 있다면 맨 앞의 정수를 출력한다. 없다면 -1을 대신 출력한다.

8: 덱에 정수가 있다면 맨 뒤의 정수를 출력한다. 없다면 -1을 대신 출력한다.

-------------------------------------------------------------------------------

첫째 줄명령의 수 N이 주어진다. (1 ≤ N ≤ 1,000,000)

둘째 줄부터 N개 줄에 명령이 하나씩 주어진다.

출력을 요구하는 명령은 하나 이상 주어진다.

출력을 요구하는 명령이 주어질 때마다 명령의 결과를 한 줄에 하나씩 출력한다. -> 무시해도 되는 요구사항이었다.. 뒷부분 참고

첫번째 시도

가장 정석인 push(), pop(), shift(), unshift()를 사용하였다.

역시나 시간 초과로 인한 오류가 발생한다.

shift(), unshift()는 배열을 한 칸씩 옮기기때문에 시간 복잡도가 O(n)로 매우 크다.

const fs = require('fs');
let input = fs.readFileSync('/dev/stdin').toString().split('\n');

function isEmpty(array){
    if(array.length < 1){
        return true;
    }
    else{
        return false;
    }
}

function solution(input) {
    let answer = [];
    for(let i = 1; i < +input[0] + 1; i++){
        if(input[i].length > 1){
            let array = input[i].split(' ');
            if(array[0] == '1'){ // 1 X: 정수 X를 덱의 앞에 넣는다. (1 ≤ X ≤ 100,000)
                answer.unshift(+array[1]);
            }
            else if(array[0] == '2'){ // 2 X: 정수 X를 덱의 뒤에 넣는다. (1 ≤ X ≤ 100,000)
                answer.push(+array[1]);
            }
        }
        else if(input[i].length == 1){
            switch(input[i]){
                case '3': // 3: 덱에 정수가 있다면 맨 앞의 정수를 빼고 출력한다. 없다면 -1을 대신 출력한다.
                    if(isEmpty(answer)){
                        console.log('-1');
                    }
                    else{
                        console.log(answer.shift());
                    }
                    break;
                case '4': // 4: 덱에 정수가 있다면 맨 뒤의 정수를 빼고 출력한다. 없다면 -1을 대신 출력한다.
                    if(isEmpty(answer)){
                        console.log('-1');
                    }
                    else{
                        console.log(answer.pop());
                    }
                    break;
                case '5': // 5: 덱에 들어있는 정수의 개수를 출력한다.
                    console.log(answer.length);
                    break;
                case '6': // 6: 덱이 비어있으면 1, 아니면 0을 출력한다.
                    console.log(isEmpty(answer) ? '1' : '0');
                    break;
                case '7': // 7: 덱에 정수가 있다면 맨 앞의 정수를 출력한다. 없다면 -1을 대신 출력한다.
                    if(isEmpty(answer)){
                        console.log('-1');
                    }
                    else{
                        console.log(answer[0]);
                    }
                    break;
                case '8': // 8: 덱에 정수가 있다면 맨 뒤의 정수를 출력한다. 없다면 -1을 대신 출력한다.
                    if(isEmpty(answer)){
                        console.log('-1');
                    }
                    else{
                        console.log(answer[answer.length - 1]);
                    }
                    break;
            }
        }
    }
}

solution(input);

두번째 시도

shift(), unshift()를 사용하지 않기 위해 addFront()와 removeFront()라는 함수를 만들었다.

새로운 배열을 생성하여 공간 복잡도가 높아질 수는 있지만, 백준에서는 시간이 항상 문제이기에 이렇게 작성하였다.

하지만 이번에도 시간 초과로 실패하였다.

const fs = require('fs');
let input = fs.readFileSync('/dev/stdin').toString().split('\n');

function addFront(array, num){
    let newArray = [];
    if(array.length === 0){
        newArray.push(num);
    }
    else{
        newArray = array.map((_, i) => i === 0 ? num : array[i - 1]);
        newArray[newArray.length] = array[array.length - 1];
    }
    return newArray;
}

function removeFront(array){
    let newArray = [];
    if(array.length === 1){
        console.log(array[0]);
    }
    else{
        newArray = array.map((_, i) => array[i + 1]);
        newArray.pop();
        console.log(array[0]);
    }
    return newArray;
}

function solution(input) {
    let answer = [];
    for(let i = 1; i < +input[0] + 1; i++){
        if(input[i].length > 1){
            let array = input[i].split(' ');
            if(array[0] === '1'){        // 1 X: 정수 X를 덱의 앞에 넣는다. (1 ≤ X ≤ 100,000)
                answer = addFront(answer, +array[1]);
            }
            else if(array[0] === '2'){   // 2 X: 정수 X를 덱의 뒤에 넣는다. (1 ≤ X ≤ 100,000)
                answer.push(+array[1]);
            }
        }
        else if(input[i].length === 1){
            if(['3', '4', '7', '8'].includes(input[i])){
                if(answer.length === 0){
                    console.log('-1');
                }
                else{
                    switch(input[i]){
                        case '3':               // 3: 덱에 정수가 있다면 맨 앞의 정수를 빼고 출력한다. 없다면 -1을 대신 출력한다.
                            answer = removeFront(answer);
                            break;
                        case '4':               // 4: 덱에 정수가 있다면 맨 뒤의 정수를 빼고 출력한다. 없다면 -1을 대신 출력한다.
                            console.log(answer.pop());
                            break;
                        case '7':               // 7: 덱에 정수가 있다면 맨 앞의 정수를 출력한다. 없다면 -1을 대신 출력한다.
                            console.log(answer[0]);
                            break;
                        case '8':               // 8: 덱에 정수가 있다면 맨 뒤의 정수를 출력한다. 없다면 -1을 대신 출력한다.
                            console.log(answer[answer.length - 1]);
                            break;
                    }
                }  
            }
            else{
                switch(input[i]){
                case '5':               // 5: 덱에 들어있는 정수의 개수를 출력한다.
                    console.log(answer.length);
                    break;
                case '6':               // 6: 덱이 비어있으면 1, 아니면 0을 출력한다.
                    console.log(answer.length === 0 ? '1' : '0');
                    break;
                }
            }
        }
    }
}

solution(input);

세번째 시도

배열 내장 함수를 사용하는 것이 문제인가 싶어 Linked List로 구현해보았다.

코드는 복잡해졌지만, array를 사용할 때 보다 채점 퍼센트가 더 올라갔다.

하지만 그럼에도 시간 초과로 실패하였다.

const fs = require('fs');
let input = fs.readFileSync('/dev/stdin').toString().split('\n');

class Node {
    constructor(value) {
      this.value = value;
      this.next = null;
      this.prev = null;
    }
}
  
class Deque {
    constructor() {
        this.head = null;
        this.tail = null;
        this.size = 0;
    }

    isEmpty() {
        return this.size === 0;
    }

    addFront(value) {
        const newNode = new Node(value);

        if (this.isEmpty()) {
            this.head = newNode;
            this.tail = newNode;
        } 
        else {
            newNode.next = this.head;
            this.head.prev = newNode;
            this.head = newNode;
        }

        this.size++;
    }

    addRear(value) {
        const newNode = new Node(value);

        if (this.isEmpty()) {
            this.head = newNode;
            this.tail = newNode;
        } 
        else {
            newNode.prev = this.tail;
            this.tail.next = newNode;
            this.tail = newNode;
        }

        this.size++;
    }

    removeFront() {
        if (this.isEmpty()) {
            return '-1';
        }

        const removedValue = this.head.value;
        if (this.size === 1) {
            this.head = null;
            this.tail = null;
        } 
        else {
            this.head = this.head.next;
            this.head.prev = null;
        }

        this.size--;
        return removedValue;
    }

    removeRear() {
        if (this.isEmpty()) {
            return '-1';
        }

        const removedValue = this.tail.value;
        if (this.size === 1) {
            this.head = null;
            this.tail = null;
        } 
        else {
            this.tail = this.tail.prev;
            this.tail.next = null;
        }

        this.size--;
        return removedValue;
    }

    printFirst() {
        if (this.isEmpty()) {
            return '-1';
        }

        return this.head.value;
    }

    printLast() {
        if (this.isEmpty()) {
            return '-1';
        }

        return this.tail.value;
    }
}

function solution(input) {
    const deque = new Deque();
    
    for(let i = 1; i < +input[0] + 1; i++){
        if(input[i].trim().length > 1){
            let array = input[i].trim().split(' ');
            if(array[0] === '1'){        // 1 X: 정수 X를 덱의 앞에 넣는다. (1 ≤ X ≤ 100,000)
                deque.addFront(+array[1])
            }
            else if(array[0] === '2'){   // 2 X: 정수 X를 덱의 뒤에 넣는다. (1 ≤ X ≤ 100,000)
                deque.addRear(+array[1])
            }
        }
        else if(input[i].trim().length === 1){
            switch(input[i].trim()){
                case '3':               // 3: 덱에 정수가 있다면 맨 앞의 정수를 빼고 출력한다. 없다면 -1을 대신 출력한다.
                    console.log(deque.removeFront());
                    break;
                case '4':               // 4: 덱에 정수가 있다면 맨 뒤의 정수를 빼고 출력한다. 없다면 -1을 대신 출력한다.
                    console.log(deque.removeRear());
                    break;
                case '5':               // 5: 덱에 들어있는 정수의 개수를 출력한다.
                    console.log(deque.size);
                    break;
                case '6':               // 6: 덱이 비어있으면 1, 아니면 0을 출력한다.
                    console.log(deque.isEmpty() ? '1' : '0');
                    break;
                case '7':               // 7: 덱에 정수가 있다면 맨 앞의 정수를 출력한다. 없다면 -1을 대신 출력한다.
                    console.log(deque.printFirst());
                    break;
                case '8':               // 8: 덱에 정수가 있다면 맨 뒤의 정수를 출력한다. 없다면 -1을 대신 출력한다.
                    console.log(deque.printLast());
                    break;
            }
        }
    }
}

solution(input);

마지막 시도 - 성공 !

내 친구 지피티와 상의해본 결과 console.log가 자주 사용되어 시간 복잡도가 높아진다는 피드백을 받았다.

+ 문자열 함수 trim()은 반드시 필요할 때만 사용하라는 피드백도 받았다.

하지만 입력 값을 받을 때 인적 실수를 항상 고려하라는 교수님의 말씀이 떠올라서 ^__^ 제외는 안했다.

그래서 console.log 대신 출력 값을 저장하는 배열을 만들고 마지막에 배열을 출력해주었다.

const fs = require('fs');
let input = fs.readFileSync('/dev/stdin').toString().split('\n');

class Node {
    constructor(value) {
      this.value = value;
      this.next = null;
      this.prev = null;
    }
}
  
class Deque {
    constructor() {
        this.head = null;
        this.tail = null;
        this.size = 0;
    }

    isEmpty() {
        return this.size === 0;
    }

    addFront(value) {
        const newNode = new Node(value);

        if (this.isEmpty()) {
            this.head = newNode;
            this.tail = newNode;
        } 
        else {
            newNode.next = this.head;
            this.head.prev = newNode;
            this.head = newNode;
        }

        this.size++;
    }

    addRear(value) {
        const newNode = new Node(value);

        if (this.isEmpty()) {
            this.head = newNode;
            this.tail = newNode;
        } 
        else {
            newNode.prev = this.tail;
            this.tail.next = newNode;
            this.tail = newNode;
        }

        this.size++;
    }

    removeFront() {
        if (this.isEmpty()) {
            return '-1';
        }

        const removedValue = this.head.value;
        if (this.size === 1) {
            this.head = null;
            this.tail = null;
        } 
        else {
            this.head = this.head.next;
            this.head.prev = null;
        }

        this.size--;
        return removedValue;
    }

    removeRear() {
        if (this.isEmpty()) {
            return '-1';
        }

        const removedValue = this.tail.value;
        if (this.size === 1) {
            this.head = null;
            this.tail = null;
        } 
        else {
            this.tail = this.tail.prev;
            this.tail.next = null;
        }

        this.size--;
        return removedValue;
    }

    printFirst() {
        if (this.isEmpty()) {
            return '-1';
        }

        return this.head.value;
    }

    printLast() {
        if (this.isEmpty()) {
            return '-1';
        }

        return this.tail.value;
    }
}

function solution(input) {
    const deque = new Deque();
    const answer = [];
    
    for(let i = 1; i < +input[0] + 1; i++){
        if(input[i].trim().length > 1){
            let array = input[i].trim().split(' ');
            if(array[0] === '1'){        // 1 X: 정수 X를 덱의 앞에 넣는다. (1 ≤ X ≤ 100,000)
                deque.addFront(+array[1])
            }
            else if(array[0] === '2'){   // 2 X: 정수 X를 덱의 뒤에 넣는다. (1 ≤ X ≤ 100,000)
                deque.addRear(+array[1])
            }
        }
        else if(input[i].trim().length === 1){
            switch(input[i].trim()){
                case '3':               // 3: 덱에 정수가 있다면 맨 앞의 정수를 빼고 출력한다. 없다면 -1을 대신 출력한다.
                    answer.push(deque.removeFront());
                    break;
                case '4':               // 4: 덱에 정수가 있다면 맨 뒤의 정수를 빼고 출력한다. 없다면 -1을 대신 출력한다.
                    answer.push(deque.removeRear());
                    break;
                case '5':               // 5: 덱에 들어있는 정수의 개수를 출력한다.
                    answer.push(deque.size);
                    break;
                case '6':               // 6: 덱이 비어있으면 1, 아니면 0을 출력한다.
                    answer.push(deque.isEmpty() ? '1' : '0');
                    break;
                case '7':               // 7: 덱에 정수가 있다면 맨 앞의 정수를 출력한다. 없다면 -1을 대신 출력한다.
                    answer.push(deque.printFirst());
                    break;
                case '8':               // 8: 덱에 정수가 있다면 맨 뒤의 정수를 출력한다. 없다면 -1을 대신 출력한다.
                    answer.push(deque.printLast());
                    break;
            }
        }
    }

    console.log(answer.join('\n'));
}

solution(input);

결과 값을 한번에 출력해도 되는거면

 

이런 요구사항은 왜 있는 건지..ㅎㅎ

알 수 없는 백준

++

혹시 console.log 때문에 계속 오류가 난건가 하여 두번째 시도 + 결과 값 배열 저장하여 출력해보았는데 역시나 시간 초과가 발생한다 ^^

+ Recent posts