본문 바로가기

CP Algorithm & Knowledge

0-1 BFS(0-1 Breadth First Search)

728x90
728x90

이 게시글에서는 특정한 상황에 놓인 그래프에서 최단 경로를 찾을 때 $O(V + E)$의 시간 복잡도로 해결할 수 있는 0-1 BFS에 대해 다룬다.

거기! 혹시 코테를 준비하신다면?

딱 말할 수 있다. 0-1 BFS를 공부할 것을 강력히 권한다.

 

본인은 어떤 기업의 코딩 테스트에서 0-1 BFS를 사용할 수 있는 문제를 목격한 뒤로 이 글을 쓰기로 마음먹었기 때문이다.

 

문제에 비공개 조약이 걸려 있으므로 어떤 기업인지 자세히 말할 수 없지만 전통적인 대기업의 계열사이다.

 

따라서 대회에 초점을 맞춘 사람이 아니어도 0-1 BFS를 공부해서 손해를 볼 일은 없을 것이다.

0-1 BFS란?

BFS는 BFS인데 특수한 환경에서 작동할 수 있는 BFS이다. 그 특수한 환경이 뭐냐고?

 

0-1을 보면 무엇인지 추측할 수 있다. 0-1 BFS는 가중치가 0과 1로만 주어진 그래프에서 최단 경로를 찾고자 할 때 BFS를 응용하여 풀 수 있게 된다.

 

아니 최단 경로는 다익스트라가 베스트가 아니란 말인가? 적어도 이 그래프에서는 아니다.

 

보편적으로 사용하는 다익스트라 알고리즘의 시간 복잡도가 $O(E \log E)$ 혹은 $O(E \log V)$인데 0-1 BFS를 사용하면 단지 $O(V + E)$의 선형 시간 복잡도로 문제를 해결할 수 있기 때문이다.

0-1 BFS의 동작

왜 0-1 BFS가 $O(V + E)$에 동작할 수 있을까?

 

이는 BFS에서 노드를 관리하기 위해 큐를 사용하는 대신 덱(deque)을 사용하여 실현할 수 있다.

 

0-1 BFS는 다음 과정에 따라 최단 경로를 찾게 된다.

 

1. 덱의 front에서 현재 노드를 꺼낸다.

 

2. 연결된 인접 노드를 살펴본다.

 

3. 현재 노드까지 소비된 비용 + 그 노드를 향하는 가중치 < 그 노드까지 가는데 소비된 비용이면 소비된 비용을 갱신해준다.

 

4. 노드가 갱신된 상태에서 만약 그 노드를 향하는 가중치가 0이면 덱의 front, 1이면 덱의 back에 삽입하도록 한다.

 

5. 덱에서 더 이상 꺼낼 노드가 없을 때까지 위 과정을 반복한다.

$O(V + E)$ 증명

위 과정을 생각해보자.

 

0-1 BFS를 수행하면 가중치가 1인 간선을 0번 거쳐간 노드 -> 가중치가 1인 간선을 1번 거쳐간 노드 -> .... -> 가중치가 1인 간선을 E번 거쳐간 노드

 

이런 식으로 비용이 적은 경로부터 탐색을 하게 되기 때문에 특정 간선을 2번 이상 지나가는 경우는 없다. $O(E)$

 

똑같이 모든 정점도 2번 이상 경유하는 경우가 없으므로 덱 크기는 최대 $|V|$이다. $O(V)$

 

따라서 0-1 BFS는 $O(V) + O(E) = O(V + E)$의 시간 복잡도를 가지게 된다.

0-1 BFS 문제 풀어보기

백준 온라인 저지에서 0-1 BFS의 입문 문제인 숨바꼭질 3을 풀어보자.

 

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

 

13549번: 숨바꼭질 3

수빈이는 동생과 숨바꼭질을 하고 있다. 수빈이는 현재 점 N(0 ≤ N ≤ 100,000)에 있고, 동생은 점 K(0 ≤ K ≤ 100,000)에 있다. 수빈이는 걷거나 순간이동을 할 수 있다. 만약, 수빈이의 위치가 X일

www.acmicpc.net

 

수빈이가 0초의 비용으로 이동하는 경우와 1초의 비용으로 이동하는 경우가 주어진다.

 

주어진 상황을 0-1 BFS로 탐색하면 최단 경로(가장 빠른 시간)를 구할 수 있게 된다.

 

다만 2배의 위치로 N의 제한을 넘어 이동했을 때 최적해를 가질 수 있다는 점을 유의하여 이동할 수 있는 노드는 넉넉히 20만 개 이상으로 설정해야 한다.

 

위치에 대해 조심하면서 구현하면 0ms로 정답을 받을 수 있다.

 

다음은 정답 코드이다.

#pragma GCC target("avx,avx2,fma")
#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")

#include <bits/stdc++.h>

using namespace std;
using ll = long long;
using pii = pair<int, int>;

int dist[222222];
int n, k;

void solve()
{
  cin >> n >> k;
  deque<int> dq;
  dq.push_back(n);
  fill(dist, dist + 222222, 1e9);
  dist[n] = 0;

  while (dq.size())
  {
    int cur = dq.front();
    dq.pop_front();

    if (cur == k)
    {
      cout << dist[k];
      return;
    }

    int warp = cur * 2;

    if (warp <= 200000 && dist[warp] > dist[cur])
    {
      dist[warp] = dist[cur];
      dq.push_front(warp);
    }

    int l = cur - 1, r = cur + 1;

    if (l >= 0 && dist[l] > dist[cur] + 1)
    {
      dq.push_back(l);
      dist[l] = dist[cur] + 1;
    }
    
    if (r <= 200000 && dist[r] > dist[cur] + 1)
    {
      dq.push_back(r);
      dist[r] = dist[cur] + 1;
    }
  }
}

int main()
{
  cin.tie(nullptr);
  ios::sync_with_stdio(false);

  solve();
}

 

0-1 BFS로 어려운 문제를 낸다고 하면 보통 성가신 구현을 하는 경우가 많다.

 

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

 

2423번: 전구를 켜라

첫째 줄에 문제의 정답을 출력한다. 전구에 불을 켜는 것이 가능하면, 몇 개의 칸을 돌려야 하는지를 출력하고, 불가능할때는 "NO SOLUTION"을 따옴표 없이 출력한다.

www.acmicpc.net

 

이런 거라던지 - 풀이

 

https://atcoder.jp/contests/abc213/tasks/abc213_e

 

E - Stronger Takahashi

AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online.

atcoder.jp

 

이런 문제같이 0-1 BFS의 사용이 핵심인 문제는 개념이 그렇게 어려운 것이 아니기 때문에 귀찮은 구현을 동반하게 된다. 그 코딩 테스트에서도 제법 성가시게 나왔었다.

 

하지만 적지 않은 연습 문제를 백준 온라인 저지에서 풀어볼 수 있으니 충분한 연습을 할 수 있을 것이다.

 

지금까지 0-1 BFS에 대해 알아보았다. 앞으로 가중치가 0과 1만 있는 최단 경로 문제를 보게 되면 바로 0-1 BFS로 참 교육을 해주자.

728x90
728x90