본문 바로가기

이것이 취업을 위한 코딩테스트다 with 파이썬

DFS&BFS

DFS/BFS 

그래프를 탐색하기 위한 대표적인 두 가지 알고리즘

 

꼭 필요한 자료구조 기초

탐색이란? 많은 양의 데이터 중에서 원하는 데이터를 찾는 과정

프로그래밍에서 그래프, 트리 등의 자료구조 안에서 탐색을 하는 문제가 많다.

ex) DFS(Depth First Search, 깊이 우선 탐색), BFS(Breath First Search, 너비 우선 탐색)

 

DFS, BFS를 제대로 이해하기 위해서는 기본 자료구조인 스택에 대한 이해가 전제된다.

 

자료구조란? 데이터를 표현하고 관리하고 처리하기 위한 구조

- 스택는 자료구조의 기초 개념

- 핵심함수

  • 삽입(push) : 데이터를 삽입
  • 삭제(pop) : 데이터를 삭제

오버플로(Overflow) : 특정한 자료구조가 사용할 수 있는 데이터의 크기를 이미 가득 찬 상태에서 삽입 연산을 할 경우 발생. 즉, 저장 공간을 벗어나 넘쳐흐를 때 발생.

언더플로(Underflow) : 특정한 자료구조에 데이터가 전혀 들어 있지 않은 상태에서 삭제 연산을 할 경우 발생.

 

 

 

스택

선입후출 또는 후입선출 구조 ex) 박스 쌓기

5 -> 2 -> 3 -> 7 -> 삭제(7) -> 1 -> 4 -> 삭제(4) 의 과정이다.

stack이라는 list를 생성하고, appned()를 통해서 리스트에 값들을 삽입한다. 

pop()으로 인해 가장 최근에 삽입된 7, 4 값들이 삭제되는 것이다. (선입후출 구조)

 

 

큐는 선입선출 구조로, 우리의 실생활 속에서 마치, 대기 줄을 서는 것과 같다.

나중에 온 사람은 나중에 들거가는 '공정한' 자료구조라고 비유된다.

 

5 -> 2 -> 3 -> 7 -> 삭제(5) -> 1 -> 4 -> 삭제(2)

큐를 구현하기 위해서는 collections 모듈에서 제공하는 deque 라이브러리를 사용해야 한다.

deque는 스택과 큐의 장점을 모두 채택한 것으로, 데이터를 넣고 빼는 속도가 리스트 자료형에 비해 효율적이다.

 

append()를 사용하여 값을 삽입한다는 점은 스택과 같지만

popleft()를 통해 가장 첫 번째 원소를 삭제한다는 점에서 가장 최근에 들어온 원소를 삭제(pop)하는 스택과 구분된다.

 

 

 

재귀 함수

DFS와 BFS를 구현하기 위해서는 재귀 함수를 이해하고 있어야 한다.

재귀함수란? 자기 자신을 다시 호출하는 함수

 

컴퓨터 내부에서 재귀 함수의 수행은 스택 자료구조를 이용한다.

함수를 계속 호출 했을 때 가장 마지막에 호출한 함수가 먼저 수행이 끝나면, 그 앞에 함수 호출이 종료되기 때문이다.

따라서 스택을 활용해야 하는 알고리즘은 재귀 함수를 이용해서 간편하게 구현 가능하다. ex) DFS

 

팩토리얼 문제

팩토리얼 문제에서 반복문과 재귀 함수를 사용하였다.

재귀 함수를 사용한 코드가 비교적 좀 더 간결하게 보이고, 팩토리얼의 수학적 점화식과 거의 유사하게 표현이 가능하다.

 

 

 

DFS

- 깊이 우선 탐색 (Depth First Search)

- 그래프에서 깊은 부분을 우선적으로 탐색하는 알고리즘

 

그래프는 노드(Node)간선(Edge)로 표현되며, 노드를 정점(Vertex)라고도 표현한다.

그래프 탐색이란? 하나의 노드를 시작으로 다수의 노드를 방문하는 것을 의미한다.

두 노드가 간선으로 연결되이 있으면 '두 노드가 인접하다(Adjacent)'라고 표현한다.

ex) 노드 = 도시, 간선 = 도로

-> A라는 도시(노드)에서 B라는 노드(도시)로 이동하기 위해, A와 B를 연결한 도로(간선)를 거친다.

 

프로그래밍에서 그래프는 2가지 방식으로 표현

  • 인접 행렬(Adjacency Matrix) : 2차원 배열로 그래프의 연결 관계를 표현하는 방식
  • 인접 리스트(Adjacency List) : 리스트로 그래프의 연결 관계를 표현하는 방식

0 -> 0, 1 -> 1, 2 -> 2

이와 같이 자기자신으로 탐색하는 데에는 비용이 들지 않기 때문에 0으로 표현하고,

1 -> 2, 2 -> 1

이와 같이 상호간에 연결이 없는 경우에는 무한의 비용이 든다고 표현한다. 

실제 코드에서는 논리적으로 정답이 될 수 없는 큰값중 9999999999, 987654321 등의 값으로 초기화 하는 경우가 많다.

 

 

인접 행렬 방식 예제

 

 

인접 리스트 방식 예제

(모든 노드에 연결된 노드에 대한 정보를 차례대로 연결하여 저장)

  메모리 속도
인접 행렬 방식 모든 관계를 저장하기 때문에 메모리 낭비 발생 해당 위치만 확인하기 때문에 비교적 빠름
인접 리스트 방식 연결된 정보만 저장하기 때문에 효율적 연결된 데이터를 하나씩 확인하기 때문에 느림

 

 

DFS 구체적인 동작 과정 (스택 구조를 이용)

  1. 탐색 시작 노드를 스택에 삽입하고 방문 처리
  2. 스택의 최상단 노드에 방문하지 않은 노드가 있으면 그 인접 노드를 스택에 넣고 방문 처리. 
    방문하지 않은 인접한 노드가 없으면 스택에서 최상단 노드를 꺼낸다.
  3. 2번의 과정을 더 이상 수행할 수 없을 때까지 반복한다.

- '방문 처리'는 스택에 한 번 삽입되어 처리된 노드가 다시 삽입되지 않게 체크하는 것을 의미함. 방문 처리를 함으로써 각 노드를 한 번씩만 처리

 

- 깊이 우선 탐색이라는 이름에서부터 알 수 있듯이 단순하게 가장 깊숙이 위치하는 노드에 닿을 때까지 확인(탐색)하면 된다.

 

- 방문하지 않은 노드가 여러 개 있으면 번호가 낮은 순서대로 처리한다. (관행적)

 

- DFS는 스택 자료구조에 기초한다는 점에서 구현이 간단하다.

 

- 실제로 굳이 스택을 쓰지 않아도 되고, 탐색 수행함에 있어서 데이터가 N개 인 경우 O(N)의 시간이 소요된다.

 

- DFS는 스택을 이용하는 알고리즘이기 때문에 실제 구현은 재귀 함수를 이용하면, 매우 간결하게 구현 가능하다.

 

 

dfs 예제

 

순환 순서

스택 구조 변화 :  교재138p ~ 142p 참고

 

 

BFS

- 너비 우선 탐색(Breadth First Search)

- 가까운 노드부터 탐색하는 알고리즘

- 최대한 멀리 있는 노드를 우선으로 탐색하는 방식의 DFS와는 반대되는 방식

- 선입선출 방시그이 큐 자료구조를 이용하는 것이 정석

- 인접한 노드를 반복적으로 큐에 넣으면 자연스럽게 먼저 들어온 것이 먼저 나가게 되어, 가까운 노드부터 탐색 진행

 

BFS 구체적인 동작 과정 (큐 이용)

  1. 탐색 시작 노드를 큐에 삽입하고 방문 처리
  2. 큐에서 노드를 꺼내 해당 노드의 인접 노드 중에서 방문하지 않은 노드를 모두 큐에 삽입 후 방문 처리
  3. 2번 과정을 더 이상 수행할 수 없을 때까지 반복

bfs 예제

큐 구조의 변화 : 교재 144 ~ 146p 참고

 

- deque 라이브러리를 사용하는 것이 좋고, 탐색 수행 시간 O(N) 소요

- 일반적인 경우 DFS보다 좋은 편이다.

 

  DFS BFS
동작 원리 스택
구현 방법 재귀 함수 이용 큐 자료구조 이용

 

 

 

음료수 얼려 먹기

문제 : 교재 149p 참고

 

 

해당 문제는 DFS로 해결할 수 있다. 얼음을 얼릴 수 있는 공간이 상, 하, 좌, 우로 연결되어 있다고 표현할 수 있다.

  1. 특정한 지점의 주변 상, 하, 좌, 우를 살펴본 뒤에 주변 지점 중에서 값이 '0' 이면서 아직 방문하지 않은 지점이 있다면 해당 지점 방문

  2. 방문한 지점에서 다시 상, 하, 좌, 우를 살펴보면서 방문을 다시 진행, 연결된 모든 지점 방문 가능

  3. 1~2번 과정을 모든 노드에 반복하며 방문하지 않은 지점의 수를 센다.

 

나의 풀이

나의 코드를 설명하자면, bfs 함수에서 범위를 벗어나면 False를 리턴하고, 만약 해당 좌표가 방문하지 않은 상태('0')라면, 방문처리('1')하고, 해당 좌표에서 우측과 아래로 이동한 좌표를 다시 bfs함수에 인자값으로 넣어서 반복하였다.

그러면, 주변의 0인 값들을 1로 바꿔주고, 1인 값을 만나면, 각각의 반복되고 있던 bfs함수들이 False를 리턴하면서, 결국 bfs함수를 반복하기 시작했던 좌표로 돌아와서 if문을 벗어나 True를 리턴한다.

위 과정을 반복하면, 값이 0인 좌표들의 주변값들을 모두 1로 바꿔주고, 다 바꿔주어야 True를 리턴해줘서, 이중 for문 안의 if문을 통해, 0으로 뭉친 그룹들의 개수를 구할 수 있다.

 

해설에서는 상, 하, 좌, 우를 모두 표현해줬는데, 본인은 어차피 (0, 0) 좌표에서 오른쪽, 아래 방향으로 확장해나간다고 생각했기 때문에, 오른쪽, 아래만 표현하였다.

 

해설을 참고한 뒤의 코드

해설 코드에는 데이터 값을 입력 받을 때, 하나의 리스트를 선언하고, for문과 append()를 사용하였다.

본인 코드와 비교했을 때, 이 방식이 더 간결하고 좋은 것 같다.

 

 

 

미로 탈출

문제 : 교재 152p 참고

 

BFS를 이용했을 때 매우 효과적으로 해결 가능

BFS는 시작 지점에서 가까운 노드부터 차례대로 그래프의 모든 노드를 탐색하기 때문

시작지점에서부터 BFS를 수행하여 모든 노드의 값을 거리 정보로 넣으면 된다

특정 노드를 방문하면 그 이전 노드의 거리에 1을 더한 값을 리스트에 넣는다.

 

 

해설을 참고한 뒤의 코드

해당 문제를 풀지 못했다. BFS를 사용하려고 했지만, 어떻게 접근해야할지 감이 안잡혀서 고민 끝에 해설을 참고하였다.

생각보다 간단했다.

시작점을 큐에 넣고 while문을 통해서 큐에 있는 값을 popleft하고 해당 좌표의 모든 방향을 검사하여, 다음 좌표가 범위를 벗어나거나, 0이라면 무시해주고, 1이라면 계속 나갈 수 있기 때문에, 해당 좌표의 값을 이전 좌표에 값에서 1 추가한 값으로 갱신해준다. 이 과정을 queue가 빌 때까지 반복하고, graph의 마지막 값을 리턴해준다. 

 

 

느낀점

DFS/BFS part의 도입부에서 개념적인 부분의 내용들은 이해하는데에 큰 어려움이 없었는데, 막상 이 개념으로 문제를 접근하려고 하니, 생각보다 어려웠다. 웬만해서는 해설을 참고하지 않고 문제를 풀고, 해설과 비교해보는 방식으로 진도를 나가는데, 오랜시간 고민해봐도 해결책을 찾지 못한 문제가 존재했다. 개념을 이해했다고는 생각했지만, 그래도 문제에 적용하는데에 있어서, 쉽지 않은 것이 아직 많이 부족한 것 같다. 다른 part들을 마무리하고, 다시 한번 봐야겠다.

'이것이 취업을 위한 코딩테스트다 with 파이썬' 카테고리의 다른 글

다이나믹 프로그래밍  (0) 2021.01.03
이진 탐색  (0) 2021.01.02
정렬  (0) 2021.01.01
구현  (0) 2020.12.28
그리디 알고리즘  (0) 2020.12.27