728x90

풀이 

문제는 직접 보고 오신 경우가 많을 것 같아서 제 풀이 먼저 올렸습니다.

const fs = require('fs');
const input = fs
  .readFileSync(__dirname + '/input.txt')
  .toString()
  .split('\n');

// 테스트 횟수
const T = +input[0];
let index = 1;

for (let t = 1; t <= T; t++) {
// 입력값 n은 건물 개수, k는 간선의 수
  const [n, k] = input[index].split(' ').map(Number);
  // 각각의 건물마다 건설 시간
  const timeArr = input[index + 1].split(' ').map(Number);
  // 입력값에서 간선의 인덱스로 이동
  index += 2;
  
  // 간선들을 그래프에 담기 위해서 (인덱스와 건물 번호가 일치하도록 n+1)
  const graph = Array(n + 1)
    .fill()
    .map(() => []);
   
  // 그래프에 간선 정보 담기
  for (let i = 0; i < k; i++) {
    const [start, end] = input[index + i].split(' ').map(Number);
    graph[end].push(start);
  }

  // 총 건설 시간의 초기값은 각각의 건물 시간으로 줌
  const minBuild = [0, ...timeArr];
  
  // 간선 지나치기
  index += k;
  // 완성 시켜야 하는 건물
  const target = input[index++];
  //총 건설 시간 배열을 활용하여 목표 건물의 총 건설 시간 구함
  console.log(countTotal(target));

// 재귀함수를 통해서 경로를 찾음, 이때 모든 건물의 건설 시간을 구하는게 아니라
// 목표 건물의 하위 건물들만 구해줌
  function countTotal(number) {
   //반복문을 통해서 목표 건물에 선행 건물이 있다면 해당 건물의 총 건설 시간 먼저 구함
    while (true) {
    // 선행 건물이 없다면 건설시간 반환
      if (graph[number].length === 0) {
        return minBuild[number];
      }
      // 선행 대상이 있다면, graph에서 해당 노드를 추출하고 총 건설시간을 구함
      const subTarget = graph[number].pop();
      // 총 건설 시간은 기존에 있는 값과 비교하여서 더 긴 경우에만 수정
      minBuild[number] = Math.max(
        minBuild[number],
        countTotal(subTarget) + timeArr[number - 1]
      );
    }
  }
}

 

문제 설명

문제 링크 및 출처 : https://www.acmicpc.net/problem/1005

 

풀이 배경

결국 목표 건물의 건설을 알기 위해선 선행 건물의 건설시간을 파악해야 한다.

이때 선행의 선행 건물이 존재할 수 있다.

따라서 재귀함수를 이용해서 문제를 해결하기로 결정

즉 , 다이나믹 프로그래밍과 dfs를 이용해서 문제를 풀었다.

따라서 그래프에 선행 건물 정보를 담아 놓고, 선행 건물이 있다면 선행 건물의 총 건설 시간 +  목표 건물의 건설 시간을 더해서 총 건설 시간을 구하는 방식으로 풀었다.

 

다른 풀이

보통 위상 정렬을 이용해서 해당 문제를 푸는 경우를 많이 봤다. 

선행 건물의 수가 적은 순으로 큐나 스택에 정보를 담아서 구하는 방법인데 아래 블로그에서 위상정렬을 이용한 풀이 방법 

을 설명해준다.

https://velog.io/@mk0504/%EB%B0%B1%EC%A4%80-1005%EB%B2%88-ACM-Craft-JavaScript

 

[백준] 1005번, ACM Craft (JavaScript)

문제 분류 : DP, 위상 정렬, 그래프 이론문제 출처 : 백준 골드 3, ACM Craft이 문제는 이전 벽 부수고 이동하기에서 벽을 부수는 횟수가 K(1 <= K <= 10)번까지로 추가된 버전이다.벽은 부수는 횟수를 카

velog.io

 

728x90

 

처음엔 문제 이해하는게 젤 어려웠다.

 

요약하자면, 아침 9시부터 셔틀버스가 직원을 회사로 데려다 주는데, 셔틀 버스의 갯수와, 배차 간격, 한 번에 탑승 가능한 인원이 정해져 있다.

이때 최대한 마지막에 탈 수 있는 경우를 구하면 된다.

 

전날에 타려고 대기하는 사람은 없고,, 한번에 여러 사람이 대기하는 경우를 유의하면서 풀면 된다.

 

 

우선 데이터 타입이 문자로 오기 때문에, 정렬하기 쉬운 숫자 타입으로 바꾸는 함수와, 숫자 값을 시간을 바꾸는 함수를 각각 만들었다.

 

이후 마지막 통학버스에 자리가 남는다면 막차 도착시간에 맞춰서 가면 된다.
1. 이를 위해서 lastBus 변수를 만들어서 막차 시간을 구했다.
2. 그리고 한번에 탑승할 수 있는 인원수가 총 크루보다 많다면 무조건 막차에 타면 된다.

3. 다른 크류가 없다면 막차에 타면 된다.
4. 이후 막차 이후에 도착한 사람들은 제거하고, 시간을 분으로 교체한 후 오름차순으로 사람들을 정렬했다.

 

결국 우리는 마지막 도착하는 인원보다 빨리 도착하면 탈 수가 있다.

즉 마지막에 도착하는 사람을 구해주면 된다.

 

1. 만약 이번에 탑승할 사람이 버스 도착시간 이하로 도착했다면 탑승한다
2.  만약 현재 버스에 못 타면 다음 버스에 탈 수 있어야 한다.

이를 기반으로 마지막에 도착하는 사람을 구하기 위해선 index를 사용하는게 적합하다고 생각했다.

n번 쨰 가 왔을 때 해당 버스의 도착 시간을 구하고, crew들의 도착시간을 비교하여 탑승할 수 있는 경우에 태운다.
이중 반복문에서 m번만큼 반복해 자리가 있는 경우 승객을 계속 태운다.

1.  남은 승객이 없는 경우에는 무조건 막차에 타면 된다.

2. 만약 마지막 칸이 다 찼따면, 해당 크류의 도착시간을 반환하고 1분 일찍 도착하도록 한다.

function solution(n, t, m, timetable) {
  // 막차시간
  const lastBus = 540 + (n - 1) * t;
  // 무조건 탈 수 있는 경우 막차탐
  if (m > timetable.length) {
    return timeConverter(lastBus);
  }

  if (totalCrew === 0) {
    return timeConverter(lastBus);
  }

  // 막차 이후의 사람들은 제거하고 오름차순 정렬
  const minuteTable = timetable
    .map((e) => minuteConverter(e))
    .filter((e) => e <= lastBus)
    .sort((a, b) => a - b);

  let currentIndex = 0;
  const totalCrew = minuteTable.length;

  for (let i = 0; i < n; i++) {
    const busTime = 540 + i * t;
    for (let j = 1; j <= m; j++) {
      // 현재 크루가 이번 버스보드 늦는 경우
      if (minuteTable[currentIndex] > busTime) {
        break;
      }
      // 현재 크류
      if (i === n - 1 && j === m) {
        return timeConverter(minuteTable[currentIndex] - 1);
      }
      if (currentIndex === totalCrew - 1) {
        return timeConverter(lastBus);
      }

      currentIndex++;
    }
  }
}

function minuteConverter(value) {
  const [hour, minute] = value.split(':').map(Number);
  return hour * 60 + minute;
}

function timeConverter(value) {
  const hour = parseInt(value / 60);
  const minute = value % 60;
  const stringMinute = minute < 10 ? '0' + minute : minute;
  return hour < 10
    ? '0' + hour + ':' + stringMinute
    : hour + ':' + stringMinute;
}

 

 

728x90

문제링크 : https://school.programmers.co.kr/learn/courses/30/lessons/64062

 

프로그래머스

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

programmers.co.kr

 

문제를 풀었는데, 내 방법은 13번에서 효율성에 통과하지 못했다. 하지만 해당 원인에 대해서 처음엔 잘 이해하지 못했고, 다른 사람의 풀이를 보고 고민한 결과를 정리하고자 게시글을 작성하게 되었다.

 

 

문제는 위와 같다.

요약하자면 현재 돌 보다  밟을 수 있는 횟수가 작은 돌이 연속으로 k번 이상 나오게 되면 건널 수가 없다.

 

나는 위와 똑같은 논리를 코드로 구현하고자 하였다.

function solution(stones, k) {
// 돌의 갯수
  const arrayLength = stones.length;
 // 한번에 건널 수 있는 수와 돌 수가 같다면 최대값이 답.
if (arrayLength == k) {
    return Math.max(...stones);
  }

// 초기에는 k번쨰 수중 가장 큰 값이 최대값
  let answer = stones[0];

  for (let i = 1; i < k; i++) {
    if (stones[i] > answer) {
      answer = stones[i];
    }
  }

// 이후 반복문을 통해서, 다음번의 돌들이 연속으로 k번 작을 경우 answer를 교체
  for (let i = 0; i < arrayLength - k; i++) {
    let max = 0;
    const current = stones[i];
    let flag = false;
    for (let j = 1; j <= k; j++) {
      if (current < stones[i + j]) {
        flag = true;
        break;
      }
      max = Math.max(max, stones[i + j]);
    }
    if (!flag) {
      answer = Math.min(answer, max);
    }
  }
  return answer;
}

 

로직에 문제가 없다면 정확성에선 통과하지만, 효율성에서 13번에서 걸리고 말았다.

이 당시에는 왜 13번만 통과를 못하지에 대한 해답을 찾지 못하였고, 나와 같은 문제를 겪은 사람이 있을까 질문하기에 들어갔다.

 

그 결과 대부분을 이분탐색을 통해서 문제를 푼다는 것을 알게 되었고, 이를 참고하여서

 

function solution(stones, k) {
//이분 탐색 좌우
  let left = 1;
  let right = 200000000;

  while (left <= right) {
    const mid = ((left + right) / 2) >> 0;

    let count = 0;
    // 반복문을 통해서 연속으로 k번 나오면 break, 이후 정답에 가까운 값을 다시 탐색)
    for (let i = 0; i < stones.length; i++) {
      if (stones[i] - mid <= 0) count++;
      else count = 0;

      if (count === k) break;
    }

   // 만약 연속 k번 나오면 right를 교체(현재 값이 최댓값을 넘었다)
    if (count === k) right = mid - 1;
    // 아니라면 더 큰 값이 있는지 찾기 위해서 left를 교체
    else left = mid + 1;
  }

  return left;
}

위 방법은 내 방법과 다르다.

내 방법은 각각의 숫자를 비교하면서 일일이 최댓값을 교체해주는 방식이면, 이분탐색 방법은 정답을 임의의 값으로 정해놓고 비교한 후 정답을 수정하는 방식이다. (200000000가지 중에서 이분 탐색을 통해서 정답을 찾는 방식)

내 방법과 다르게 대부분의 방식에선 속도가 느린데, 13번만 유독 빠르다.

 

이에 대한 호기심을 해결하기 위해서 두 방식의 시간 복잡도를 계산해 보려고 하였다.

 내 방법의 시간복잡도는 n * k 이다.  ( n: 배열의 길이 , k : k의 값)

k가 길어질수록 반복 계산이 많아진다.

 

 정답에 통과한 두 번쨰 방법의 시간 복잡도는 nlogM이다( n: 배열의 길이, M은 left와 right의 간격 200000000)

이분 탐색을 통해 찾기 때문에 만약 정답이 중앙에 가까울수록 속도가 빠를 것이다.

 

즉 테스트에서 대부분 내 값이 빨랐던 이유는 k 값이 log( 200000000)에 비해서 작았기 떄문이다. 

하지만 가장 오래 걸리는 값을 기준으로 했을 때는 후자가 훨씬 좋은 방법이다.

 

 

 

회고

문제를 풀면서, 시간 복잡도에 대해서 깊게 생각하지 않았기 때문에 좀 더 막막하게 느껴졌었던 것 같다.

백준에서 시간 복잡도의 중요성을 느끼지만, 막상 문제 풀 때는 문제 유형에 맞춘 방법을 사용했었다... 안 좋은 습관인 거 같다. 막상 혼자 밑 바닥 부터 생각해야 할 때, 좋은 방법이 잘 떠오르지 않았느데, 이번 기회에 시간 복잡도와 문제를 고민하는 방법에 대해서 다시 생각하게 되었다.

 

728x90

 

 

N의 숫자가 10억이기에, 각각의 페이지를 일일이 확인해서는 풀 수 없는 문제입니다.

 

따라서 각각의 자릿수에 대해서 해당 숫자가 몇번 나올지 측정을 합니다.

 

1. 0~9까지 공통적으로 반복되는 횟수를 측정.

 

20을 예로 들면  일의 자리에서 0~9 까지는 총 2번 반복됩니다. 이를 부모의 값을 통해서 측정을 합니다.

220 을 예로 들면 0~9까지는 총 22번 반복되게 됩니다.

 

이때 유의 해야 할 점은 0입니다.  페이지가 01이 아닌 1 페이지기 때문에, 이 0에 의해서 오류가 자주 생깁니다.

201 의 경우에는 

나머지는 두 번째 자리에 대해서는 20번씩 반복되지만 0의 경우는 12번만 반복됩니다. 즉 0의 경우에는 하위 자릿수에 대한 영향도 받음을 알 수 있습니다.

100~ 109 , 200~201

 

 

2. 해당 숫자가 몇번까지 존재하는지

 

124를 예를 들겠습니다.

1의 경우에는 110~119 까지 10번 반복되는데 2의 경우에는 20~24까지 5번만 반복되는 하위 자릿수의 값에 대한 영향을 받습니다.

 

이 두가지를 고려해서 알고리즘을 짰습니다.

 

numberList :  입력값을 자릿수마다 분해해서 배열에 담음

number :  입력값을 number 타입으로 변환한 값

sames : 공통적으로 반복되는 값을 추후 각각의 숫자에 대해서 더하는 용도

answer : 그리고 각각의 숫자가 몇번 사용되었는지 담을 배열 

digit : 자릿수

depth : 해당 숫자의 부모의 자릿수 ( parentDigit)을 했으면 변수명이 더 깔끔했을 까요,,?

target : 현재 자릿수에 대한 값.

const fs = require('fs');
const input = fs.readFileSync('dev/stdin').toString();
const numberList = input.split('').map(Number);
const number = +input;

let sames = 0;

const answer = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];

const digit = numberList.length;

for (let i = digit - 1; i >= 0; i--) {
  const depth = 10 ** (digit - i);
  sames += (parseInt(number / depth) * depth) / 10;
  const target = numberList[i];
  if (target === 0 && digit - i !== 1) {
    answer[0] -= depth / 10 - 1 - (number % (depth / 10));
  }
  for (let j = target; j >= 1; j--) {
    if (j === target) {
      answer[j] += (number % (depth / 10)) + 1;
    } else {
      answer[j] += depth / 10;
    }
  }
}

let counts = answer.map((e) => e + sames).join(' ');
console.log(counts);

 

반북문을 통해서 1의 자리부터 최고 자릿수까지 코드를 진행합니다.

 

우선 반복되는 값은 부모의 자릿수에 대해서 영향을 받는데 이는 부모 자릿수 까지만 값을 고려하고 나머지는 제거한 값입니다. 이후 해당 값을 10으로 나누어 주면 됩니다.(갯수 / 분모(0~9 총 10개)

ex) 105 의 경우에  10(100/10 ) 개의 0~9가 가 공통적으로 존재함 

 

이때 유의해야 할 점이 0이라고 하였습니다.

이를 반복문을 통해서 만약  target값이 0이고 첫번째 자릿수가 아니라면  9999~~에 현재 자릿수 이하의 값을 뺀 값을  제거해줍니다. 

105의 경우에   2번쨰 자리에 대한 sames가 10개라고 했는데 0의 경우에는 실질적으로 6개 입니다.

따라서 4개를 제거해야 하는데 이를  9(10-1) - 5 를 하면 됩니다.

 

  sames += (parseInt(number / depth) * depth) / 10;
  const target = numberList[i];
  if (target === 0 && digit - i !== 1) {
    answer[0] -= depth / 10 - 1 - (number % (depth / 10));
  }

 

마지막으로 현재 자릿수 이하의 값을 반영해줍니다.

 

1042의 두번 째 자리를 예로 들면

1~3까지는 0~9가 반복됩니다. 따라서  자릿수만 큼 더하면 됩니다. 하지만 4의 경우에는 0 1 2 총 3번 반복되므로, 

 for (let j = target; j >= 1; j--) {
    if (j === target) {
      answer[j] += (number % (depth / 10)) + 1;
    } else {
      answer[j] += depth / 10;
    }
  }

 

마지막으로  중복되는 sames값을 더한후에 출력하면 됩니다. 이때 join을 써서 하나의 문자열로 출력했습니다.

 

 

코드가 투박한 것 같아서, 추후 백준님의 풀이를 봤는데, 좀 더 문제가 심화되어도 확장성이 좋고, 함수가 분리되어 있어서 이해하기도 좋은 방식으로 코드 짠 것 같습니다... 한번 참고해 보셔도 좋을 것 같아요..

 

https://www.slideshare.net/Baekjoon/baekjoon-online-judge-1019?qid=9aa7818e-779e-499a-9c13-d2a5ac2ef8af&v=&b=&from_search=1

728x90

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

 

1119번: 그래프

N개의 도시가 있고, 몇몇 도시들이 양방향 도로로 연결되어 있는 나라가 있다. 은진이는 이나라의 도로 몇 개를 수정해서 모든 도시가 서로 연결되어 있게 하려고 한다. 이때, 도로를 수정하는

www.acmicpc.net

 

 

 

문제풀이

 

문제를 제일 처음 봤을 때, 생각나는 구조는 트리구조 였습니다.

점이 N개 주어졌을 때,  그래프에서 각각의 점들이 연결된 선 (간선)이 N-1 개라면 변형에 의해서 각각의 떨어진 점 들을 교환을 통해서 이을 수 있습니다.

 

1 .따라서 우선적으로는 간선의 개수와 정점의 개수를 먼저 확인합니다.

(만약 5개의 점인데 선이 3개라면 이을 수 없으니깐요..) 

 

const fs = require('fs');
const input = fs
  .readFileSync(__dirname + '/input.txt')
  .toString()
  .split('\n');

const n = +input[0];
let edge = 0;
const graph = Array.from(Array(n), () => []);

for (let i = 1; i <= n; i++) {
  const nodes = input[i].split('');
  for (let j = 0; j < n; j++) {
    if (nodes[j] === 'Y') {
    // 양방향 그래프이므로, 간선의 개수를 셀 때 0.5를 더 해줌
      edge += 0.5;
      graph[i - 1].push(j);
    }
  }
}

입력 값을 받고 ,  이를 그래프에 등록하면서 간선의 개수를 측정합니다.

 

이후 간선의 개수가 작다면 -1을 출력합니다.

 

if (edge < n - 1) return console.log(-1);

 

 

이후 주어진 그래프에서 서로 연결된 컴퍼넌트(덩어리)가 몇 개인지 측정해줍니다.

이때 사용한 것이 DFS 알고리즘입니다.

const visited = Array(n).fill(false);
let comp = 0;

for (let i = 0; i < n; i++) {
  if (!visited[i]) {
    comp++;
    dfs(i);
  }
}

function dfs(index) {
  visited[index] = true;
  for (let i = 0; i < graph[index].length; i++) {
    !visited[graph[index][i]] && dfs(graph[index][i]);
  }
}

console.log(comp - 1);

 

방문 처리를 통해서 이미 방문한 지점은 추가 방문을 방지하고, dfs알고리즘에 대해서 하나의 점에 대한 탐색을 끝냈음에도 방문이 되지 않은 점이 있다면, 이는 다른 컴퍼넌트입니다. 따라서 이때 comp++을 실행해줍니다.

 

앞에서 이미 간선이 부족해서 수정이 되지 않는 경우에 대한 처리를 했기 때문에,  서로 떨어진 컴퍼넌트를 연결만 하면 됩니다.  따라서 comp-1 개를 출력합니다.

 

하지만 문제 조건 중에서, 이미 연결된 선들을 분해 한 후에, 4개의 점에 대한 연결을 진행합니다. 따라서  아무것도 연결되지 않은 점은 연결할 수 없습니다. ( 다만 정점이 하나인 경우는 예외처리해야합니다.)

이를 반영한 최종 코드는

 

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

const n = +input[0];
let edge = 0;
const graph = Array.from(Array(n), () => []);
const visited = Array(n).fill(false);

for (let i = 1; i <= n; i++) {
  const nodes = input[i].split('');
  for (let j = 0; j < n; j++) {
    if (nodes[j] === 'Y') {
      edge += 0.5;
      graph[i - 1].push(j);
    }
  }
}

if (edge < n - 1) return console.log(-1);
// 아무것도 연결되지 않은 점이 있다면
for (let i = 0; i < n; i++) {
  if (graph[i].length === 0 && n !== 1) return console.log(-1);
}
let comp = 0;

for (let i = 0; i < n; i++) {
  if (!visited[i]) {
    comp++;
    dfs(i);
  }
}

function dfs(index) {
  visited[index] = true;
  for (let i = 0; i < graph[index].length; i++) {
    !visited[graph[index][i]] && dfs(graph[index][i]);
  }
}

console.log(comp - 1);
728x90

 

최단거리 알고리즘을 풀게 되면서 한 지점에서 다른 모든 지점에 대한 최단거리를 구할 때, 다익스트라 알고리즘사용한다는 사실을 알게 되었습니다.

 

다익스트라 알고리즘의 동작원리는

1. 출발 노드를 설정

2. 최단 거리 테이블을 초기화

3. 방문하지 않은 노드 중에서 최단 거리가 가장 짧은 노드를 선택

4. 해당 노드를 거쳐 다른 노드로 가는 비용을 계산하여 최단 거리 테이블을 갱신 혹은 우선순위 큐에 삽입

5. 위 과정에서 3번과 4번을 반복

 

이때 왜 우선순위큐를 사용하냐면

출처 : 패캠 초격차 나동빈님 강의

 

그렇게에 강의에서는 우선순위 큐를 사용하는 것을 추천해 주었는데, 자바스크립트에서는 다른 언어와 달리 우선순위 큐가 내장되어 있지 않습니다.  대신 아래에 있는 누군가 구현한 코드를 활용해서 알고리즘을 풀면 된다고 했는데, 이 코드를 해석해보려고 합니다

https://github.com/janogonzalez/priorityqueuejs/blob/master/index.js

 

/**
 * Expose `PriorityQueue`.
 */
module.exports = PriorityQueue;

/**
 * Initializes a new empty `PriorityQueue` with the given `comparator(a, b)`
 * function, uses `.DEFAULT_COMPARATOR()` when no function is provided.
 *
 * The comparator function must return a positive number when `a > b`, 0 when
 * `a == b` and a negative number when `a < b`.
 *
 * @param {Function}
 * @return {PriorityQueue}
 * @api public
 */
function PriorityQueue(comparator) {
  this._comparator = comparator || PriorityQueue.DEFAULT_COMPARATOR;
  this._elements = [];
}

/**
 * Compares `a` and `b`, when `a > b` it returns a positive number, when
 * it returns 0 and when `a < b` it returns a negative number.
 *
 * @param {String|Number} a
 * @param {String|Number} b
 * @return {Number}
 * @api public
 */
PriorityQueue.DEFAULT_COMPARATOR = function(a, b) {
  if (typeof a === 'number' && typeof b === 'number') {
    return a - b;
  } else {
    a = a.toString();
    b = b.toString();

    if (a == b) return 0;

    return (a > b) ? 1 : -1;
  }
};

/**
 * Returns whether the priority queue is empty or not.
 *
 * @return {Boolean}
 * @api public
 */
PriorityQueue.prototype.isEmpty = function() {
  return this.size() === 0;
};

/**
 * Peeks at the top element of the priority queue.
 *
 * @return {Object}
 * @throws {Error} when the queue is empty.
 * @api public
 */
PriorityQueue.prototype.peek = function() {
  if (this.isEmpty()) throw new Error('PriorityQueue is empty');

  return this._elements[0];
};

/**
 * Dequeues the top element of the priority queue.
 *
 * @return {Object}
 * @throws {Error} when the queue is empty.
 * @api public
 */
PriorityQueue.prototype.deq = function() {
  var first = this.peek();
  var last = this._elements.pop();
  var size = this.size();

  if (size === 0) return first;

  this._elements[0] = last;
  var current = 0;

  while (current < size) {
    var largest = current;
    var left = (2 * current) + 1;
    var right = (2 * current) + 2;

    if (left < size && this._compare(left, largest) >= 0) {
      largest = left;
    }

    if (right < size && this._compare(right, largest) >= 0) {
      largest = right;
    }

    if (largest === current) break;

    this._swap(largest, current);
    current = largest;
  }

  return first;
};

/**
 * Enqueues the `element` at the priority queue and returns its new size.
 *
 * @param {Object} element
 * @return {Number}
 * @api public
 */
PriorityQueue.prototype.enq = function(element) {
  var size = this._elements.push(element);
  var current = size - 1;

  while (current > 0) {
    var parent = Math.floor((current - 1) / 2);

    if (this._compare(current, parent) <= 0) break;

    this._swap(parent, current);
    current = parent;
  }

  return size;
};

/**
 * Returns the size of the priority queue.
 *
 * @return {Number}
 * @api public
 */
PriorityQueue.prototype.size = function() {
  return this._elements.length;
};

/**
 *  Iterates over queue elements
 *
 *  @param {Function} fn
 */
PriorityQueue.prototype.forEach = function(fn) {
  return this._elements.forEach(fn);
};

/**
 * Compares the values at position `a` and `b` in the priority queue using its
 * comparator function.
 *
 * @param {Number} a
 * @param {Number} b
 * @return {Number}
 * @api private
 */
PriorityQueue.prototype._compare = function(a, b) {
  return this._comparator(this._elements[a], this._elements[b]);
};

/**
 * Swaps the values at position `a` and `b` in the priority queue.
 *
 * @param {Number} a
 * @param {Number} b
 * @api private
 */
PriorityQueue.prototype._swap = function(a, b) {
  var aux = this._elements[a];
  this._elements[a] = this._elements[b];
  this._elements[b] = aux;
};

 

 

위의 코드를 이해하기에 앞서 힙(heap)이라는 자료구조를 알 필요가 있습니다.

 

(heap)은 최댓값 및 최솟값을 찾아내는 연산을 빠르게 하기 위해 고안된 완전이진트리를 기본으로 한 자료구조로서 

  • A가 B의 부모노드(parent node) 이면, A의 키(key)값과 B의 키값 사이에는 대소관계가 성립한다.

이때 부모가 자식보다 크면 최대 힙, 작으면 최소 힙이라고 한다. (출처 : 위키백과)

아래와 같은 구조는 최대 힙이다. 

이를 통해서 부모노드는 항상 자식보다 큰 값을 가지며, 우리는 추상적 자료구조인 우선순위큐를 구현할 수 있다.

 

출처 : 위키 백과

 

 

 

 

 

최대힙에서 삽입

만약 추가적인 자료가 삽입된다면, 우선 마지막 노드 위치에 추가되고, 부모 노드와 비교하여 최대힙 조건이 만족될 떄까지 이동한다. 그렇기 때문에 배열에 단순히 추가하는 것은 O(1) 이지만 최대 힙에서 시간복잡도는 O(log(N))이다

최대힙에서 삭제

최대힙에서 삭제할 때는, 부모 노드를 제거한다 .

이후 마지막 노드를 부모로 이동시킨 후, 두 값 중에서 더 큰 값을 가진 노드와 스왑을 진행하여, 최대 힙이 만족될 떄까지 정렬한다.

 

 

이제 힙에 대한 설명이 끝났으니, 다시 위의 코드에 대한 분석을 해보자. 결국 우리의 목적은 알고리즘(백준?)에 쓸 우선순위 큐를 만드는 것이고, 위의 코드는 라이브러리 형식이기에 모듈로 export 되어있다. 라이브러리 기에 기본적인 우선순위 큐를 설정해두었고, 외부에서 비교로직을 준다면 해당 로직으로 사용한다. 따라서 우리는 위의 코드를 아래와 같이 수정할 수 있다. (불필요한 주석 제거)

 

1. 외부에서 무조건 comparator를 전달하는 구조를 만들자.

function PriorityQueue(comparator) {
  this._comparator = comparator
  this._elements = [];
}

 

2. isEmpty 와 peek , size

size는 elements의 길이를 측정해준다. elements에 저장된 노드를 담긴 때문에 해당 배열의 길이가 우선순위큐의 크기다.

isEmpty는 큐가 비었는지 확인하는 함수이다. 따라서  ;size가 0이면 비었있다'라고 구현했다.

peek의 경우 가장 먼저 제거되는 녀석(부모 노드)를 알려주면 된다. 이는 elements라는 배열의 0번째 인덱스 값이다.

function PriorityQueue(comparator) {
  this._comparator = comparator 
  this._elements = [];
}


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


PriorityQueue.prototype.peek = function() {
  if (this.isEmpty()) throw new Error('PriorityQueue is empty');

  return this._elements[0];
};


PriorityQueue.prototype.size = function() {
  return this._elements.length;
};

3. forEach, swap, compare 

우선순위를 정해서 삽입 및 추가하려면, 기존 노드들을 비교, 스왑할 필요가 있다고 했다.

forEach: 큐 내의 elements에 대해서 배열의 forEach 메서드 실행 (  fn을 통해 함수를 매개변수로 받어 콜백함수로 사용)

compare :forEach노드에 사용할 비교함수. 각각의 노드를 comparator를 활용해서 비교함

swap : aux에 a 를 잠시 저장하고, b를 a에 할당, b에 aux를 할당함으로서 a와 b를 교체함


PriorityQueue.prototype.forEach = function(fn) {
  return this._elements.forEach(fn);
};


PriorityQueue.prototype._compare = function(a, b) {
  return this._comparator(this._elements[a], this._elements[b]);
};

PriorityQueue.prototype._swap = function(a, b) {
  var aux = this._elements[a];
  this._elements[a] = this._elements[b];
  this._elements[b] = aux;
};

 

4. enq와 deq  (삽입과 제거)

이제 내부 프로토타입들은 완성 되었으므로, 이를 활용하여 deq와 enq  프토로타입을 만들면 됩니다.

PriorityQueue.prototype.enq = function(element) {
  var size = this._elements.push(element);
  var current = size - 1;

  while (current > 0) {
    var parent = Math.floor((current - 1) / 2);

    if (this._compare(current, parent) <= 0) break;

    this._swap(parent, current);
    current = parent;
  }

  return size;
};

 

위쪽 위키백과 이미지에 배열 테이블을 보시면 한 하위노드에 대한 부모노드의 취니는 해당 노드의 인덱스/2를 내림 한 값과 같습니다. 따라서 부모 노드를 찾고, 비교했을 때, 현재값이 작거나 같다면 멈추고, 아니면 swap을 계속 진행해줍니다.

이후 현재값을 parent노드 값(기존에 삽입한 값)으로 변경 후  해당 위치에서는 힙구조(최대인지 최소인지)에 맞는 위치인지를 확인합니다. 맞다면 변경된 size를 반환합니다.

 

PriorityQueue.prototype.deq = function() {
  var first = this.peek();
  var last = this._elements.pop();
  var size = this.size();

  if (size === 0) return first;

  this._elements[0] = last;
  var current = 0;

  while (current < size) {
    var largest = current;
    var left = (2 * current) + 1;
    var right = (2 * current) + 2;

    if (left < size && this._compare(left, largest) >= 0) {
      largest = left;
    }

    if (right < size && this._compare(right, largest) >= 0) {
      largest = right;
    }

    if (largest === current) break;

    this._swap(largest, current);
    current = largest;
  }

  return first;
};

1. 부모노드의 값을 first에 담아주고, 마지막 값을 추출해서 last에 할당합니다.

(size가 0이면 추출이 안되므로 에러가 뜨고 아니라면  first는 root 노드의 값)

2.  elements 배열에 마지막 자식 노드를 할당합니다 ( 부모노드 덮어씌워지기 떄문에 first에 미리 할당하는 것입니다.)

그리고 해당 index는 0이기에 current에 0을 할당합니다.

3. while문 안에서, 인덱스를 통해서 자식 노드의 위치를 찾습니다.  이때 현재 위치가 사이즈보단 작아야함으로 이를 조건에넣습니다.

4. 만약 자식 노드 중 왼쪽과 오른쪽이 size보다 작으면서 비교결과가 크다면 largest의 값을 해당 인덱스로 할당해줍니다. 

5. largest가 current랑 같다는건 교체되지 않았다는 것이므로 while문을 멈추고, 그렇지 않다면 swap을 통해 현재값과 largest값을 swap하고, 해당인덱스에 대해서 다시  비교를 합니다.

 

 

알고리즘 테스트에 사용할 최종코드는 아래와 같습니다. ( 주석은 제거하고 var => let으로 교체하였습니다)

function PriorityQueue(comparator) {
  this._comparator = comparator;
  this._elements = [];
}

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

PriorityQueue.prototype.peek = function () {
  if (this.isEmpty()) throw new Error('PriorityQueue is empty');
  return this._elements[0];
};

PriorityQueue.prototype.deq = function () {
  let first = this.peek();
  let last = this._elements.pop();
  let size = this.size();

  if (size === 0) return first;

  this._elements[0] = last;
  let current = 0;

  while (current < size) {
    let largest = current;
    let left = 2 * current + 1;
    let right = 2 * current + 2;

    if (left < size && this._compare(left, largest) >= 0) {
      largest = left;
    }

    if (right < size && this._compare(right, largest) >= 0) {
      largest = right;
    }

    if (largest === current) break;

    this._swap(largest, current);
    current = largest;
  }

  return first;
};

PriorityQueue.prototype.enq = function (element) {
  let size = this._elements.push(element);
  let current = size - 1;

  while (current > 0) {
    let parent = Math.floor((current - 1) / 2);

    if (this._compare(current, parent) <= 0) break;

    this._swap(parent, current);
    current = parent;
  }

  return size;
};

PriorityQueue.prototype.size = function () {
  return this._elements.length;
};

PriorityQueue.prototype.forEach = function (fn) {
  return this._elements.forEach(fn);
};

PriorityQueue.prototype._compare = function (a, b) {
  return this._comparator(this._elements[a], this._elements[b]);
};

PriorityQueue.prototype._swap = function (a, b) {
  let aux = this._elements[a];
  this._elements[a] = this._elements[b];
  this._elements[b] = aux;
};

 

 

 

 

사용 예시

function PriorityQueue(comparator) {
  this._comparator = comparator;
  this._elements = [];
}

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

PriorityQueue.prototype.peek = function () {
  if (this.isEmpty()) throw new Error('PriorityQueue is empty');
  return this._elements[0];
};

PriorityQueue.prototype.deq = function () {
  let first = this.peek();
  let last = this._elements.pop();
  let size = this.size();

  if (size === 0) return first;

  this._elements[0] = last;
  let current = 0;

  while (current < size) {
    let largest = current;
    let left = 2 * current + 1;
    let right = 2 * current + 2;

    if (left < size && this._compare(left, largest) >= 0) {
      largest = left;
    }

    if (right < size && this._compare(right, largest) >= 0) {
      largest = right;
    }

    if (largest === current) break;

    this._swap(largest, current);
    current = largest;
  }

  return first;
};

PriorityQueue.prototype.enq = function (element) {
  let size = this._elements.push(element);
  let current = size - 1;

  while (current > 0) {
    let parent = Math.floor((current - 1) / 2);

    if (this._compare(current, parent) <= 0) break;

    this._swap(parent, current);
    current = parent;
  }

  return size;
};

PriorityQueue.prototype.size = function () {
  return this._elements.length;
};

PriorityQueue.prototype.forEach = function (fn) {
  return this._elements.forEach(fn);
};

PriorityQueue.prototype._compare = function (a, b) {
  return this._comparator(this._elements[a], this._elements[b]);
};

PriorityQueue.prototype._swap = function (a, b) {
  let aux = this._elements[a];
  this._elements[a] = this._elements[b];
  this._elements[b] = aux;
};

 let pq = new PriorityQueue((a, b) => b[0] - a[0]);

 

728x90

제 방법이 최고인 것도 아닐테지만,  그냥 node에서 저만 맞춘게 기분이 너무 좋아서 올려봅니다.

문제

좌표 평면에 점 N개가 있다.

이때, 빗변을 제외한 나머지 두 변이 좌표축에 평행한 직각삼각형을 이루는 점 3개를 고르는 방법을 수를 구하는 프로그램을 작성하시오.

직각삼각형은 한각이 직각인 삼각형이며, 직각의 대변을 빗변이라고 한다.

 

 

입력

 

첫째 줄에 점의 개수 N(3 ≤ N ≤ 100,000)이 주어진다. 둘째 줄부터 N개의 줄에는 점의 좌표가 X Y 순서대로 주어진다. (1 ≤ X,Y ≤ 100,000) 겹치는 점은 없다.

출력

첫째 줄에 직각삼각형의 개수를 출력한다.

예시

풀이 

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

const n = +input[0];
const array = [];
const yCount = new Array(100001).fill(0);
for (let i = 1; i <= n; i++) {
  const [x, y] = input[i].split(' ').map(Number);
  if (array[x] === undefined) {
    array[x] = [y];
    yCount[y]++;
  } else {
    array[x].push(y);
    yCount[y]++;
  }
}

let result = 0;
for (let i = 1; i < array.length; i++) {
  if (array[i] && array[i].length > 1) {
    for (let j = 0; j < array[i].length; j++) {
      result += (yCount[array[i][j]] - 1) * (array[i].length - 1);
    }
  }
}
console.log(result);

생각 : 우선 배열 안에다 x,y축에 대한 정보를 담습니다.

그리고 x축이 같은 값 2개를 선정하고, 각각의 점에 대해서 y축값은 같은데 x축이 다른 점의 개수를 세어주면 됩니다.

그러기 위해선 우선 배열안에 x축에 대한 y값을 담습니다.

이후 yCount라는 배열을 통해서  y축 같은 x의 값의 개수를 세어줍니다.

조건에서 값이 100000만 이하이기 떄문에 yCount는 100001 까지 초기값을0 으로 만들어 줍니다.

 

이후 들어온 input값에 대해서 yCount를 증가시키고, array배열안에 해당 x축에 대한 y값을 넣어줍니다.

위 input예시에 대한 정리된 값은 아래와 같네요

 

 

let result = 0;
for (let i = 1; i < array.length; i++) {
  if (array[i] && array[i].length > 1) {
    for (let j = 0; j < array[i].length; j++) {
      result += (yCount[array[i][j]] - 1) * (array[i].length - 1);
    }
  }
}

이후  for 문을 통해서 순회를 하는데 yCount에는 자기자신이 포함되니 -1, 그리고 각각의 값을 x축에 본인이 아닌 값이 있는 횟수만큼 더해지기 때문에 array[i].length-1

빨간점을 기준으로  잡앗을 때(10,10)와 (10,20), 1 +2 가  (20,10)  +   (20,20) , (30,20)이 더 해지는데 

이는 10,10에 대해서 y 값이 같은 (10) 인 값들 중 본인이 아닌 값을 더 한 거고

이는 만약 x축이 같으면서 y축이 다른 값이 여러개 존재하게 되면, 해당 값들이 더 해주는 횟수가 x축이 같으면서 y축이 다른 값들의 갯수 만큼 증가하기 때문이다.

 

for (let i = 1; i < array.length; i++) {
  if (array[i] && array[i].length > 1) {
    for (let j = 0; j < array[i].length; j++) {
      for (let k = j + 1; k < array[i].length; k++) {
        result += yCount[array[i][j]] - 1;
        result += yCount[array[i][k]] - 1;
      }
    }
  }
}

이 전에 시간초과가 났떤 이유를 위와 같은 로직이 아닌 반복문 한 개를 더 해서 일일이 다 더해줬기 떄문입니다.



유독 그래프에 대한 문제가 없어서, 한 개 풀었는데 성장하는 기분이라 좋네요, ㅎㅎ,,,,

728x90

패스트캠퍼스 나동빈 강사님 강의를 보면서 알고리즘을 학습하고 있습니다.

제가 유독 dfs, bfs, 이쪽에 약한 거 같아서,, 한번 포스팅으로 정리하면서 이해해 보려고 합니다.

참고했던 포스팅으로는 https://forward-gradually.tistory.com/61 이 있습니다.

 

초기에 혼자 썼던 코드,, 예제에서 주어진 케이스는 통과했는데, 실제 테스트에선 실패했습니다.

반례를 찾던 중 위 포스팅하신 분의 반례 케이스를 테스트 해보니 다 틀리더군요...

4
3
2 3 3
5
2 3 4 5 4
4
2 3 4 2
5
1 1 1 1 1


//답
2
3
1
4

 

const fs = require('fs');
const input = fs
  .readFileSync(__dirname + '/input.txt')
  .toString()
  .split('\n');
let visited = [];
const testCase = +input[0];
let favorite = [];
let selected = [];
let count = 0;
for (let t = 0; t < testCase; t++) {
  const memberCnt = +input[1 + 2 * t];
  favorite = input[2 + 2 * t].split(' ').map((e) => e - 1);
  count = 0;
  visited = new Array(memberCnt).fill(false);
  for (let i = 0; i < memberCnt; i++) {
    if (!visited[i]) {
      selected = [i];
      visited[i] = true;
      dfs(favorite[i]);
    }
  }
  console.log(count);
}

function dfs(like) {
  if (!visited[like]) {
    visited[like] = true;
    const favOfLike = favorite[like];
    if (favOfLike === like) {
      count++;
    } else if (visited[favOfLike]) {
      const targetIndex = selected.findIndex((e) => e === favOfLike);
      if (targetIndex !== -1) {
        count += selected.length - targetIndex;
      }
    } else {
      selected.push(like);
      dfs(favOfLike);
    }
  }
}

 

생각했던 로직

1. 학생들은 1번 부터 n까지 번호가 주어지기 때문에, 각각의 case에서  Map을 돌릴 때,  1을 뺀 값을 통해 새로운 배열을 만듭니다.  ( favortie 배열)

2. 이후 중복 작업을 제거하기 위해서 visited 배열을 만들었고, 현재 dfs에서 선택된 녀석들을 담을 selected 배열을 만들었습니다.

3. 새로운 dfs가 실행될 떄마다 selected를 초기화 시켜줬고, 각각의 케이스마다 visited와 favorite 배열을 초기화 시켰습니다.

 

위 코드로 해당 케이스 테스트 시  1 1 2 0 이 나옵니다..


1차 보완

일단  dfs함수가 잘못 된 부분이 많았네요..그리고 for 문 안에서 이미 좋아했떤 애를 좋아하는 경우에 대한 경우가 없었네요..
위의 케이스에 대해선 통과 했는데 여전히 틀린 코드 뭐가 문젤까요..

let visited = [];
const testCase = +input[0];
let favorite = [];
let selected = [];
let count = 0;
for (let t = 0; t < testCase; t++) {
  const memberCnt = +input[1 + 2 * t];
  // 케이스마다 좋아하는 배열 초기화
  favorite = input[2 + 2 * t].split(' ').map((e) => e - 1);
  // 조 못이룬 사람 수
  count = 0;
  // 확인 한 경우
  visited = new Array(memberCnt).fill(false);

  for (let i = 0; i < memberCnt; i++) {
    //아직 확인 안 한 사람중에
    if (!visited[i]) {
      // 현재 순회중인 대상
      selected = [i];
      visited[i] = true;
      const like = favorite[i];
      if (like === i) {
        // 혼자 하는 놈
      } else if (visited[like]) {
        // 이미 탐색이 끝난 애(조를 이루거나 조가 이루어 질 수 없는 애랑 하고 싶어하면 실패)
        count++;
      } else {
        // 나머진 탐색
        dfs(like);
      }
    }
  }
  console.log(count);
}

function dfs(like) {
  visited[like] = true;
  const favOfLike = favorite[like];
  if (favOfLike === like) {
    // 혼자 하고 싶어하면 순회중인 나머진 조 못 이룸
    count += selected.length;
  } else if (visited[favOfLike]) {
    // 이미 방문했던 애를 좋아한다면
    const targetIndex = selected.findIndex((e) => e === favOfLike);
    if (targetIndex === -1) {
      // 순회 중이지 않다면
      count += selected.length;
    } else {
      // 만약 대상이 현재 순회중인 애 라면
      count += targetIndex;
    }
  } else {
    // 순회 대상에 추가하고 새로운 애를 통해 순회
    selected.push(like);
    dfs(favOfLike);
  }
}

 

2차보완

let visited = [];
const testCase = +input[0];
let favorite = [];
let selected = [];
let count = 0;
for (let t = 0; t < testCase; t++) {
  const memberCnt = +input[1 + 2 * t];
  favorite = input[2 + 2 * t].split(' ').map((e) => e - 1);
  count = 0;
  visited = new Array(memberCnt).fill(false);

  for (let i = 0; i < memberCnt; i++) {
    if (!visited[i]) {
      visited[i] = true;
      const like = favorite[i];
      if (like === i) {
      } else if (visited[like]) {
        count++;
      } else {
        selected = [i];
        dfs(like);
      }
    }
  }
  console.log(count);
}

function dfs(like) {
  visited[like] = true;
  const favOfLike = favorite[like];
  if (favOfLike === like) {
    count += selected.length;
  } else if (visited[favOfLike]) {
    const targetIndex = selected.findIndex((e) => e === favOfLike);
    if (targetIndex === -1) {
    //수정
      count += selected.length + 1;
    } else {
      count += targetIndex;
    }
  } else {
    selected.push(like);
    dfs(favOfLike);
  }
}

순환중이지 않다면의 조건을 잘못 걸었다.. selected.length+1 왜냐면   순회대상은 아직 selected배열에 안 넣었으니깐..

성능도 향상이 많이 됐다. 앞에 처음 맞춘 코드는 강사님 코드입니다.

성능차이가 나는건,  강사님의 경우 방문과 완료 두 가지 배열을 사용하고,  성공적으로 팀을 이룬 ( 사이클이 생성된 경우) 경우에 result배열에 넣는 방식을 사용하였습니다. 즉 3개의 배열에 값을 수정하면서 최종결과가 도출됐기 떄문에, 시간도 1.5배 정도 걸린 것이라 저는 생각했습니다..

 

DFS랑 그래프에 유독 약했는데, 혼자 풀었던 게 성능이 좋아서 기분이 좋네요,,

const fs = require('fs');
const input = fs
  .readFileSync(__dirname + '/input.txt')
  .toString()
  .split('\n');

let testCases = +input[0];
let line = 1;
while (testCases--) {
  let n = +input[line];
  let graph = [0, ...input[line + 1].split(' ').map(Number)];
  let visited = new Array(n + 1).fill(false);
  let finished = new Array(n + 1).fill(false);
  let result = [];
  for (let x = 1; x <= n; x++) {
    if (!visited[x]) dfs(x, graph, visited, finished, result);
  }
  line += 2;
  console.log(n - result.length);
}
function dfs(x, graph, visited, finished, result) {
  visited[x] = true;
  let y = graph[x];
  if (!visited[y]) {
    dfs(y, graph, visited, finished, result);
  } else if (!finished[y]) {
    while (y !== x) {
      result.push(y);
      y = graph[y];
    }
    result.push(x);
  }
  finished[x] = true;
}

 

 

728x90

DFS 알고리즘에 대해서 내가 약한 거 같다.. 패스트캠퍼스 강의를 들으면서 먼저 풀었는데, 실패해서 강의자분의 코드를 리뷰하면서 dfs 알고리즘에 대해서 정리하고자 게시글을 쓰게 되었습니다.

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

 

2842번: 집배원 한상덕

상덕이는 언덕 위에 있는 마을의 우체국에 직업을 얻었다. 마을은 N×N 행렬로 나타낼 수 있다. 행렬로 나뉘어진 각 지역은 우체국은 'P', 집은 'K', 목초지는 '.' 중 하나로 나타낼 수 있다. 또, 각

www.acmicpc.net

const fs = require('fs');
const input = fs
  .readFileSync(__dirname + '/input.txt')
  .toString()
  .split('\n');

const n = +input[0];
const arr = [];
for (let i = 1; i <= n; i++) {
  arr.push(input[i].split(''));
}

const height = [];
for (let i = n + 1; i <= n + n; i++) {
  height.push(input[i].split(' ').map(Number));
}

let dx = [-1, -1, -1, 0, 0, 1, 1, 1];
let dy = [-1, 0, 1, -1, 1, -1, 0, 1];

let uniqueHeight = new Set();
let target = 0;
let result = 1e9;
let startX;
let startY;
for (let i = 0; i < n; i++) {
  for (let j = 0; j < n; j++) {
    uniqueHeight.add(height[i][j]);

    if (arr[i][j] === 'P') {
      startX = i;
      startY = j;
    }
    if (arr[i][j] === 'K') target += 1;
  }
}

uniqueHeight = [...uniqueHeight].sort((a, b) => a - b);
let left = 0;
let right = 0;
let middle = 0;
let cnt = 0;
for (let i = 0; i < uniqueHeight.length; i++) {
  if (uniqueHeight[i] === height[startX][startY]) {
    right = i;
    middle = i;
    break;
  }
}
let visited = [];
while (true) {
  while (true) {
    visited = [];
    cnt = 0;
    for (let i = 0; i < n; i++) visited.push(new Array(n).fill(false));

    dfs(startX, startY);
    if (right < uniqueHeight.length - 1 && cnt < target) right += 1;
    else break;
  }
  if (cnt === target) {
    result = Math.min(result, uniqueHeight[right] - uniqueHeight[left]);
  }
  left += 1;
  if (left > middle ) break;
}

console.log(result);

function dfs(x, y) {
  visited[x][y] = true;
  if (arr[x][y] === 'K') cnt += 1;
  for (let i = 0; i < 8; i++) {
    let nx = x + dx[i];
    let ny = y + dy[i];
    if (nx < 0 || nx >= n || ny < 0 || ny > n) continue;
    if (!visited[nx][ny]) {
      if (
        height[nx][ny] >= uniqueHeight[left] &&
        height[nx][ny] <= uniqueHeight[right]
      ) {
        dfs(nx, ny);
      }
    }
  }
}

 

문제는 배달을 하면서 각 지역마다 높이가 있는데, 배달을 하기 위해서 필요한 높이차의 최소값을 구하는 것 입니다.

3
P..
.KK
...
3 2 4
7 4 2
2 3 1

입력 예시는 위와 같은데, P는 시작점, K는 반드시 들려야하는 지점입니다.

이때 한상덕 씨는 대각선 및 좌우로 이동할 수 있으니 3에서 시작해서 4와 2를 방문하면 되니 답은 4-2 인 2가 나옵니다.

이를 알고리즘으로 구현할 때, 로직은 아래 순서 입니다.

1. 우선 입력값을 통해서, 각각의 유니크한 높이를 구한다. 이후 시작점을 찾고 시작점의 높이를 구합니다.

2. 투포인터 개념으로 접근할 건데, 유니크한 높이를 구한 배열에서 좌측포인터는 0, 우측포인터는 시작점의 높이 인댁스를 구합니다. 

3. 이후 dfs를 통하여서, 각각의 지점에 포인터의 높이 범위 내의 있는 경우 방문을 진행합니다. 그리고 만약 방문 지점이 타겟 숙소인 경우 count를 올려줍니다. 

4. 이때 count가 목표숙소 수보다 작다면, 높이 범위가 맞지 않아서 방문 못하는 것이기 때문에 우측포인터의 index를 증가시켜줍니다.

5. 이후 방문이 완료되면(count와 타겟수가 같다면)  우측 index값은 고정시킵니다.

6. 이후 좌측 포인터를 이동시켜서 좌측 포인터의 최소값을 찾습니다.

 

즉 위 문제는 투 포인터와 dfs 로직이 섞여 있습니다.

하나하나 해석하면

 

STEP1 

const fs = require('fs');
const input = fs
  .readFileSync(__dirname + '/input.txt')
  .toString()
  .split('\n');

const n = +input[0];
const arr = [];
for (let i = 1; i <= n; i++) {
  arr.push(input[i].split(''));
}

const height = [];
for (let i = n + 1; i <= n + n; i++) {
  height.push(input[i].split(' ').map(Number));
}

let dx = [-1, -1, -1, 0, 0, 1, 1, 1];
let dy = [-1, 0, 1, -1, 1, -1, 0, 1];

1. 우선 입력값을 받고, 총 개수를 특정하고, 각각의 높이값과 위치에 대한 정보를 추출해서 배열에 담습니다.

2. dfs시에 배열 내에서 상하좌우대각선 이동을 하기에 dx,dy를 통해서 총 8방위를 담아줍니다.

 

STEP2

let uniqueHeight = new Set();
let target = 0;
let result = 1e9;
let startX;
let startY;
for (let i = 0; i < n; i++) {
  for (let j = 0; j < n; j++) {
    uniqueHeight.add(height[i][j]);

    if (arr[i][j] === 'P') {
      startX = i;
      startY = j;
    }
    if (arr[i][j] === 'K') target += 1;
  }
}

uniqueHeight = [...uniqueHeight].sort((a, b) => a - b);

 

1. 이후 높이값을 통해서 유니크한 높이값을 찾아줍니다. ( 포인터의 이동시에 중복된 높이는 필요없기 때문!)

2. 결과값은 우선 가능한 가장 큰 값을 할당 (1e9가 문제 제약 조건이었습니다)

3. 이후 반복문을 통해서 각각의 높으로 Set에 담고, K가 나오면 target 개수 증가, P가 나오면 시작점으로 기록

4. 마지막으로 완성된 Set을  배열로 바꾸고 오름차순으로 정렬 (오름차순이야지 포인터가 의미 있습니다)

 

STEP3

 

let left = 0;
let right = 0;
let middle = 0;
for (let i = 0; i < uniqueHeight.length; i++) {
  if (uniqueHeight[i] === height[startX][startY]) {
    right = i;
    middle = i;
    break;
  }
}
let cnt = 0;
let visited = [];
while (true) {
  while (true) {
    visited = [];
    cnt = 0;
    for (let i = 0; i < n; i++) visited.push(new Array(n).fill(false));

    dfs(startX, startY);
    if (right < uniqueHeight.length - 1 && cnt < target) right += 1;
    else break;
  }
  if (cnt === target) {
    result = Math.min(result, uniqueHeight[right] - uniqueHeight[left]);
  }
  left += 1;
  if (left > middle ) break;
}

console.log(result);

 

1. 방문 배열을 만들어줍니다. 방문 배열을 통해서 dfs로 각각의 좌측포인터와 우측포인터에 대한 탐색을 시작합니다.

2. 우측 포인터의 최소값은 시작점의 값으로 설정하고 중간지점을 설정합니다 ( 탈출 조건 용도로 중간지점 설정)

3. 이후 방문 배열(visited)과 횟수(cnt)를 선언합니다.

 

4. 반복문에서 dfs을 통해서 집에 도착할 경우 cnt를 증가시키고 작은 경우 right의 값을 증가, 최종적으로 cnt가 목표수와 같아지면 결과값(이전값과 비교해서 더 작으면)을 할당합니다.

5. 이후 left 값을 증가시킵니다 . 이때 초기 설정했던 middle까지만 확인하고 탈출 시킵니다.

 

 

728x90

악.,. 알고리즘이 계속 RangeError 떠서 너무 궁금해서 테스트 해본다는게 맞았습니다..
https://lhoiktiv.tistory.com/401

 

Node.js)백준 1016번: 제곱 ㄴㄴ 수

https://www.acmicpc.net/problem/1016 1016번: 제곱 ㄴㄴ 수 어떤 정수 X가 1보다 큰 제곱수로 나누어 떨어지지 않을 때, 그 수를 제곱ㄴㄴ수라고 한다. 제곱수는 정수의 제곱이다. min과 max가 주어지면, min보

lhoiktiv.tistory.com

이분 코드 복붙해 갔었습니다... 왜 내껀 안됐을까를 설명해보려고 합니다.

const fs = require('fs');
const [min,max] = fs.readFileSync("./dev/stdin").toString().trim().split(' ').map(Number);

// 총 개수 구하기
let count = max - min + 1;

const primeFilter = new Array(max + 1).fill(1); //RangeError 생긴 부분
// 소수 구하기
const primeNumber = [];
for (let i = 2; i <= Math.sqrt(max); i++) {
  if (primeFilter[i]) {
    primeNumber.push(i);

    for (let j = i ** 2; j <= max; j += i) {
      if (primeFilter[j]) {
        primeFilter[j] = 0;
      }
    }
  }
}
// nono가 아닌 애들 구하기
let notNoNo = new Set();
for (let i = 0; i < primeNumber.length; i++) {
  const primeDouble = primeNumber[i] * primeNumber[i];
  const startPoint = Math.ceil(min / primeDouble);

  for (let j = startPoint; j <= max / primeDouble; j++) {
    notNoNo.add(j * primeDouble);
  }
}

console.log(count - notNoNo.size);

RangeError는 허용되지 않는 범위의 값을 주려고 할 때 , 생긴다고 하더군요.

https://runebook.dev/ko/docs/javascript/global_objects/rangeerror

 

차이점은 max의 제곱근까지만 소수를 구하면 되는데 저는 max까지 에란토스체로 거르고 있었더군요...

이를 보완해도 시간초과가 떴다..

let count = max - min + 1;

const maxSqrt = Math.floor(Math.sqrt(max));
const primeFilter = new Array(maxSqrt + 1).fill(true);

const primeNumber = [];
for (let i = 2; i <= primeFilter.length; i++) {
  if (primeFilter[i]) {
    primeNumber.push(i);

    for (let j = i ** 2; j <= max; j += i) {
      if (primeFilter[j]) {
        primeFilter[j] = false;
      }
    }
  }
}

let notNoNo = new Set();
for (let i = 0; i < primeNumber.length; i++) {
  const primeDouble = primeNumber[i] * primeNumber[i];
  const startPoint = Math.ceil(min / primeDouble);

  for (let j = startPoint; j <= max / primeDouble; j++) {
    notNoNo.add(j * primeDouble);
  }
}

console.log(count - notNoNo.size);

 

이떄 에란토스체에서 최종적으로  primeNumber만 구하면 되니깐 함수로 만들어서 효율적으로 하겠다.

function sieveOfEratosthenes(max) {
  const isPrime = new Array(max + 1).fill(true);
  isPrime[0] = isPrime[1] = false;

  for (let p = 2; p * p <= max; p++) {
    if (isPrime[p]) {
      for (let i = p * p; i <= max; i += p) {
        isPrime[i] = false;
      }
    }
  }

  const primes = [];
  for (let i = 2; i <= max; i++) {
    if (isPrime[i]) {
      primes.push(i);
    }
  }

  return primes;
}

let count = max - min + 1;

const maxSqrt = Math.floor(Math.sqrt(max));
const primeNumber = sieveOfEratosthenes(maxSqrt);

let notNoNo = new Set();

for (let i = 0; i < primeNumber.length; i++) {
  const primeDouble = primeNumber[i] * primeNumber[i];
  const startPoint = Math.ceil(min / primeDouble);

  for (let j = startPoint; j <= max / primeDouble; j++) {
    notNoNo.add(j * primeDouble);
  }
}

console.log(count - notNoNo.size);

통과는 했지만 원글보다 더 느리다.. 혹시 set대신 반복문을 하는게 메모리적으로 더 효율적이지 않을까?

 

function sieveOfEratosthenes(max) {
  const isPrime = new Array(max + 1).fill(true);
  isPrime[0] = isPrime[1] = false;

  for (let p = 2; p * p <= max; p++) {
    if (isPrime[p]) {
      for (let i = p * p; i <= max; i += p) {
        isPrime[i] = false;
      }
    }
  }

  const primes = [];
  for (let i = 2; i <= max; i++) {
    if (isPrime[i]) {
      primes.push(i);
    }
  }

  return primes;
}

let count = max - min + 1;

const maxSqrt = Math.floor(Math.sqrt(max));
const primeNumber = sieveOfEratosthenes(maxSqrt);

const notNoNo = new Array(count).fill(0);

for (let i = 0; i < primeNumber.length; i++) {
  const primeDouble = primeNumber[i] * primeNumber[i];
  let startPoint = Math.ceil(min / primeDouble) * primeDouble;

  while (startPoint <= max) {
    notNoNo[startPoint - min] = 1;
    startPoint += primeDouble;
  }
}

const notNoNoCount = notNoNo.reduce((acc, val) => acc + val, 0);
console.log(count - notNoNoCount);

결국 메모리는 더 컸지만 빠른 코드가 되었다..

 

후 ...

 

후기)

 

프로그래머스에 비해서 백준이 불친절한 느낌인데, 오히려 더 깊게 생각하게 만들어서 코드 개선이 많이 되는 느낌이다.. 문제 난이도 자체는 골드랑 프로그래머스 lv1~2랑 비슷한 느낌?

수정 ) 난이도가 카카오 기출이런건 lv1이랑 프로그래머스 실버~골드 사인거 같은데, 몇몇 거품 문제가 많아서 전체적으로 골드랑 lv3이랑 비슷한 거 같습니다.

+ Recent posts