ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [CodeKata] 위클리 프로그래머스(2월 3주차)
    Algorithm 2021. 2. 15. 15:22
    반응형

    🥋 Ooooth!! (Level 2)

    코드카타에 들이는 시간이 많아졌지만, 그만큼 풀었을 때 쾌감도 정말 짜릿하다!

    당분간 3차 프로젝트를 위한 공부의 기간이기 떄문에, 바이오리듬 유지하면서 오전에 알고리즘에 몰두하도록 해야겠다.


    👊 2.15(월) / 멀쩡한 사각형

    📜 문제

     

    🧮 풀이

    풀이를 설계하는 과정을 먼저 설명해야 코드풀이가 가능할 것 같다.

    위 예시는, 8 * 12 박스의 대각선 경우이다. 대각선은 2 * 3 박스를 기준으로 총 4회 반복되었다.

    4의 의미를 고민했고, 이것이 너비(8)와 높이(12)의 최대공약수 임을 확인했다. (다른 경우로, 5 * 3 이라면 1이 되야겠다.)

     

    대각선 최소단위(너비, 높이를 최대공약수로 나눈)에서 빠진 박스 개수를, 최대공약수 만큼 곱해주면 전체 빠진 박스개수가 나온다.

    이를 전체 개수에서 빼줘서 정답을 도출하는 것으로 생각하고 문제를 풀었다.

    function getGCD(a,b) {
      let min = Math.min(a,b)
      let max = Math.max(a,b)
      let r = max % min
      return r === 0 ? min : getGCD(min, r)
    }
    
    function solution(w, h) {
      const gcd = getGCD(w,h)  
      const diagW = w/gcd;
      const diagH = h/gcd;
      const diagUnit = diagW*diagH - ((diagW - 1) * (diagH - 1));
      const diagTotal = diagUnit * gcd
      return w * h - diagTotal
    }
    1. 최대공약수를 구하는 getGCD() 함수를 만들었다. 유클리드 호제법을 기반으로 한 재귀함수이다.
    2. gcd 변수에 최대공약수를 담는다. 대각선 단위의 너비(diagW), 높이(diagH)는 본래 너비, 높이를 gcd로 나눈 값이다.
    3. 대각선 단위에서 남은 박스개수는 diagW와 diagH에서 1씩 뺀 값을 곱한 사각형이 된다. 
    4. 반대로, 빠진 박스개수대각선 단위 전체개수(diagW * diagH)에서 위 값((diagW - 1) * (diagH - 1))을 빼준다.
    5. 전체 빠진 박스개수(diagTotal)는, 대각선 단위 빠진 박스개수(diagUnit)와 gcd 의 곱이 된다.
    6. 이를 , 전체 개수인 w * h에서 빼면 총 남은 박스개수가 된다. 해당값을 반환하면 된다.

     

    🖇 리뷰

    - 유클리드 호제법

     

    두 수의 최대공약수를 구하는 방법은 2가지가 있다.

     

    1) 두 수를 각각 나눴을 때 나머지가 0인 수 중에 가장 큰 수가 최대공약수이다. 이를, 반복문으로 구한다. (범위는, 둘 중 작은수 이하) 

    let gcd = 0;
    let num = [a, b];
    
    for(let j = Math.min.apply(null, num) ; j >= 1 ; j--) { 
      if ((Math.min.apply(null, num)%j == 0) && (Math.max.apply(null, num)%j == 0)) { 
        gcd = j;
        break;
      } 
    }

     

    2) 유클리드 호제법으로 구한다. a % b = r 이라고 했을 때, (a, b)의 최대공약수는 (b, r)의 최대공약수와 같다는 원리이다. (a > b)

    이를, b % r = r' 로 다시 나눠서, 나머지가 0이 될 때까지 반복한다. 0이 된다면, 둘 중 작은값(b 혹은 r) 이 둘의 최대공약수가 된다.

    function gcdlcm(a, b) {
        var answer = [];
        var minNum = Math.min(a, b);
        var maxNum = Math.max(a, b);
        answer[0] = gcd(minNum, maxNum);
        answer[1] = lcm(minNum, maxNum);
        return answer;
    }
    
    // 최대공약수
    function gcd(minNum, maxNum){
      return (maxNum % minNum) === 0 ? minNum : gcd(minNum, maxNum % minNum);
    }
    
    // 최소공배수
    function lcm(minNum, maxNum){
      return minNum * maxNum / gcd(minNum, maxNum);
    }

    이처럼, 나머지가 0이면 minNum을, 아니라면 gcd() 함수를 재실행하는, gcd() 재귀함수를 활용하면 된다.

    최소공배수는, minNum 과 maxNum 의 곱에서 최대공약수를 나눠주면 된다.

    * 출처: swess 님의 블로그 (swess.tistory.com/13)

     

     

    - 모범예시

    // 유클리드 호제법을 이용한 최대 공약수 구하기
    function gcd(w, h) {    // 처음 W와 H를 받습니다.
    
        // W와 H의 나머지를 구합니다.
        const mod = w % h;
        // 만약 나머지가 0일 경우 H를 반환합니다.
        if (mod === 0) {
            return h;
        }
        // 만약 0이 아닐경우 W에 H를 넣고 H에 나머지인 mod를 넣어 해당 함수를 다시 호출해 줍니다.
        return gcd(h, mod);
    }
    
    function solution(w, h) {
        // 최대 공약수를 구해줍니다.
        const gcdVal = gcd(w, h);
        // 공식에 맞춰 사용
        return w * h - (w + h - gcdVal);
    }

    * 출처: noogoonaa.tistory.com/74

     

    전체 개수(w * h)에서, (w + h - gcd)를 빼면 된다고 하는데.... 사실 공식 자체가 이해가 되진 않았다. (이해가 되신분은 댓글을...😅)

    아마, 대각선이 꺾이는 부분에서 너비와 높이가 한 칸씩 겹치기 때문에 이를 빼준다는 개념인 것 같다.

     

    내 풀이는 코드도 길고 그만큼 계산량도 많지만, 반복적인 패턴에 대해서 이해하기엔 용이한 방법인 것 같다.


    👊 2.16(화) / 문자열 압축

    📜 문제

    요약하면, 문자열(s)를 1부터 전체 길이값까지 반복해서 잘라가면서, 겹치는 문자가 있으면 앞에 숫자를 더한다.

    이렇게 수정한 문자열의 길이들 중, 가장 짧은 케이스의 길이값이 결과가 되는 것이다.

     

    🧮 풀이

    function solution(s) {
      let minLen = 1000;
      for (let l = 1 ; l <= s.length ; l++) {
        let strArr = [];
        let i = 0
        while (i < s.length) {
          const strUnit = s.substr(i, l);
          const lastIdx = strArr.length - 1;
          if (strArr[lastIdx] === strUnit) {
            if (typeof strArr[lastIdx-1] === 'number') {
              strArr[lastIdx-1]++
            }
            else {
              strArr.splice(lastIdx, 0, 2)
            }
          }
          else {
            strArr.push(strUnit)
          }
          i += l
        }
        const strLen = strArr.join('').length
        minLen = strLen < minLen ? strLen : minLen;
      }
      return minLen
    }
    1. 최소값(결과)이 될 minLen 변수를 선언한다. 문자열이 최대 1000자이므로, 비교연산을 위해 1000이라는 값을 주었다.
    2. 2중 반복문을 활용한다. l은 문자열을 자를 단위길이가 된다. 범위는, 1부터 문자열(s)의 길이까지이다.
    3. 자른 문자들을 담는 strArr 배열을 선언한다. 자르기가 완료되면, join으로 합친 문자열 길이(strLen)를 최소값과 비교한다.
    4. i는 문자열을 자르기 시작하는 인덱스이다. while 반복문 내 조건처리가 끝나면, 단위길이(l)만큼 계속 더해준다.
    5. 첫 번째 조건은, strArr 끝값과 현재 자른 문자열(strUnit)과 같은지다. 같다면 두 번째 조건, 다르면 push를 한다.
    6. 두 번째 조건은, 숫자를 세는 것이다. 이미, 같은 문자열 앞에 숫자가 있다면 + 1, 없다면 문자열 앞에 2를 넣는다.(splice)

     

    🖇 리뷰

    const solution = s => {
      if (s.length === 1) return 1;
      let length = s.length;
      let prevValue = "";
      let cnt = 0;
      let result = [];
      for (let i = 1; i <= length / 2; i += 1) {
        let compArray = [];
        prevValue = s.substring(0, i);
        cnt = 1;
        compArray[0] = prevValue;
        for (let j = i, c = 0; j < length; j += i) {
          let nowValue = s.substring(j, j + i);
          if (nowValue === prevValue) {
            compArray[c] = ++cnt + nowValue;
          } else {
            cnt = 1;
            compArray[++c] = nowValue;
          }
          prevValue = nowValue;
        }
        result[i - 1] = compArray.reduce((total, cur) => (total += cur.length), 0);
      }
      return result.sort((a, b) => a - b)[0];
    };

    풀이이해가 쉽진 않지만, 풀이 포인트의 좋은점이 몇 가지 있어 가져와봤다.

    • s.length가 1인 경우, 1을 그대로 반환했다. 좋은 예외처리이다.
    • 또한, 문자열을 자르는 단위는 전체길이(s.length)의 절반까지만 한다. 이것이 넘어가면, s.length가 최소길이가 되기 때문이다.

    👊 2.17(수) / 삼각 달팽이

    📜 문제

     

    🧮 풀이

    function solution(n) {
      const ansArr = Array.from(new Array(n), () => []);
    
      let l = n;
      let num = 1;
      let i = 1;
      while (l > 0) {
        const lastIdx = n - i;
        for (let a = 0 ; a < l ; a++) {
          ansArr[2*(i-1) + a].splice(i-1, 0, num)
          num++
        }
        for (let b = 0 ; b < l-1 ; b++) {
          ansArr[lastIdx].splice(b+i, 0, num)
          num++
        }
        for (let c = 0 ; c < l-2 ; c++) {
          ansArr[lastIdx-c-1].splice(ansArr[lastIdx-c-1].length+1-i, 0, num)
          num++
        }
        l -= 3;
        i += 1;
      }
      return ansArr.reduce((acc, cur) => acc.concat(cur))
    }
    1. 삼각 달팽이를 말 그대로 코드로 옮겨놓은 풀이다. 먼저, 피라미드를 쌓을 2차원 배열(ansArr)을 만든다.
    2. l은 변의 길이이다. n = 6 인 경우, 달팽이는 바깥기준 왼쪽 6, 아래 5, 오른쪽 4개의 숫자를 두른다.
    3. 그 다음, 안쪽 변은 왼쪽 3, 아래 2, 오른쪽 1개의 숫자를 두른다. 그렇기 때문에, 반복마다 l을 3씩 빼준다.
    4. num은 우리가 넣을 숫자이다. 매번 숫자를 두를 때마다, num++로 숫자값을 증가시켜준다.
    5. i는 두르는 횟수라고 생각하자. 이 두르는 횟수는 각 변의 시작점이나 splice 인덱스를 잡는데 유용하다.
    6. 왼쪽(a)/시작점: [2*(i-1)] 부터이다. 첫 번째는 0번줄, 두 번째는 2번줄, 세 번째는 4번줄의 규칙성을 보이기 때문
    7. 왼쪽(a)/splice: 인덱스는 i-1이다. 첫 번째는 0번째, 두 번째는 1번째, 세 번째는 2번째 삽입 규칙성을 가짐
    8. 아래(b)/위치: lastIdx 이다. 이 lastIdx는 n-i 로, 맨 밑에서부터 한 칸씩 올라오는 값을 가짐
    9. 아래(b)/splice: 인덱스는 b+i이다. 첫 번째는 1번째 부터 5개, 두 번째는 2번째부터 2개 의 규칙성을 보이기 때문
    10. 오른쪽(c)/시작점: [lastIdx-1] 부터이다. 아래를 다 채웠으니, 그 바로 위부터 시작하기 위해 -1을 해준다.
    11. 오른쪽(c)/splice: 해당원소(ansArr[lastIdx-1-c]의 길이값에서 i씩 빼주면서 splice 인덱스를 잡아야 한다.

    설명이 길고 어렵지만, 삼각 달팽이를 직접 그려보면서 규칙성을 그대로 구현한다 생각하면 단순한 작업이다.

     

    안의 숫자값들을 처음엔 배열로 만들어서 넣어줬다. (Array.from(), 전체 길이는 n(n+1)/2, splice 시 num 대신 shift() 값)

    위처럼 풀었더니, 배열을 생성하고 shift() 하는데 있어 시간초과가 발생했다. 그래서, 이처럼 숫자누적으로 변경한 것이다.

     

    🖇 리뷰

    아~~~~~주 좋고 직관적인 풀이가 있어 가져왔다. 

    처음 answer 배열을, 미리 빈 값을 가진 달팽이로 만든다. 이를 통해, answer[x][y] 좌표값에 접근하면서 값을 넣을 수 있는 것이다.

    function solution(n) {
        const answer = new Array(n).fill().map((_, i) => new Array(i + 1));
        
        let count = 0;
        let x = -1;
        let y = 0;
        while (n > 0) {
            for (let i = 0; i < n; i++) answer[++x][y] = ++count;
            for (let i = 0; i < n - 1; i++) answer[x][++y] = ++count;
            for (let i = 0; i < n - 2; i++) answer[--x][--y] = ++count;
            n -= 3;
        }
        
        return answer.flatMap(e => e);
    }
    • 빈칸으로 이루어진 2차원 배열(answer)를 만들었다. 행은 Array(n), 열은 Array(i+1) 만큼 만들어준 것이다.
    • while 안의 세 개의 for문은 각각의 변을 의미한다. 또한, answer[x][y]로 2차원 배열 좌표에 숫자값(count)을 넣으면서 더해준다.
    • flatMap() 메서드는 2차원 배열을 1차원 배열로 가공해주는 메서드이다.

    👊 2.18(목) / 큰 수 만들기

    📜 문제

     

    🧮 풀이

    function solution(number, k) {
      while (k > 0) {
        let newNum = 0;
        for (let i = 0 ; i < number.length ; i++) {
          const cutNum = Number(number.slice(0,i) + number.slice(i+1));
          if (cutNum > newNum) {
            newNum = cutNum
          }
        }
        number = newNum.toString();
        k--;
      }
      return number;
    }

    이처럼, k번 반복하면서 한글자씩 자른 숫자의 최대값을 최신화하는 로직을 짰다.

    하지만, 실패 및 시간초과가 75% 발생하여서 좀 더 좋은 풀이를 찾아보았다.

    🖇 리뷰

    stack 을 이용한 풀이인데, 최대한 이해를 해보려고 노력했다.

    function solution(number, k) {
        let stack = [];
        for (let i = 0; i < number.length; i++) { 
            const now = number[i];         
            while (k > 0 && stack[stack.length - 1] < now) {
                stack.pop();
                k--;
            }
            stack.push(now);
        }
        stack.splice(stack.length - k, k);
        const answer = stack.join('');
    
        return answer;
    }
    
    1. 먼저, 정답 숫자들이 쌓일 stack 배열을 만들어준다.
    2. number 숫자들을 순회하면서 스택을 쌓아간다. 처음은, 바로 push가 된다.
    3. 두 번째 부터는, 현재값이 stack 끝값보다 크다면 stack 끝값을 pop() 한 뒤 현재값을 넣고 k를 감소시킨다.
    4. 만약, number 자체가 정답이면 pop() 이 일어나지 않을 것이다. 예) solution('1010', 2) => '1010'
    5. 이 때, k 역시 소모되지 않고 남아있으므로(2), 남은 k만큼 뒷 부분을 잘라줘야 우리가 원하는 길이만큼 남는다.

    👊 2.18(목) / 조이스틱

    📜 문제

     

    🧮 풀이

    function changeChar(l) {
      const charL = l.charCodeAt();
      return  charL < 78 ? charL - 65 : 91  - charL
    }
    
    function solution(name) {
      let nameArr = name.split('').map(l => changeChar(l));
      let stickIdx = 0;
      let stickDir = (nameArr[nameArr.length-1] !==0 && nameArr[nameArr.length-1] < nameArr[1]) || nameArr[1] === 0 ? -1 : 1;
      let answer = 0;
      answer += nameArr[stickIdx];
      nameArr[stickIdx] = 0;
      while (nameArr.reduce((acc, cur) => acc + cur) !== 0) {
        answer++;
        stickIdx = stickIdx + stickDir < 0 ? nameArr.length - 1 : stickIdx + stickDir;
        answer += nameArr[stickIdx];
        nameArr[stickIdx] = 0;
      }
      return answer
    }

     

     

    🖇 리뷰

    위처럼 풀었을 때, 테스트 케이스 11만 실패가 발생한다. 이유는, 한쪽으로만 가지 않는 경우때문 ('BABAAAB')

    findIndex() 메서드로 0이 아닌 값들의 위치를 찾아, 현재 인덱스와 거리를 비교했지만 런타임 에러가 발생했다.

    function solution(name) {
        let sum = 0;
        for (let i = 0; i < name.length; i++) {
            let diff = name[i].charCodeAt() - 'A'.charCodeAt();
            sum += diff > 13 ? 26 - diff : diff;
        }
    
        let minMove = name.length - 1;
        for (let i = 1; i < name.length; i++) {
            if (name[i] === 'A') {
                for (var j = i + 1; j < name.length; j++) {
                    if (name[j] !== 'A') {
                        break;
                    }
                }
    
                const left = i - 1;
                const right = name.length - j;
                minMove = Math.min(minMove, left > right ? left + right * 2 : left * 2 + right);
    
                i = j;
            }
        }
    
        return sum + minMove;
    }
    

    2단계인데도 이렇게 어렵다니... 1단계에 자만한 내가 부끄러워졌다.

    문제를 구현하는 로직도 로직이지만, 오늘 문제처럼 예외처리에 대한 고민이 많아진다.

    반응형
Designed by Tistory.