문제

https://www.acmicpc.net/problem/9251

 

LCS(Longest Common Subsequence, 최장 공통 부분 수열)문제는 두 수열이 주어졌을 때, 모두의 부분 수열이 되는 수열 중 가장 긴 것을 찾는 문제이다.

 

예를 들어, ACAYKPCAPCAK의 LCS는 ACAK가 된다.

풀이

const fs = require("fs");

// 문제 링크 : https://www.acmicpc.net/problem/9251
// 제출 시 path : /dev/stdin

const input = fs.readFileSync("/dev/stdin").toString().trim().split("\n");

const LCS = (str1, str2) => {
  const m = str1.length;
  const n = str2.length;

  const dp = new Array(m + 1).fill(null).map(() => new Array(n + 1).fill(0));

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (str1[i - 1] === str2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }

  let result = "";
  let i = m,
    j = n;

  while (i > 0 && j > 0) {
    if (str1[i - 1] === str2[j - 1]) {
      result = str1[i - 1] + result;
      i--;
      j--;
    } else if (dp[i - 1][j] > dp[i][j - 1]) {
      i--;
    } else {
      j--;
    }
  }

  return result;
}

const solution = (input) => {
  const [firstArr, secondArr] = input;
  const answer = LCS(firstArr, secondArr);
  return answer.length;
};

console.log(solution(input));

해설

for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (str1[i - 1] === str2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }

str1 = ACAYKP

str2 = CAPCAK

dp[i][j] j=0 j=1 j=2 j=3 j=4 j=5 j=6
i=0 0 0 0 0 0 0 0
i=1 0 0 1 1 1 1 1
i=2 0 1 1 1 2 2 2
i=3 0 1 2 2 2 3 3
i=4 0 1 2 2 2 3 3
i=5 0 1 2 2 2 3 4
i=6 0 1 2 3 3 3 4

i=0 -> j=0,1,2,3,4,5,6 다 비교. 

 

dp[1][1]:

ACAYKP 과 CAPCAK 비교

A !== C 이므로 

dp[1][1]은dp[0][1]과 dp[1][0] 중 최댓값 

dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) 

 

dp[1][2]:

ACAYKP 과 CAPCAK 비교

A === A 이므로 

dp[1][2]은 dp[0][1]+1 

dp[i][j] = dp[i - 1][j - 1] + 1; 

 

문제

https://www.acmicpc.net/problem/12865

 

준서가 여행에 필요하다고 생각하는 N개의 물건이 있다. 각 물건은 무게 W 가치 V를 가지는데, 해당 물건을 배낭에 넣어서 가면 준서가 V만큼 즐길 수 있다. 아직 행군을 해본 적이 없는 준서는 최대 K만큼의 무게만을 넣을 수 있는 배낭만 들고 다닐 수 있다. 준서가 최대한 즐거운 여행을 하기 위해 배낭에 넣을 수 있는 물건들의 가치의 최댓값을 알려주자.

 

첫 줄에 물품의 수 N(1 ≤ N ≤ 100)과 준서가 버틸 수 있는 무게 K(1 ≤ K ≤ 100,000)가 주어진다. 두 번째 줄부터 N개의 줄에 거쳐 각 물건의 무게 W(1 ≤ W ≤ 100,000)와 해당 물건의 가치 V(0 ≤ V ≤ 1,000)가 주어진다. 입력으로 주어지는 모든 수는 정수이다.

4 7 // N K
6 13 // W V
4 8 // W V
3 6 // W V
5 12 // W V

접근

배낭 문제는 짐을 쪼갤 수 있는 경우(무게가 소수일 수 있는 경우)와 짐을 쪼갤 수 없는 경우(이 경우 짐의 무게는 0 이상의 정수만 가능) 두 가지로 나눌 수 있는데, 짐을 쪼갤 수 있는 경우의 배낭 문제를 분할 가능 배낭 문제(Fractional) -Greedy 문제 , 짐을 쪼갤 수 없는 경우의 배낭 문제를 0-1 배낭 문제(0-1 Knapsack) - DP문제 라 부른다. - 위키백과

 

해당 문제는 0-1 배낭 문제(0-1 Knapsack)로 DP로 접근한다.

DP문제는 문제의 답을 구하는 규칙성(점화식)을 빨리 찾는게 관건이다.(개인생각, 2023.02.022)

결론부터 말하면 문제의 점화식은 아래와 같다: 

DP[i][j] = max(DP[i - 1][j], DP[i - 1][j - W[i]] + V[i]);

 

풀이

const fs = require("fs");

// 문제 링크 : https://www.acmicpc.net/problem/12865
// 제출 시 path : /dev/stdin

const input = fs
  .readFileSync("./input.txt")
  .toString()
  .trim()
  .split("\n")
  .map((v) => v.split(" ").map((v) => parseInt(v)));

const solution = (input) => {
  const [N, K] = input.shift();
  const weight = input.flat().filter((val, idx) => idx % 2 === 0);
  const value = input.flat().filter((val, idx) => idx % 2 !== 0);
  const dp = Array.from(Array(N + 1), () => Array(K + 1).fill(0));

  // 점화식: dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])

  for (let i = 1; i <= N; i++) {
    for (let j = 1; j <= K; i++) {
      if (j - weight[i] >= 0)
        dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
      dp[i][j] = dp[i - 1][j];
    }
  }

  return dp[N][K];
};
console.log(solution(input));

 

해설

DP[i][j] = max(DP[i - 1][j], DP[i - 1][j - W[i]] + V[i]); 인가?

인덱스 문제 때문에 arr[0] 부분을 형식상 채워준다.

 

row: 아이템1, 2, 3, 4

col: 현재 배낭의 무게

 

 

 

 

 

문제풀이는 이해가 가는데 계속 런타임 에러가 나는 이유를 모르겠다. 내일은 아래 보면서 이부분 디버깅 해봐야겠음.

let fs = require("fs");
let knapsack = fs.readFileSync("예제.txt").toString().split("\n");
const N = Number(knapsack[0].split(" ")[0]);
const K = Number(knapsack[0].split(" ")[1]);
knapsack = knapsack.map((element) =>
  element
    .trim()
    .split(" ")
    .map((value) => Number(value))
);
const weight = [];
const value = [];
for (let n = 0; n <= N; n++) {
  weight.push(knapsack[n][0]);
  value.push(knapsack[n][1]);
}

const dp = Array.from(new Array(N + 1), () => new Array(K + 1));

for (let i = 0; i <= N; i++) {
  dp[i][0] = 0;
}
for (let j = 0; j <= K; j++) {
  dp[0][j] = 0;
}
for (let j = 1; j <= K; j++) {
  if (knapsack[1][0] <= j) {
    dp[1][j] = knapsack[1][1];
  } else {
    dp[1][j] = 0;
  }
}

for (let i = 2; i <= N; i++) {
  for (let j = 1; j <= K; j++) {
    if (j < weight[i]) {
      dp[i][j] = dp[i - 1][j];
    } else {
      // 현재 물건을 포함하지 않는 경우 : dp[i - 1][j]
      // 현재 물건을 포함하는 경우: dp[i-1][j - weight[i]] + value[i]
      dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    }
  }
}
console.log(dp[N][K]);

문제

https://www.acmicpc.net/problem/1463

접근

1을 빼거나 2나 3을 나누어 1로 만드는데 걸리는 연산 횟수의 최솟값을 구하는 문제입니다.

첫째줄는 1이상 10^6이하의 정수 N이 주어집니다. 수의 범위가 크기 때문에 계산가능한 모든 수를 구하는 완전탐색 등은 이 문제에 어울리지 않은 접근법입니다. 한 번 연산한 값을 기억해 두고 문제를 풀 수 있는 DP가 적절해 보입니다.

 

DP의 인덱스는 숫자를 뜻합니다.

D[N-1]: N에서 1을 빼서 1을 만드는 연산 횟수

D[N/2]: N에서 2를 나눠 1을 만드는 연산 횟수

D[N/3]: N에서 3을 나눠 1을 만드는 연산 횟수

 

D[N] 숫자 N이 1이 되는데 걸리는 최소한의 연산 횟수를 저장합니다.

N이 2로 나눠진다면 D[N] = D[N-1] + 1 과 D[N/2] + 1 중 더 작은 값, N이 3으로 나눠진다면 D[N] = D[N-1] + 1 과 D[N/3] + 1 중 더 작은 값이 됩니다.

각 경우의 수 뒤에 +1 이 되는 이유는 D[N-1],D[N/2],D[N/3] 이 각 N을 구하기 이전의 연산횟수를 나타내기 때문입니다.

 

예를 한 번 들어볼까요?

D[N-1] + 1 :

4를 1을 빼서 1로 만드는 연산 횟수는 3입니다. 4-1-1-1=1

3을 1을 빼서 1로 만드는 연산 횟수는 2입니다. 3-1-1=1

2를 1을 빼서 1로 만드는 연산 횟수는 1입니다. 2-1=1

이를 뒤집어보면 숫자 N을 1을 만들 때까지 1을 빼는 연산의 횟수는 N-1을 1을 만들 때까지 1을 빼는 연산의 횟수 +1 임을 알 수 있습니다. 

D[N/2] + 1

2를 2로 나눠서 1로 만드는 연산 횟수는 1입니다. 2/2 = 1

4를 2로 나눠서 1로 만드는 연산 횟수는 2입니다. 4/2 = 2, 2/2 =1 

8을 2로 나눠서 1로 만드는 연산 횟수는 3입니다. 8/2 = 4, 4/2 = 2, 2/2 = 1

이를 뒤집어보면 숫자 N을 1을 만들 때까지 2를 나누는 연산의 횟수는 N-1을 1을 만들 때까지 N을 나누는 연산의 횟수 +1 임을 알 수 있습니다.

D[N/3] + 1도 위와 D[N/2] + 1의 경우와 마찬가지입니다.

 

문제를 한 번 풀어볼까요? N=4 로 예를 들어봅시다.

우선 D[0]은 문제 범위 밖이므로 0으로 처리해 줍니다. 

D[1] = 0 입니다. 1을 1로 만들 수 있는 연산의 횟수는 0회이기 때문입니다.

D[4] = Math.min( D[3] + 1 , D[4/2] +1) = Math.min( 1 + 1, 2 + 1 ) = Math.min(2, 3) = 2 가 됩니다.

 

이를 코드로 풀어보면 아래와 같습니다.

풀이

const input = fs
  .readFileSync("/dev/stdin")
  .toString()
  .trim()
  .split("\n")
  .map((v) => v.split(" ").map((v) => parseInt(v)));
const solution = (input) => {
  const N = +input;
  const DP = Array.from({ length: N + 1 }, () => 0);

  for (let i = 2; i < N + 1; i++) {
    DP[i] = DP[i - 1] + 1; // 1을 빼서 1로 만드는 연산의 횟수로 우선 설정

    if (i % 2 === 0) {
      // 2로 나눠진다면,
      DP[i] = Math.min(DP[i], DP[i / 2] + 1); // 1을 빼서 1로 만드는 연산의 횟수와  2로 나눠 1로 만드는 횟수 중 더 작은 경우로 선택.
    }

    if (i % 3 === 0) {
      DP[i] = Math.min(DP[i], DP[i / 3] + 1);
    }
  }

  return DP[N];
};

console.log(solution(input)); // 3

 

문제

https://www.acmicpc.net/problem/10026

 

10026번: 적록색약

적록색약은 빨간색과 초록색의 차이를 거의 느끼지 못한다. 따라서, 적록색약인 사람이 보는 그림은 아닌 사람이 보는 그림과는 좀 다를 수 있다. 크기가 N×N인 그리드의 각 칸에 R(빨강), G(초록)

www.acmicpc.net

접근

기본적인 그래프 탐색 문제. 

적록색맹인 경우와 적록색맹이 아닌 경우로 visited 배열을 따로 만들어 계산한다.

풀이

참조(https://zzang9ha.tistory.com/227)

 

시도1: 우선 notRBG부터 구하기 

expected: 4

result: 2

debugging: visited 의 문제였다.

const wrongVisited = Array(N).fill(Array(N).fill(false));
  const rightVisited = Array.from(Array(N), () => Array(N).fill(false));

이유는 모르겠지만, x=0, y=0 일때, visited[x][y] = true 를 진행했을 때 전자는 이렇게 y=0 부분이 전부 true 로 바뀐 걸 볼 수 있다. 

후자는 예상했던 것처럼 딱 visitied[0][0]부분만 true 로 표시된 걸 알 수 있다.

근데 웃긴건 전자 후자 모두 맨 처음 상태 콘솔찍어보면 같은 모양이다. 

 이유는 모르겠지만, 이런 개떡같은 이유로 40분을 날린 것이 원통하다. 우선은 문제 풀이에 필요한 기본 논리는 파악한 것에 만족하고 넘어가자. 앞으로는 절대 visited 후자로만 쓸 것이다.

const solution = (input) => {
  const N = +input.shift();
  const graph = input.map((v) => v.split(""));

  const dx = [0, 1, 0, -1];
  const dy = [1, 0, -1, 0];
  const visited = Array(N).fill(Array(N).fill(false));
  let cnt = 0;

  const isInRange = (nx, ny) => {
    if (nx >= 0 && ny >= 0 && nx < N && ny < N) return true;
    return false;
  };

  for (let i = 0; i < N; i++) {
    for (let j = 0; j < N; j++) {
     
     const dfs = (x, y) => {
        visited[x][y] = true;

        for (let k = 0; k < dx.length; k++) {
          const nx = x + dx[k];
          const ny = y + dy[k];

          if (
            isInRange(nx, ny) &&
            !visited[nx][ny] &&
            graph[nx][ny] === graph[x][y]
          ) {
            dfs(nx, ny);
          }
        }
      };

      if (!visited[i][j]) {
        dfs(i, j);
        cnt++;
      }
    }
  }
  return cnt;
};

[NEXTSTEP] TDD with React

 

들어가며

 NEXTSTEP 에서 진행하는 TDD with React 2기 프로그램에 운이 좋게 참가하게 되었다. 

 잘 모르시는 분들을 위해 간단한 설명을 덧붙이자면, NEXTSTEP은 개발 교육 프로그램으로, 우아한형제들 개발자분들이 연사로 참여하신다. 내가 참여한 이번 TDD with React 코스는 2월 6일(월) ~ 3월 28일(화)까지 총 8주간 진행되는교육프로그램으로, TDD(Test-Driven Development)와 CDD(Component-Driven Development) 등 React로 클린코드 개발 학습을 목표로 한다.

 

 이번주는 온보딩 미션 기간이었다. 이번 글에서는 온보딩 미션으로 리액트로 계산기 구현 미션을 진행하며 배운 것을 정리해보고자 한다. 

 

+ 추가로 파이널 미션이 있다.

 

미션을 진행하며 내가 배운 것들 - React 관련

이번 미션의 리뷰어님은 오태은님이셨다. 리뷰어님이 남기신 코멘트에서 다시 찾아봐야겠다 싶은 부분 중 리액트 관련 내용만 추려봤다:

1) props 에서 익명함수 가능한 자제하기

2) 컴포넌트 분리 (a.k.a 선언적이다의 뜻)

 

1)  props 에서 익명함수 가능한 자제하기

1-1) 불필요한 레퍼런스 변경 막기 

 리액트에서 이벤트를 핸들링할 때 인자가 필요한 콜백함수를 넘겨주어야하는 경우가 있다. 콜백함수를 인자를 넘겨주는 방법에는 익명함수, 이벤트 객체 사용으로 2가지가 있지만, 그중 익명함수를 쓰는 것이 대표적이라고 한다. 따라서, 인자를 넘겨주는 콜백함수를 쓰는 경우를 대비해 익명함수로 콜백을 넘겨주는 걸 습관으로 들였었다.

 하지만 콜백을 익명함수로 넘길 경우 JSX가 호출될 때마다 새로운 함수를 생성하게 된다고 한다. 즉, 그 때마다 새로운 레퍼런스를 참조하게 되는 것이다. 계속해서 새로운 레퍼런스를 참조하게 되면 vdom을 실제dom에 반영하는 과정이나 useMemo를 사용하는 경우에 좋지 않다고 한다. 따라서 인자를 넘겨주어야 하는 콜백이 아니면 익명함수로 넘기지 않는 것이 성능상 좋다.

 

1-2) 객체 타입은 useMemouseCallback 사용하기

 첨언으로 useMemo와 useCallback에 대해서도 언급해주셨는데, 이부분은 아직 다루지 않은 개념이라 낯설어 찾아봤다. 간단히 말해, useMemo는 한 번 계산한 값을 다시 쓰는 것이고 useCallback은 한 번 사용한 함수 결과를 저장했다 재사용하는 것이다. 

 JSX 호출마다 객체에 대한 참조값도 계속 바뀌게 되는데, 호출마다 새로운 레퍼런스를 참조하는 것은 앞서 언급했듯 앱의 성능을 낮춘다. 따라서, object 나 array 같이 객체 사용에 있어서 useMemo 를 사용하는 것이 좋다.

 

2) 컴포넌트 분리  (a.k.a 선언적이다의 뜻)

리액트를 제대로 공부한지 얼마안됐지만 항상 언급되는 것이 제대로된 '컴포넌트의 분리'인듯 하다. 리액트의 핵심 개념인 컴포넌트를 어떻게 재사용가능하게 분리하는 지가 중요한가보다. 이와 관련해서는 그냥 (1)컴포넌트는 재사용이 가능해야 하고, (2)앱 전반적 비즈니스 로직별로 굵직하게 나누는게 좋다고 알고 있다. 그런데 이걸 의도해서 적용한 건 아니었고...주어진 기본 HTML의 구조가 크게 4개로 나누어지는 것 같아, 그 역할대로 나누었을 뿐이었다. 그런데 '선언적'이라는 용어를 쓰시며 뜻밖의 칭찬을 들었다. 그래서 찾아보게 되었다.

 

2-1) 리액트 컴포넌트 분리는 어떤 기준으로 하는 게 좋을까?

 

 해당 물음에 대한 답을 찾아보다 FEConf2021의 원지혁님 강의와 토스슬래시의 한재엽님의 강의를 보게 되었다. 두 분의 의견에 따르면, "컴포넌트를 쪼개는 절대적 기준 같은 것은 없다. 하지만 상대적으로 잘 쪼개는 컴포넌트 분리 기준은 존재한다."는 것이었다.두 분 모두 '유지보수를 쉽게 하는'것에 기준을 두고 컴포넌트 분리를 설명하신 것이 인상 깊었다. 유지보수를 쉽게 하려면 어떻게 해야할까? 미래에 변할 수 있는 것과 상대적으로 잘 변하지 않을 것 구분하는 것이다.

 

 원지혁님은 컴포넌트의 구성을 스타일, 로직, 전역상태, 리모드 데이터 스키마(데이터)로 분리하셨다. 유지보수를 위해 변화가 가장 잘 일어나는 데이터 모델에 따라 컴포넌트 분리를 해주는 것을 추천하셨다. 한재엽님은 변하는 것에 앞서 설명된 데이터를 데이터계산과 상호작용으로 나누셨다. 결국 둘다 데이터 관리부분인데 처음부터 제공되는 데이터냐, 사용자의 액션이 있어야 계산되는 데이터냐에 따라 한 번 더 나누신듯하다. 변하는 부분을 데이터, 변하지 않는 부분을 UI로 잡고 분리한다는 점에서 원지혁님과 의견이 같았다. 두 분 다 컴포넌트 분리의 기준을 유지보수로 잡고 계시고, 상대적으로 잘 변하는 것, 즉 컴포넌트가 다루는 데이터를 기준으로 컴포넌트를 분리하는 것을 제안하신다는 데에서 동일했다.

 

 이것을 이번 계산기 모델에 적용해보았다. Total, Digits, Modifier, Operations 4가지 컴포넌트가 모두 useCalculator를 통해 calculator 데이터 모델 하나만 필요하다. calculator 모델에는 4개의 메서드 -숫자입력(addDigit), 연산자입력(addOperation), 계산(caculate), 리셋(reset)-상태관리 부분-현재 상태(state)와 현재 상태를 변경하는 calcReducer-가 있다. 각 컴포넌트 Total -> state.total, Digits -> addDigit, Operations -> addOperation, calculate, Modifier -> reset 에 의존하고 있다. 아마 데이터 모델을 calculator 하나에 다 때려박지 않고 각각 나눠서 정의했다고 친다면, 필요한 데이터에 따라 컴포넌트를 잘 나눈 것이기 때문에 칭찬을 받은 게 아닐까? 다만 이번 계산기 미션은 온보딩이고 작은 앱인만큼, 굳이 데이터를 여러개로 나눌 필요가 없다고 생각하셔서 데이터 모델을 분리하는 것까지는 언급하지 않으셨던게 아닐까하는 생각이 든다. 

 

2-2) 리액트 컴포넌트 분리를 '선언적'으로 한다는 것은 어떤 의미일까?

 

"...컴포넌트는 선언적이다: 컴포넌트는 개발자로 하여금 어떻게(HOW)UI를 그려야 할지에 대한 고민 없이 주어진 상태에 기반해서 어(What) UI가 생겨야하는지 적기만 하면 되도록 도와준다. - Thinking in Relay

 의도한 것은 아니었지만 '선언적이다'는 용어의 의미를 좀 더 명료하게 알기 위해 찾아보다보니 원지혁님의 강의가 상단에 떴다.

 

 결국 선언적이란 건 무엇인지에 대해서만 짧게 추리자면, 선언적(Declarative)의 뜻은 누가봐도 명료하도록 컴포넌트의 분리를 나눈 것이다. 반대 개념으로 언급되는 명령형(Impertaive)가  로직의 순서대로 구현하여 흐름을 파악하기 위해 순서대로 코드를 전부 읽어야 한다면, 선언적이라는 것은 그중 공통된 주제(데이터)를 가진 로직(HOW)를 엮어 주제별(WHAT)으로 잘 뭉쳤다는 뜻인 것 같다. 

 

 사실 이렇게 컴포넌트를 나눈 것은 다른 미션수행자의 안목을 빌려온 것인데, 다음 미션부터는 어떻게 데이터를 관리하고 유저와 상호작용할 지에 대한 HOW를 고민해보고, 그 중 공통된 HOW들을 뭉쳐 WHAT 으로 잘 선언하는 설계하는 고민에 시간을 들여야겠다고 생각했다.

 

미션을 진행하며 내가 배운 것들 - React 외의 것들

리액트와 관련된 부분 이외의 자잘한 부분들을 정리하면 아래와 같다.

1) 왜 jsx 는 확장자를 js 로 해도 돌아가나

2) 정규식을 제대로 사용하기

3) 에러핸들링을 통해 사용자 경험 높이기

4) prettier 설정, 정확히 알고 하기:

4-1) 디폴트 설정 굳이 중복하지 말기 4-2) extend: [prettier: recommend] 4-3) VSCode 에서이 설정과 프로젝트별 설정

정규식 공부하자
에러핸들링 공부하자

 

마무리하며

 간단한 미션인데도 복습하고 공부할 게 꽤 많았다.

 

 리액트 문법에 대해서만 배웠던 현재 단계에서, 한 단계 더 들어가 1)리액트 최적화 2)리액트 컴포넌트 분리 등 어떻게 더 '잘' 짤 것인지에 대한 고민과 공부를 할 수 있었던 시간이었다. 이러한 면에서 리액트 '문법만' 다룰 줄 아는 학습 단계에 계신 분들께 감히 추천하고 싶다.

 

 또 각 미션별로 이렇게 회고를 쓰는 것이 학습에 정말 많이 도움이 되었다는 구하지 않은 조언도 덧붙이고 싶다. 회고를 쓰며 '이 미션에서 얻은 것'을 정리하기 위해 리뷰를 학습 방향 지표로 삼아, 추가적으로 공부할 수 있었다. 아니었더라면 단순히 리뷰를 읽고 끝냈을 것이다...

 

 이제 겨우 온보딩일 뿐이지만 배운 것이 많은 한 주였다. 우선 미션을 하고, 그 후 리뷰에 따라 학습하고 수정해나가나는 자세에서 진정한 Learn by Doing 을 느낄 수 있었다. 그냥 Doing 만 하면 학습효과를 이만큼 못가져간다는 걸 회고를 쓰며 깨달았다. 마지막까지도 꾸준히 배우고 회고하는 자세를 가져갈 수 있도록 노력해야겠다. 

 


2023.03.19 Updated

문제

https://school.programmers.co.kr/learn/courses/30/lessons/49994

접근

게임 캐릭터가 지나간 길 중 캐릭터가 처음 걸어본 길의 길이를 구하는 문제이다.

- 그래프탐색

- 중복배열체크 -> 중복에 있다면 바로 그때까지의 결과 return

- 주의사항: 그래프를 벗어나는 이동은 무시한다.

- 그래프의 크기는 10*10 정사각형이다.

 

https://developerm.tistory.com/196

 

[Programmers/JS] 방문 길이 / 프로그래머스 코딩 테스트 연습

방문 길이 문제 설명 게임 캐릭터를 4가지 명령어를 통해 움직이려 합니다. 명령어는 다음과 같습니다. U: 위쪽으로 한 칸 가기 D: 아래쪽으로 한 칸 가기 R: 오른쪽으로 한 칸 가기 L: 왼쪽으로 한

developerm.tistory.com

check 배열 쓰는 방식을 굉장히 특이하게 하는 문제더라.

 

 

풀이

function solution(input) {
  const dirs = input.split("");
  const visited = new Set();
  const pos = { x: 0, y: 0 };

  for (const dir of dirs) {
    const prevPos = {
      x: pos.x,
      y: pos.y,
    };

    if (dir === "U" && pos.y < 5) pos.y += 1;
    if (dir === "D" && pos.y > -5) pos.y -= 1;
    if (dir === "R" && pos.x < 5) pos.x += 1;
    if (dir === "L" && pos.x > -5) pos.x -= 1;

    const move1 = prevPos.x + "" + prevPos.y + pos.x + pos.y;
    const move2 = pos.x + "" + pos.y + prevPos.x + prevPos.y;

    if (move1 != move2 && !visited.has(move1) && !visited.has(move2)) {
      visited.add(move1);
      visited.add(move2);
    }
  }

  return visited.size / 2;
}

* 마지막에 둘 다 넣어주는 이유:

 (0,0) -> (1,2) 를 지나갔다고 치자. 돌고 돌다보니 다시 (1,0) 지점으로 왔다. 그리고 다시 (1,0) 에서 (0,0) 가는 형태였다. 

이경우 이 길은 지나갔기 때문에 탐색을 멈추고 결과를 도출해야 한다. 

 (0,0) -> (1,0)

 (1,0) -> (0,0) 

를 visited 가 두 경로를 모두 가지고 있지 않으면 아직 방문하지 않은 새로운 경로로 착각해 또 경로 수를 카운팅한다. 

 

'Board > 알고리즘' 카테고리의 다른 글

백준 1463 자바스크립트  (0) 2023.02.15
백준 10026 자바스크립트  (0) 2023.02.13
프로그래머스 멀쩡한 사각형  (0) 2023.02.10
프로그래머스 예산  (0) 2023.02.10
프로그래머스 소수 만들기  (0) 2023.02.10

문제

https://school.programmers.co.kr/learn/courses/30/lessons/62048

접근

1. 사각형, 즉 이중배열 -> 그래프 탐색

2. 대각선, 즉 row[x]-row[y] = row[x+1]-row[y+1]

 

접근 방식이 아예 달랐다. 

https://codingwell.tistory.com/30

 

솔직히 한 번 푸는 지금은 왜 이런식에 도달하는지 이해가 안간다.

일단은 직사각형의 대각선 문제는 이렇게 접근할 수 있다는 정도만 이해하기로 한다.

직사각형에서 직사각형의 대각선이 지나가는 1*1정사각형의 갯수(W+H-최대공약수) 이다.

 

다시 풀어보면,

1. 최대공약수(GCD) 구하기

2. (W*H) - (W+H-GCD)

풀이

const getGCD = (num1, num2) => {
  let gcd = 1;

  for (let i = 2; i <= Math.min(num1, num2); i++) {
    if (num1 % i === 0 && num2 % i === 0) {
      gcd = i;
    }
  }

  return gcd;
};

function solution(W, H) {
  const GCD = getGCD(W, H);
  return W * H - (W + H - GCD);
}

'Board > 알고리즘' 카테고리의 다른 글

백준 10026 자바스크립트  (0) 2023.02.13
프로그래머스 방문 길이  (0) 2023.02.11
프로그래머스 예산  (0) 2023.02.10
프로그래머스 소수 만들기  (0) 2023.02.10
백준 9663 N-Queen  (0) 2023.02.08

문제

https://school.programmers.co.kr/learn/courses/30/lessons/12982

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

접근

 

앞서 풀었던 조합 문제와 거의 같다. 

주어진 부서별 예산 n 개의 합이 주어진 예산 m 을 넘지 않을 때까지 합한다.

만약 sum >= m 이면 해당 재귀 종료 준비를 한다.

 

풀이

접근1. 재귀

결과: 틀렸습니다! 재귀로 풀었는데 계속 다른 두 케이스가 같은 답이 나왔다.

 

접근2: 그냥 단순하게 접근하기로. 반복문

결과: 틀렸습니다! 답은 맞게 나오는데 테스트 케이스 몇 개에서 걸렸다.


  const N = d.length;
  let answer = 0;
  let sum = 0;

  for (let i = 0; i < N; i++) {
    sum += sortedD[i];
    if (sum > budget) {
      answer = i;
      break;
    }
  }
  if (sum <= budget) answer = d.length;

  return answer;

접근3: sort 추가

결과: 맞았습니다!

  const sortedD = d.sort((a, b) => a - b);
  const N = d.length;
  let answer = 0;
  let sum = 0;

  for (let i = 0; i < N; i++) {
    sum += sortedD[i];
    if (sum > budget) {
      answer = i;
      break;
    }
  }
  if (sum <= budget) answer = d.length;

  return answer;

function solution(d, budget) {
    d.sort((a, b) => a - b);

    while (d.reduce((a, b) => (a + b), 0) > budget) d.pop();

    return d.length;
}

'Board > 알고리즘' 카테고리의 다른 글

프로그래머스 방문 길이  (0) 2023.02.11
프로그래머스 멀쩡한 사각형  (0) 2023.02.10
프로그래머스 소수 만들기  (0) 2023.02.10
백준 9663 N-Queen  (0) 2023.02.08
백준 15652 N과 M(4)  (0) 2023.02.05

문제

https://school.programmers.co.kr/learn/courses/30/lessons/12977

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

접근

1) 주어진 수 n(숫자 배열의 길이) 개 중 3개의 수를 골라 합을 만드는 경우의 수 -> 조합문제, dfs

2) 합이 소수인지 확인한다. (아래 isPrime 함수를 이용한다.)

3) 소수라면 경우의 수 +1 을 한다.

풀이

function isPrime(num) {
  for (let i = 2, s = Math.sqrt(num); i <= s; i++) {
    if (num % i === 0) return false;
  }
  return num > 1;
}

function solution(nums) {
  const N = nums.length;
  const M = 3;
  const checked = Array(N).fill(false);
  let sum = 0;
  let answer = 0;

  function dfs(level, start) {
    if (level === M) {
      if (isPrime(sum)) answer++;
      return;
    }

    for (let i = start; i < N; i++) {
      if (checked[i]) continue;

      checked[i] = true;
      sum += nums[i];
      dfs(level + 1, i);
      sum -= nums[i];
      checked[i] = false;
    }
  }

  dfs(0, 0);

  return answer;
}

'Board > 알고리즘' 카테고리의 다른 글

프로그래머스 멀쩡한 사각형  (0) 2023.02.10
프로그래머스 예산  (0) 2023.02.10
백준 9663 N-Queen  (0) 2023.02.08
백준 15652 N과 M(4)  (0) 2023.02.05
백준 15651 N과 M(3)  (0) 2023.02.05

문제

https://www.acmicpc.net/problem/9663

 

9663번: N-Queen

N-Queen 문제는 크기가 N × N인 체스판 위에 퀸 N개를 서로 공격할 수 없게 놓는 문제이다. N이 주어졌을 때, 퀸을 놓는 방법의 수를 구하는 프로그램을 작성하시오.

www.acmicpc.net

접근

2가지 방법이 있다: 

- 각 스퀘어를 전부 탐색하며 조건을 찾는다. => n^n 

- 한 행에 하나의 퀸이 온다는 것을 이용해 행별로 탐색 => n (풀이 참조)

 

2번째의 경우가 훨씬 시간복잡도가 줄기 때문에 두 번째 방법으로 간다

즉, 행이나 열 하나를 잡고 그 행에 첫 퀸을 두었을 때 다음 행이나 열을 돌면서 경우의 수를 탐색한다. 

 

풀이

const input = require("fs").readFileSync("/dev/stdin").toString();

const solution = (input) => {
  const N = +input;
  const row = Array(N).fill(0);
  let cnt = 0;

  const isPossible = (x) => {
    for (let i = 0; i < x; i++) {
      if (row[x] === row[i]) return false;
      if (x - i === Math.abs(row[x] - row[i])) return false;
    }
    return true;
  };

  const dfs = (x) => {
    if (x === N) {
      cnt++;
      return;
    }

    for (let y = 0; y < N; y++) {
      row[x] = y; 
      if (isPossible(x)) {
        dfs(x + 1); 
      }
    }
  };

  dfs(0);

  return cnt;
};

console.log(solution(input));

'Board > 알고리즘' 카테고리의 다른 글

프로그래머스 예산  (0) 2023.02.10
프로그래머스 소수 만들기  (0) 2023.02.10
백준 15652 N과 M(4)  (0) 2023.02.05
백준 15651 N과 M(3)  (0) 2023.02.05
백준 15650 N과 M(2)  (0) 2023.02.05

문제

https://www.acmicpc.net/problem/15652

 

15652번: N과 M (4)

한 줄에 하나씩 문제의 조건을 만족하는 수열을 출력한다. 중복되는 수열을 여러 번 출력하면 안되며, 각 수열은 공백으로 구분해서 출력해야 한다. 수열은 사전 순으로 증가하는 순서로 출력해

www.acmicpc.net

접근

자연수 N과 M이 주어졌을 때, 아래 조건을 만족하는 길이가 M인 수열을 모두 구하는 프로그램을 작성하시오.

  • 1부터 N까지 자연수 중에서 M개를 고른 수열
  • 같은 수를 여러 번 골라도 된다.
  • 고른 수열은 비내림차순이어야 한다.

➡️ 어렵게 생각할 것 없이 중복을 허용하는 순서가 상관없는 경우의 수를 구하라는 것이다: 중복순열. 순열 문제였던 백준 15650 N과 N(2) 문제에서 중복체크하는 checked 배열만 없애주면 된다.

풀이

const input = require("fs")
  .readFileSync("/dev/stdin")
  .toString()
  .trim()
  .split(" ")
  .map(Number);

const [N, M] = input;
const output = [];
let result = "";

function dfs(level, start) {
  if (level === M) {
    result += `${output.join(" ")}\n`;
    return;
  }

  for (let i = start; i < N; i++) {
    output.push(i + 1);
    dfs(level + 1, i);
    output.pop();
  }
}

dfs(0, 0);

console.log(result);
 

'Board > 알고리즘' 카테고리의 다른 글

프로그래머스 소수 만들기  (0) 2023.02.10
백준 9663 N-Queen  (0) 2023.02.08
백준 15651 N과 M(3)  (0) 2023.02.05
백준 15650 N과 M(2)  (0) 2023.02.05
백준 15649 N과 M(1)  (0) 2023.02.05

문제

https://www.acmicpc.net/problem/15651

 

15651번: N과 M (3)

한 줄에 하나씩 문제의 조건을 만족하는 수열을 출력한다. 중복되는 수열을 여러 번 출력하면 안되며, 각 수열은 공백으로 구분해서 출력해야 한다. 수열은 사전 순으로 증가하는 순서로 출력해

www.acmicpc.net

접근

백준 15651 N과 M(1) 과 같이 순열 문제이다.

단, 중복을 허용하기에 check 배열의 기능을 삭제해 주었다.

풀이

const input = require("fs")
  .readFileSync("/dev/stdin")
  .toString()
  .trim()
  .split(" ")
  .map(Number);

const [N, M] = input;
const output = [];
let result = "";
const checked = Array(N).fill(false);

function dfs(level, start) {

  if (level === M) {
    result += `${output.join(" ")}\n`;
    return;
  }
  
  for (let i = start; i < N; i++) {
    if (checked[i]) continue; // backtracking
    checked[i] = true;
    output.push(i + 1);
    dfs(level + 1, i);
    output.pop();
    checked[i] = false;
  }
}

dfs(0, 0);

console.log(result);

'Board > 알고리즘' 카테고리의 다른 글

백준 9663 N-Queen  (0) 2023.02.08
백준 15652 N과 M(4)  (0) 2023.02.05
백준 15650 N과 M(2)  (0) 2023.02.05
백준 15649 N과 M(1)  (0) 2023.02.05
백준 14502 연구소 (JavaScript, Node.js)  (0) 2023.01.31

+ Recent posts