Skip to content

Conversation

@bona1122
Copy link
Collaborator

[그래프]

문제를 풀면서 나온 개념인 DFS/BFS, 다익스트라 알고리즘에 대해 배운 내용을 정리했습니다.

DFS/BFS

DFS,BFS는 그래프에서 모든 노드를 탐색하는 기본적인 방법이다.

DFS와 BFS 비교

특성 DFS (깊이 우선 탐색) BFS (너비 우선 탐색)
구현 방식 재귀함수 구조 활용 큐(Queue) 자료구조 활용
구현 난이도 상대적으로 간단 상대적으로 복잡
위험 요소 스택 오버플로우 발생 가능 메모리 사용량이 많을 수 있음
탐색 속도 일반적으로 느림 일반적으로 빠름

DFS

  • 시작노드를 스택에 넣고 방문처리
  • 최상단 노드와 인접한 방문하지 않은 노드 확인
    • 있다면, 스택에 삽입 후 방문처리
    • 없다면, 최상단 노드 추출
function dfs(graph, v, visited) {
  visited[v] = true; // 현재노드 방문 처리
  console.log(v);
  for(i of graph[v]){ // 현재 노드와 인접한, 방문하지않은 노드 재귀적 방문
  	if(!visited[i]) dfs(graph, i, visited);
  }
}

BFS

  • BFS는 그래프의 모든 간선 비용이 동일하면, 최단거리/최소횟수 탐색에 쓰이기도한다.

  • 시작노드를 큐에 삽입하고 방문처리

  • 큐에서 노드를 꺼내고, 인접한 방문하지 않은 노드를 큐에 삽입/방문처리 - 계속 반복

function bfs(graph, start, visited) {
  let queue = new Queue();
  // 시작노드 방문처리 + 큐에 삽입
  queue.enqueue(start);
  visited[start] = true;
  // 큐가 빌 때까지 반복
  while (queue.getLength() !== 0) {
    // 큐에서 하나 빼기(탐색 순서 됨)
    const v = queue.dequeue();
    console.log(v);
    // 인접 노드 방문처리 + 큐에 삽입
    for (let i of graph[v]) {
      if (!visited[i]) {
        queue.enqueue(i);
        visited[i] = true;
      }
    }
  }
}

다익스트라 알고리즘

다익스트라 알고리즘은 최단 경로 알고리즘과 그리디 알고리즘의 일종으로,
한 노드에서 다른 모든 노드로 가는 최단 경로를 계산할 때 사용한다. (모든 지점에서 다른 모든 지점까지의 최단 경로는 플로이드 워셜 알고리즘 이용)

+) 음의 간선이 포함되지 않은 경우만 사용 가능하며, 음의 간선이 포함된 경우는 벨만포드 알고리즘을 활용할 수 있다.
+) 매 상황, 방문하지않은 노드 중, 가장 비용이 적은 노드 선택해서 반복하기에 그리디 알고리즘이다.
+) 다익스트라는 중복간선이 있어도 동작하는 알고리즘이다.

  1. 출발노드 설정
  2. 최단 거리 테이블 초기화
  3. 방문하지 않은 노드 중, 최단 거리가 가장 짧은 노드 선택
  4. 3에서 선택한 노드를 거치는 비용을 계산하여 최단거리 테이블 갱신(테이블 갱신이 아닌, 우선순위 큐에 삽입하는 방식도 있다)
  5. 3,4 반복
    +)우선순위 큐: 우선순위가 높은 데이터부터 삭제
let pq = new PriorityQueue();
pq.enqueue([0,start]); // 시작노드에서 시작노드까지의 최단거리는 0
distance[start] = 0;

while(pq.size() !== 0){ // 큐가 빌 때까지
  let [dist, node] = pq.dequeue();
  
  if(distance[node] < dist) continue; // 이미 처리된 노드 스킵(이미 더 짧은 경로를 찾은 경우 스킵)
  
  //인접노드 확인하며 최단거리 갱신 가능한지 확인
  for(let [next, weight] of graph[now]){
  	let cost = dist + weight; // 현재노드를 거쳐서 다음 노드로 가는 비용
    
    if(cost < distance[next]){ // 최단거리테이블 갱신하고, 큐에 삽입
      distance[next] = cost;
      pq.enqueue([cost, next]);
    }
  }
}

다익스트라 수행 시, 테이블에 한 노드의 각 노드까지의 최단거리정보가 저장된다. (최단거리가 아닌, 경로를 구하려면 부모 배열을 이용한 역추적이나, 역방향 그래프를 이용한 BFS 역추적 구현이 추가적으로 필요)

📌 푼 문제


📝 간단한 풀이 과정

게임 맵 최단거리

  • 2차원 격자에서 (0, 0) 부터 오른쪽 끝의 지점까지의 최단거리를 구하는 문제였는데,
  • 모든 간선의 비용이 동일하다고 볼 수 있으므로 BFS를 이용해서 최단거리를 구할 수 있었습니다.
const solution = (maps) => {
  const n = maps.length
  const m = maps[0].length
  const dir = [
    [1, 0],
    [-1, 0],
    [0, -1],
    [0, 1],
  ]
  const visited = Array.from({ length: n }, () => Array(m).fill(false))
  visited[0][0] = true
  const q = [[0, 0, 1]] // [x,y,거리] 저장

  while (q.length) {
    let [x, y, depth] = q.shift()
    if (x === n - 1 && y === m - 1) {
      return depth
    }
    for (let [dx, dy] of dir) {
      let nx = x + dx
      let ny = y + dy
      if (nx >= 0 && nx < n && ny >= 0 && ny < m) {
        if (maps[nx][ny] === 1 && !visited[nx][ny]) {
          visited[nx][ny] = true
          q.push([nx, ny, depth + 1])
        }
      }
    }
  }

  return -1
}

타겟 넘버

  • 배열의 각 숫자는 빼거나/더하는 분기로 나누어질 수 있으므로 dfs를 먼저 떠올렸습니다.
  • 연습차원에서 큐에 [현재까지의 합계, 다음에 처리할 인덱스]를 저장하는 방식으로 bfs로도 구현해보았습니다.
// DFS 풀이
function solution(numbers, target) {
  let result = 0

  const dfs = (acc, depth) => {
    if (depth === numbers.length) {
      if (acc === target) result++
      return
    }
    dfs(acc + numbers[depth], depth + 1)
    dfs(acc - numbers[depth], depth + 1)
  }

  dfs(0, 0)
  return result
}

// BFS 풀이
function solution(numbers, target) {
  let result = 0
  const queue = []

  queue.push([0, 0]) // [합계, 처리할 인덱스]

  while (queue.length) {
    const [acc, depth] = queue.shift()

    if (depth === numbers.length) {
      if (acc === target) result++
      continue
    }

    queue.push([acc + numbers[depth], depth + 1])
    queue.push([acc - numbers[depth], depth + 1])
  }

  return result
}

전력망을 둘로 나누기

  • 모든 간선에 대해 끊어지는 것을 확인했습니다.(완전탐색)
  • 하나씩(a-b) 끊어보면서 끊어진 노드 중 하나(a)를 기준으로 bfs를 수행해서 포함된 노드수를 카운트하고
  • 다른 전력망은 전체노드에서 a의 전력망에 속하는 노드 수를 빼서 크기를 구했습니다.
  • bfs 구현 시, 끊어진 것을 처리하기 위해 끊어진 노드를 미리 방문처리해서 풀었습니다.
// n개의 송전탑이 트리 형태
// 하나를 끊어서 2개로 분할하고자 함. 최대한 갯수 같게.
// 와이어중에 하나 끊은다음에, 하나에 대해 bfs수행하기
const solution = (n, wires) => {
  let diff = n
  const graph = Array.from({ length: n + 1 }, () => [])
  for ([a, b] of wires) {
    graph[a].push(b)
    graph[b].push(a)
  }
  // 송전선 별로 끊어보기(완전탐색)
  for ([a, b] of wires) {
    const visited = new Array(n + 1).fill(false)
    visited[b] = true // b로는 못가도록 처리

    let cnt = 0
    const q = [a]
    visited[a] = true

    while (q.length) {
      let cur = q.shift()
      cnt++

      for (let next of graph[cur]) {
        if (!visited[next]) {
          visited[next] = true
          q.push(next)
        }
      }
    }

    diff = Math.min(diff, Math.abs(cnt - (n - cnt))) // 전력망 노드 차이 갱신
  }
  return diff
}

배달

  • 가중치, 중복간선이 있는 무방향 그래프에서 특정노드로부터 모든 노드까지의 최단거리를 구하는 문제라고 파악한 후, 다익스트라 알고리즘을 적용했습니다.
// 가중치 있는 무방향 그래프(중복간선 존재)
// 다익스트라 알고리즘 활용
// 1번에서 각 마을로 음식배달. K시간 이하로 배달 가능한 곳 개수 구하기

const solution = (N, road, K) => {
  const graph = Array.from({ length: N + 1 }, () => [])
  for (let [a, b, w] of road) {
    graph[a].push([b, w])
    graph[b].push([a, w])
  }

  const distance = Array(N + 1).fill(Infinity)
  distance[1] = 0

  const pq = [[0, 1]] // [거리, 노드]

  while (pq.length > 0) {
    pq.sort((a, b) => a[0] - b[0]) // 매번 sort보다 우선순위큐 구현해서 사용하는 것이 베스트
    const [dist, cur] = pq.shift()

    if (distance[cur] < dist) continue

    for (const [next, w] of graph[cur]) {
      const cost = dist + w
      if (cost < distance[next]) {
        distance[next] = cost
        pq.push([cost, next])
      }
    }
  }

  return distance.filter((dist) => dist <= K).length
}

미로 탈출

  • 출발점 - 레버 - 출구 까지의 최단경로를 구하는 문제여서 먼저 출발점,레버,출구의 위치를 파악했습니다.
  • 출발점 - 레버 의 최단경로와 레버 - 출구의 최단경로를 더해서 답을 도출했습니다.
  • 이 문제 또한, 모든 간선의 비용이 동일하다고 볼 수 있으므로 BFS를 이용해서 최단거리를 구했습니다.
// 출발점부터 빠르게 레버있는 곳으로 간 후, 다시 레버에서 탈출구로 가는 것 목표. 못가면 -1 반환
const solution = (maps) => {
  const dir = [
    [0, 1],
    [0, -1],
    [1, 0],
    [-1, 0],
  ]
  const n = maps.length
  const m = maps[0].length
  let S, L, E
  // 시작, 레버, 출구 위치 저장하고
  // 시작 - 레버 / 레버 - 출구 경로의 최단경로 더하기
  maps = maps.map((row, x) => {
    row = row.split("")
    row.forEach((item, y) => {
      if (item === "S") S = [x, y]
      if (item === "L") L = [x, y]
      if (item === "E") E = [x, y]
    })
    return row
  })

  const bfs = (start, end) => {
    const visited = Array.from({ length: n }, () => Array(m).fill(false))
    const q = [[...start, 0]]

    while (q.length) {
      let [x, y, dist] = q.shift()
      if (x === end[0] && y === end[1]) return dist

      for (let [dx, dy] of dir) {
        let [nx, ny] = [x + dx, y + dy]
        if (nx >= 0 && nx < n && ny >= 0 && ny < m) {
          if (!visited[nx][ny] && maps[nx][ny] !== "X") {
            visited[nx][ny] = true
            q.push([nx, ny, dist + 1])
          }
        }
      }
    }

    return -1
  }

  let toL = bfs(S, L)
  if (toL === -1) return -1
  let toE = bfs(L, E)
  if (toE === -1) return -1

  return toL + toE
}

@bona1122 bona1122 self-assigned this Feb 26, 2025
@JooKangsan JooKangsan merged commit b4692f5 into codingTestStd:main Feb 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants