✍️문제 설명

전화번호부에 적힌 전화번호 중, 한 번호가 다른 번호의 접두어인 경우가 있는지 확인하려 합니다.

전화번호가 다음과 같을 경우, 구조대 전화번호는 영석이의 전화번호의 접두사입니다.

  • 구조대 : 119
  • 박준영 : 97 674 223
  • 지영석 : 11 9552 4421

전화번호부에 적힌 전화번호를 담은 배열 phone_book 이 solution 함수의 매개변수로 주어질 때, 어떤 번호가 다른 번호의 접두어인 경우가 있으면 false를 그렇지 않으면 true를 return 하도록 solution 함수를 작성해주세요.

제한 사항
  • phone_book의 길이는 1 이상 1,000,000 이하입니다.
  • 각 전화번호의 길이는 1 이상 20 이하입니다.
  • 같은 전화번호가 중복해서 들어있지 않습니다.

조건으로 얻을 수 있는 정보

1. 배열의 길이가 `10^6` 이므로, 시간복잡도 `O(n x logn)`이 되도록 구현하여야 1초(10^8) 내에 실행 가

2. `O(n^2)` 방법은 시간 내에 처리 불가능하지만, 시간이 부족하면 이를 채택 (실전)

 

1️⃣ Brute False

누구나 가장 먼저 생각나는 방법으로, 각 phone_number를 순회하면서 해당 번호를 접두어로 사용하고 있는 phone_number가 있는지 판단하는 방법이다.

 

- 최소한의 최적화를 위해, 비교하는 두 phone_number를 문자열이 아닌 숫자로 변환하여 비교하였다.

is_jupdo : 문자열 비교

is_jupdo2 : 숫자 비

// #시도1. Brute false 문자열 비교, 시간초과
function is_jupdo(target, diff) {
  const target_len = target.length;

  for (let i = 0; i < target_len; i++) {
    if (target[i] !== diff[i]) {
      return false;
    }
  }
  return true;
}

// #시도2. Brute False 숫자 비교, 시간초과
function is_jupdo2(target, diff, power_10) {
  const target_len = target.length;
  const diff_len = diff.length;

  if (Math.floor(diff / power_10[diff_len - target_len]) - target === 0) {
    return true;
  }
  return false;
}
function solution(phone_book) {
  const len = phone_book.length;
  const power_10 = [1];

  // 10의 거듭제곱 값 넣기
  for (let i = 1; i <= 20; i++) {
    power_10[i] = power_10[i - 1] * 10;
  }

  phone_book.sort((a, b) => a.length - b.length);

  for (let i = 0; i < len - 1; i++) {
    const target = phone_book[i];
    for (let j = i + 1; j < len; j++) {
      const diff = phone_book[j];
      // Compare
      if (is_jupdo2(target, diff, power_10)) {
        return false;
      }
    }
  }
  return true;
}

 

효율성 테케 실행 결과 역시나 실패

 

2️⃣Hash Table

1. phone_number 값을 객체 (Hash Table)에 저장한다. (아래 그림 참고)

2. 각 phone_number를 순회하며, 생성가능한 모든 접두어가 객체(Hash Table)에 존재하는지 확인한다.

Hash Table 구조

// 시도3: Hash Table만들기
function solution(phone_book) {
  const table = {};
  for (const phone of phone_book) {
    table[phone] = true;
  }

  for (const phone of phone_book) {
    const len = phone.length;
    for (let i = 1; i < len; i++) {
      const substr = phone.substring(0, i);
      if (table[substr] === true) {
        return false;
      }
    }
  }

  return true;
}

 

효율성 테케 실행 결과 성공

 


분석

1️⃣Brute False

  • solution 함수에서는 전화번호를 길이 순으로 정렬한 후, 각 번호와 그 다음 번호들 간의 비교를 반복한다.
  • 여기서 이중 반복문을 사용하므로, 시간 복잡도는 최악의 경우 O(n^2)다.
  • 또한, is_jupdo 호출은 두 문자열의 길이에 비례하여 O(m) 시간이 소요되며, 이는 두 문자열 길이의 최대값에 의해 결정된다.
  • 따라서 전체 시간 복잡도는 `O(n^2×m)`로, 전화번호의 수가 많거나 각 번호의 길이가 길면 성능이 최악이다.

2️⃣Hash Table

  • 해시 테이블에 각 전화번호를 저장한 후, 각 전화번호의 접두사가 테이블에 있는지 확인한다. (O(n))
  • 각 phone_number의 길이-1 만큼 hash table에 접두어가 있는지 확인한다. (O(m))
  • 따라서, 이 방법의 시간 복잡도는 `O(n×m)`로, n은 전화번호 개수이고, m은 각 전화번호의 길이이다.

'PS' 카테고리의 다른 글

[파이썬] 코테 꿀팁  (2) 2024.10.10
Javascript PS 꿀팁 저장소  (0) 2024.06.29

파이썬은 아름답다. 생각나는대로 쓰면 그게 파이썬 문법이다.

 

# 2차원 배열의 특정 크기만큼 Slicing하기

2차원 배열에서 특정 크기를 slicing하기 위해서는 일반적으로, 새로운 배열을 선언하여 각 자리의 값을 `append` 하는 방식으로 구현할 수 있다.

# 크기가 3 * 3인 sub array 만들기
square = []
for x in range(i, i + 3):
    for y in range(j, j + 3):
        square.append(board[x][y])

 

만약, 2차원 배열의 가능한 모든 값들에 대해 가능한 모든 크기의 subArray를 만들어야 한다면 코드의 가독성이 저해된다.

for i in (0, 3, 6):
    for j in (0, 3, 6):
    	# 크기가 3 * 3인 sub array 만들기
        square = []
        for x in range(i, i + 3):
            for y in range(j, j + 3):
                square.append(board[x][y])

 

따라서, 다음과 같이 선언하여 사용하면 훨씬 편리하게 사용가능하다.

 

for i in (0, 3, 6):
    for j in (0, 3, 6):
    	# 크기가 3 * 3인 sub array 만들기
        square = [[board[x][y] for x in range(i, i + 3)] for y in range(j, j + 3)]

전체 코드 (가능한 모든 크기의 subArray 만들기)

ARR_LEN = 10
SUB_ARR_LEN = 4

# 배열 짜르기
for i in range(0,ARR_LEN -SUB_ARR_LEN + 1):
    for j in range(0,ARR_LEN -SUB_ARR_LEN + 1):
        subarr = [[arr[row][col] for row in range(i,i+SUB_ARR_LEN)] for col in range(j,j+SUB_ARR_LEN)]
        # Do Something With Subarr

 


 

# 약팁

1. 2차원 배열 출력하기

def print_2darr(array):	
    for arr in array:
        print(*arr)

 

 

생기는 대로 입력하겠습니다~ (마지막 입력: 2024-10-10)

'PS' 카테고리의 다른 글

프로그래머스 전화번호 목록 JS 풀이  (0) 2024.11.04
Javascript PS 꿀팁 저장소  (0) 2024.06.29

(BFS에 대한 기본적인 지식이 있다고 가정하고 작성하였습니다. BFS에 대한 학습이 필요하면 여기를 참고해주세요)

 

BFS는 그래프 자료구조에서 유용하게 쓰이는 알고리즘이다.

백준 2178번에서 Time Out Error가 발생하여 이를 최적화한 과정을 기록하여 공유하고자 한다.

 

문제

 

전형적인 BFS 문제이며 로직은 다음과 같다.

  1. 시작점을 `queue`에 넣는다. ([row, col, 지금까지 거리] 형태로 넣어, 최소거리를 추적)
  2. `queue`의 길이가 0이 될때까지 다음 행동을 반복한다.
    • 해당 지점으로부터 4방향중 이동가능한 방향을 찾는다.
    • 이동가능한 방향을 큐에 넣는다.
  3. 이때, `queue`에서 꺼낸 위치가 최종 위치이면 최소값인지 판단하여 결과를 최신화한다.
// 최초 코드 (Time out Error)
const fs = require("fs");
const input = fs
  .readFileSync(process.platform === "linux" ? "/dev/stdin" : "../input.txt")
  .toString()
  .trim()
  .split("\\n");

const [N, M] = input.shift().split(" ").map(Number);
const arr = input.map((subarr) => subarr.trim().split(""));
// # 방향 변수
const dr = [0, 1, 0, -1];
const dc = [1, 0, -1, 0];

const queue = [[0, 0, 1]];
let visited = Array.from(new Array(N), () => new Array(M).fill(false));
visited[0][0] = true;
let min = -1;

while (queue.length) {
  const [row, col, cnt] = queue.shift();
  visited[nrow][ncol] = true;
  // # 도착 지점 도달
  if (row === N - 1 && col === M - 1) {
    min = min === -1 ? cnt : Math.min(min, cnt);
  }
  // # 도착 지점 이동중
  else {
    let i = 0;
    for (i; i < 4; i++) {
      const [nrow, ncol] = [row + dr[i], col + dc[i]];
      if (
        nrow >= 0 &&
        nrow < N &&
        ncol >= 0 &&
        ncol < M &&
        arr[nrow][ncol] === "1"
      ) {
        queue.push([nrow, ncol, cnt + 1]);
      }
    }
  }
}
console.log(min); // 결과 출력

Time out Error가 발생하였다.

로직에는 전혀 문제가 없어 보였고, 최적화가 필요한 것 같았다.

시스템적 최적화 외 실행 횟수를 줄이기 위한 최적화를 하기 위해서는 `queue`에 들어가는 좌표의 개수를 줄이는 방법 밖에 없다.

 

그래서, `visited`에 집중하기 시작했다.

`visited`는 지금까지 방문한 노드와 방문하지 않은 노드를 구분하기 위해 사용하였다.

실제, BFS에서는 방문한 노드를 다시 방문하지 않기 때문이다. (그림 참고)

출처: https://cdragon.tistory.com/entry/Algorithm-너비-우선-탐색BFS그래프-탐색

 

해당 문제는 모든 경로를 고려해야 한다고 판단하여, `visited[row][col] = true` 를 실제 방문했을 때 넣어주었던 것이다. 하지만, BFS의 개념에 대한 이해가 있었다면 이렇게 설정할 이유는 없다.

BFS는 DFS와 달리 같은 LEVEL의 노드들을 먼저 탐색한다.

즉, BFS를 진행하다가 어떤 좌표에 도달했다면, 해당 경로는 최소 경로가 된다.
따라서, 처음 최종 경로에 도달하는 경우가 최소 경로이다.

 

 

예를 들어,

출발 2 2 2
3 0 0 2
3 0 0 2
3 0 3 도착
3 3 3 0

이렇게 구성되어 있는 지도가 있다고 가정하자. (전체 좌표의 부분만을 예시로 설명합니다)

  1. 도착 위치까지 2로 이루어진 경로와 3로 이루어진 경로 2가지 경로가 존재한다.
  2. 2경로의 거리가 3경로의 거리보다 짧으며, BFS 로직 상 먼저 실행된다.
    참고로, BFS의 Queue에 좌표를 추가하는 행위는 "해당 좌표로부터 최소 거리를 찾겠습니다.” 를 의미한다.
  3. 결국, 나중에 도달한 방향(3경로)은 이미 최소 경로가 아니므로 고려할 필요가 없다.

설명이 길었습니다.

요약하자면

  1. `visited[row][col] = true`는 해당 위치가 valid한 위치라고 판단되었을때 바꿔주면 최적화가 가능합니다.
  2. 최종 위치에 도달하면 `break` 합니다.
const fs = require("fs");
const input = fs
  .readFileSync(process.platform === "linux" ? "/dev/stdin" : "../input.txt")
  .toString()
  .trim()
  .split("\n");

const [N, M] = input.shift().split(" ").map(Number);
const arr = input.map((subarr) => subarr.trim().split(""));
// # 방향 변수
const dr = [0, 1, 0, -1];
const dc = [1, 0, -1, 0];

const queue = [[0, 0, 1]];
let visited = Array.from(new Array(N), () => new Array(M).fill(false));
visited[0][0] = true;
let min = -1;

while (queue.length) {
  const [row, col, cnt] = queue.shift();

  // # 도착 지점 도달
  if (row === N - 1 && col === M - 1) {
    min = cnt; 
	break; // 한가지 밖에 없습니다.
  }
  // # 도착 지점 이동중
  else {
    let i = 0;
    for (i; i < 4; i++) {
      const [nrow, ncol] = [row + dr[i], col + dc[i]];
      if (
        nrow >= 0 &&
        nrow < N &&
        ncol >= 0 &&
        ncol < M &&
        arr[nrow][ncol] === "1" &&
        !visited[nrow][ncol]
      ) {
        queue.push([nrow, ncol, cnt + 1]);
        visited[nrow][ncol] = true;
      }
    }
  }
}
console.log(min);

추가

만약에, 모든 경로를 구해야 하는 경우라면 처음에 했던 코드대로 진행해야 합니다.

참고 문헌

https://cdragon.tistory.com/entry/Algorithm-너비-우선-탐색BFS그래프-탐색

'PS > BFS+DFS' 카테고리의 다른 글

[PS] 파이썬 BFS / DFS 구현방법  (2) 2024.04.12

+ Recent posts