[프로그래머스 0레벨] 안전지대

이민선(Jasmine)·2023년 1월 16일
0
post-thumbnail

우선 자축부터하고 시작하자면

프로그래머스 0레벨 모두 해결!!
👏🏻👏🏻👏🏻👏🏻👏🏻👏🏻👏🏻👏🏻👏🏻👏🏻👏🏻👏🏻👏🏻👏🏻
추카추카 쟤스민 ~!~!
원래 저번달에 마무리하고 싶었지만 평행이랑 안전지대가 너무 어려워서 그만.. 그래도 겨우 어찌저찌 끝내기는 했다 휴
이제 진짜 코딩을 시작하는 것 같은 느낌이다 ㅋㅋㅋ 얼른 1레벨도 끝낼 수 있길 !!

내가 제일루 어려워했던 안전지대 코드는 아래와 같다. (억지스러움 감안 ㅋㅋ)

function solution(board) {  
  const leftRight = board.map((v)=> v.map((w,i,a)=> a[i+1] === 1 || a[i-1] === 1 ? 1 : w));
  const risky =  leftRight.map((v,i)=> v.map((w,j)=> w === 1  ?  [[i,j], [i-1,j],[i+1,j]]: w).flat());
   let feas = [];
    for (let v of risky){
        for (let w of v){
            w !== 0 ? feas.push(w) : null;
        }
    }
    let ans = [];
for (let v of feas) v[0] >= 0 & v[0] < board.length ? ans.push(String(v)) : null;
    const boardSize = Math.pow(board.length, 2);
    return boardSize - [...new Set(ans)].length;

}

역시 map무새라 첫 코드부터 map 등장.
leftRight변수에서, 각 행마다 왼쪽 또는 오른쪽에 있는 수가 1이면 자신도 1로 바꿔주는 작업을 먼저했다. (위 아래는 아직 반영이 안됨.)

그 다음에는 1이 들어있는 원소일 경우 i) 자신의 이전 행이면서 자신의 열과 같은 경우, ii)자신의 다음행이면서 자신의 열과 같은 경우의 인덱스를 모두 뽑아냈다. (이후 중복 제거 필요)
그리고 이중 for문을 쓴 이유는 feas라는 빈 배열에 위험 지역의 인덱스를 모두 담기 위함이다.

이렇게 하면 -1행이나 board의 행의 개수보다 1만큼 큰 인덱스도 feas에 함께 딸려오는데, 이들을 제거하기 위해 ans에 0행부터 board 길이에 해당하는 행을 가진 인덱스 배열만 문자열로 변환하여 넣었다.
문자열로 변환한 이유는 중복 제거를 해야하기 때문.

[1,2] === [1,2] // false

배열끼리는 원소가 같더라도 true를 반환하지 않는다.
그래서 문자열로 반환하여 같은 배열끼리는 중복 제거가 되도록 처리한 것이다. (이게 바람직한 방법인지는 나도 아직 잘 모르겠다.)

이렇게 해서 boardSize 에서 (5*5, 6*6 ,, 등 문제에서 주어진 크기)
위험 지역의 수를 빼서 안전지대의 수를 구했다.

하지만 이 문제를 겨우 깨고나니 비로소 이 문제를 어떻게 접근해야 하는지에 대한 큰 깨달음을 주는 코드를 보게되었다.

function solution(board) {

    let outside = [[-1,0], [-1,-1], [-1,1], [0,-1],[0,1],[1,0], [1,-1], [1,1]];
    let safezone = 0;

    board.forEach((row, y, self) => row.forEach((it, x) => {
        if (it === 1) return false;
        return outside.some(([oy, ox]) => !!self[oy + y]?.[ox + x])
               ? false : safezone++;
    }));

    return safezone;
}

이 코드를 겨우 이해하고 난 나의 모습

이 코드에서 배운 것들
1. forEach와 map의 차이를 명시적으로 구분해서 숙지할 필요가 있다. (기술면접 질문이라 함.)
2. Array.prototype.some()
3. optional chaining (?.를 쓰는 이유)
4. 단축어 (!!를 쓰는 이유)

위의 개념들을 하나씩 살펴보고 난 후 마무리로 코드를 뜯어보자.

1. forEach와 map의 차이.

지금까지는 map무새로서 forEach가 out of 안중이었지만, 이 코드를 보고 map보다 forEach를 쓰는게 더 적합한 상황들이 있다는 것을 배웠다.

둘다 배열 내 원소들을 순회하면서 콜백함수를 적용하는 건 알겠는데 정확한 차이가 뭘까?

- 콜백함수의 결과값으로 이루어진 새로운 배열을 반환하는 map

forEach는 단순히 for문을 대체하여 사용하는 것으로, 자신의 내부에서 순회하면서 수행해야할 처리를 콜백 함수로 전달받아 반복 호출하는 메서드.
이러한 forEach의 역할을 map도 동일하게 수행하지만,
⭐️ map은 콜백함수의 반환값들로 구성된 새로운 배열을 반환한다는 중요한 차이가 있다!!⭐️

그래서 map을 적용하여 원본 배열을 mapping할 경우, 원본 배열과 원소의 개수가 동일해진다.

즉, 원본배열에서 1대1 매핑을 거친 새로운 배열을 반환하고자 할 경우 map을 쓰고, 1대1 매핑이 아닌 경우 굳이 map을 쓸 필요가 없는 것이다.

- return 값을 보내지 않는 forEach

한 가지 더 중요한 차이가 있다면, forEach는 return값을 보내지 않지만 map은 return 값을 보낸다.

forEach 적용한 board를 b_forEach라는 임시 변수에 담아 console 찍어보면 return값을 보내지 않아 undefined 반환. 그저 forEach 내부의 함수만 묵묵히 수행할 뿐이다. 이 코드에서는 board의 각 원소를 순회하며 제시한 조건에 맞을 경우 safezone이라는 변수를 ++하는 것이 주요 목적이므로, 1대1 매핑된 새로운 배열을 반환할 필요가 없는 것이다.

const board = [
    [0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 0, 0, 0],
  ];

  const b_forEach = board.forEach((row, y, self) =>
    row.forEach((it, x) => {
      if (it === 1) return false;
      return outside.some(([oy, ox]) => !!self[oy + y]?.[ox + x])
        ? false
        : safezone++;
    })
  );
  console.log(b_forEach); //undefined;

map 적용하여 b_map이라는 임시 변수에 담아 console 찍어보면, return이 쏴주는 콜백함수 결과값들로 이루어진 배열을 반환

const board = [
  [0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0],
  [0, 0, 1, 0, 0],
  [0, 0, 0, 0, 0],
];
let safezone = 0;

const b_map = board.map((row, y, self) =>
  row.map((it, x) => {
    if (it === 1) return false;
    return outside.some(([oy, ox]) => !!self[oy + y]?.[ox + x])
      ? false
      : safezone++;
  })
);
console.log(b_map);
//
[
[ 0, 1, 2, 3, 4 ],
[ 5, 6, 7, 8, 9 ],
[ 10, false, false, false, 11 ],
[ 12, false, false, false, 13 ],
[ 14, false, false, false, 15 ]
]

참고:
https://d-cron.tistory.com/11

https://dream-frontend.tistory.com/341

마지막으로 중요 뽀인트 재정리!!

  • map은 forEach와 달리 원본배열과 1대1 대응되는 콜백함수의 결과값으로 이루어진 새로운 배열을 반환.
  • forEach는 map과 달리 return 값을 외부로 보내지 않음.

2. Array.prototype.some()

우선 MDN에 있는 정의는 다음과 같다.

some() 메서드는 배열 안의 어떤 요소라도 주어진 판별 함수를
적어도 하나라도 통과하는지 테스트합니다.
만약 배열에서 주어진 함수가 true을 반환하면 true를 반환합니다.
그렇지 않으면 false를 반환합니다.
이 메서드는 배열을 변경하지 않습니다.

some 메서드라는 게 있다는 것만 어렴풋이 안 상태에서 괄호 안에 이것저것 넣어봤다가 실패한 적이 있다. ㅋㅋㅋㅋ () 안에는 반드시 콜백함수가 들어가야 한다규!

element, index, array를 인자로 받아 콜백함수를 호출한다.

some((element, index, array) => { /* … */ })

boolean 값을 반환하는 메서드라는 것이 중요하군!

[1, 2, 3, 4, 5].some((element) => element % 2 === 0);
// true

every의 사용법도 동일하다. 간단한 의미적 차이만 있으므로 every 메서드 설명은 생략!

MDN 참고:
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/some

3. optional chaining (?.를 쓰는 이유)

존재하지 않는 프로퍼티에 접근하려할 때 오류가 나는 것을 막아준다.
MDN 예시를 보자.
adventurer이라는 객체에 dog라는 key가 없는데도, 'dog의 name'을 호출하려고 하는 경우 Error가 발생. optional chaining 사용으로 이를 방지.

. 사용 시 Error 발생

const adventurer = {
  name: "Alice",
  cat: {
    name: "Dinah",
  },
};

const dogName = adventurer.dog.name;
console.log(dogName);
// TypeError: Cannot read properties of undefined (reading 'name')

?. 사용 시 undefined 반환

const adventurer = {
  name: "Alice",
  cat: {
    name: "Dinah",
  },
};

const dogName = adventurer.dog?.name;
console.log(dogName);
// undefined

참고 :
console.log(adventurer.dog)
까지만 입력하면 그냥 undefined라고 뜬다.
adventurer.dog.name까지 호출했을 때 에러가 난다.

배열에서도 optional chaining을 이용해보자.

const arr = [[1], [1, 2], [1, 2, 3, 4]];
let arrayItem = arr[3][4];
console.log(arrayItem);
//TypeError: Cannot read properties of undefined (reading '4')

arr에서 index가 3인 원소가 없는데도 index가 3인 원소에서 index가 4인 원소를 찾고 있으니 에러가 난다.
이 때 optional chaining을 쓰면?

const arr = [[1], [1, 2], [1, 2, 3, 4]];
let arrayItem = arr[3]?.[4];
console.log(arrayItem);
// undefined

undefined 반환하고 에러가 나는 것을 방지해준다.

기억하자 ⭐️ optional chaining을 쓸 때는 (오른쪽이 아닌) 왼쪽에 있는 것이 존재하지 않을 때에 오류가 날 것에 대비하여 사용하는 것!

MDN 참고:
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Optional_chaining

4. 단축어 (!!를 쓰는 이유)

확실한 논리결과를 boolean 값으로 반환 받고 싶을 때 사용한다.
undefined, null, "" 등 falsey 값의 경우 명확하게 false로 반환해줄 수 있다.

예를 들어

const arr = [];
console.log(arr[0]); // undefined
console.log(!!arr[0]); //false

참고:
https://velog.io/@rawoon/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%97%90%EC%84%9C-%ED%94%BC%ED%95%B4%EC%95%BC%ED%95%A0-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EB%B2%95

https://hermeslog.tistory.com/279

이렇게 새로운 코드를 이해하기 위한 4가지 중요한 개념들을 살펴봤다.

이제 마지막으로 코드를 뜯어보자.

function solution(board) {

    let outside = [[-1,0], [-1,-1], [-1,1], [0,-1],[0,1],[1,0], [1,-1], [1,1]];
    let safezone = 0;

    board.forEach((row, y, self) => row.forEach((it, x) => {
        if (it === 1) return false;
        return outside.some(([oy, ox]) => !!self[oy + y]?.[ox + x])
               ? false : safezone++;
    }));

    return safezone;
}

board에서 forEach로 행 원소들을 순회하도록 하고, 이 때 각 행들의 current value는 row, 행의 인덱스는 y, board 자체는 self로 두고 인자로 받음.
그리고 각 행의 원소에 대해서 내부에서 forEach를 한 번 더 걸고 콜백함수를 실행한다.
각 원소가 1이라면? return false 하지만, forEach를 쓰므로 사실상 아무것도 return 하지 않음.
만약 1이 아니라면 outside에 1이 있는지 보아야 한다.
이 때 outside.some() 의 값이 true일 경우 주변에 폭탄이 있다는 뜻이므로 안전지대가 아니라는 의미에서 false. outside.some() 의 값이 false일 경우 주변에 폭탄이 없다는 뜻이므로 safezone의 값을 1만큼 증가시킨다.

그렇다면 어떤 원리로 outside.some()의 true/false 값이 정해질까?
outside의 각 배열 원소는 두 가지 원소로 되어 있으므로, [oy, ox]로 구조분해 할당하여 인자로 받음.
self라는 보드 배열에 현재의 row(y)에 oy를 더한 행이 존재할 수도 있고 없을 수도 있다.
예를 들어 y(행 번호)가 0인데, outside의 원소 중 [-1,0]의 -1을 행번호 0에 더할 경우 board의 -1번째 행을 가리키게 되는데, -1번째 행이라는 것은 존재하지 않는다.
이 때 optional chaining을 쓰지 않으면

TypeError: Cannot read properties of undefined (reading '0')

이런 오류가 난다.
-1번째 행은 없는데 -1번째 행에서 0번째 열을 찾고 있으니 오류가 나는 것.
그래서 optional chaining을 쓰는 것!

그리고 !!가 undefined를 false로 바꿔주는 케이스는 2가지 이다.
i) self[oy + y]가 undefined일 경우. (이 때 뒷 부분은 optional chaining으로 없는 셈 칠 수 있다.)
ii) self[oy + y][ox + x])가 undefined일 경우. 즉, 행번호는 존재하는 데 열번호가 없으면 undefined값이 된다.

따라서 !!를 쓰면 outside를 some으로 순회할 경우 반드시 true/false 값만 반환.
true가 한 개라도 있으면
outside.some(([oy, ox]) => !!self[oy + y][ox + x])은 true가 되고,
한개도 없으면 false가 되어 현재 구역이 safezone임을 의미하게 되는 것.

마지막까지 미루고 미루다가 푼 문제인만큼 공부하고 배워야할 개념들도 많았다.
1레벨 풀 때 필요하면 써먹으면서 내 실력으로 만들자.
화이팅!

profile
기록에 진심인 개발자 🌿

0개의 댓글