이 문제를 정의하기에 재밌는 영상이 있어서 미리 보면 좋다.

토끼와 거북이 알고리즘(플로이드 알고리즘)Permalink

어떤 단방향 그래프 GG의 모든 노드가 단 하나의 간선을 갖고 연결된 그래프라고 하자.

그러면 무조건 ρ\rho 모양의 그래프가 생긴다.

ρ\rho 모양이 아닐 수도 있고, 대략 다음과 같을 수도 있다.

image.png

어쨌든 중요한건, 하나의 싸이클이 존재한다는 점이다!

이 싸이클에 들어있는 노드가 무엇인지, 싸이클의 길이가 얼마나 되는지 O(N)O(N) 만에 찾을 수 있는 알고리즘이 바로 플로이드 알고리즘이다.

왜 토끼와 거북이 알고리즘이라고도 불리는 지 잠시 후 알아볼 수 있다.

꼭 싸이클이 있는 곳에서만 쓸 수 있는 알고리즘은 아니다. 오히려 싸이클의 유무를 판별하는 데도 쓰인다.

쓰이는 곳Permalink

저런 그래프의 싸이클을 찾는 것은 직접 DFS를 돌려 역방향 간선을 찾는 것으로도 가능하다.

따라서 사실 이 플로이드 알고리즘은 직접 그래프 탐색을 하는것으로 대체 가능한 알고리즘이다.

하지만 왜 의미가 있냐면, 위 영상에서 보듯이

  • O(N)O(N)에 해결해야 하고
  • 배열을 변경할 수 없고
  • O(1)O(1) 의 메모리에 해결해야 하는 (추가적인 자료구조 사용 불가)

조건들이 모두 충족될 수 있는 알고리즘이기 때문이다.

중복된 수 찾기Permalink

또한 이 알고리즘은 위와 같은 상황에 있는 그래프에서 중복된 두 수를 찾는데도 사용할 수 있다.

0N10 \sim N-1의 수들로 이루어진 N+1N+1 개의 수들 중, 어떤 한 수가 두 번 쓰였다고 할 때, 그 수를 찾아라

같은 문제를 해결할 수 있는데, 어떻게 동작하는지는 아래서 살펴보자.

알고리즘을 사용하는 법Permalink

원리를 살펴보기전에 그냥 먼저 어떻게 구현하는지를 알아보자.

일단 아무 시작점 ss를 정한 다음에 t,ht, h 두 변수를 가지고 있다고 하자.

tt는 한 번에 f(t)f(t) 로 이동하고 hhf(f(h))f(f(h)) 로 이동한다.

눈치챘겠지만 tt는 tortoise로 거북이고, hh는 hare로 토끼여서 각각 한 칸과 두 칸씩 이동한다.

이 동작을 반복하다보면 어느 순간 t=ht=h 인 시점이 오는데,

그 시점에 tst \coloneqq s로 해주고 이제 t,ht, h 모두 한 칸씩만 이동하게 해주면 만나는 지점이 싸이클의 시작점이다.

이 시작점은 위에서 말한 문제의 중복된 수 이기도 하다.

원리Permalink

다음과 같은 그래프를 보자.

image.png

  • ss : 시작 위치
  • uu : t=ht=h 가 되는 지점
  • ee : 싸이클의 시작
  • KtK_t : 거북이가 싸이클을 돈 횟수
  • KhK_h :토끼가 싸이클을 돈 횟수

처음에 t,ht,h는 모두 ss에 존재한다.

이제 tf(t)t \coloneqq f(t)hf(f(h))h\coloneqq f(f(h)) 를 반복하여 t=h(=u)t=h (=u)가 되는 시점을 생각해보자.

여기서, sus \to u 라는 경로는 세 구간으로 나누어 볼 수 있다.

  1. ses \to e 구간 AA
  2. eue \to u 구간 BB
  3. ueu \to e 구간 CC

B+C=싸이클의 길이B+C=\text{싸이클의 길이}

image.png

이때까지 tt가 이동한 거리를 TT, hh가 이동한 거리를 HH라고 하면,

T=A+B+(B+C)Kt  2T=H=A+B+(B+C)KhT=(B+C)(KhKt)      (1) \begin{aligned} T=A+B+(B+C)K_t\\ -~~\underline{2T=H=A+B+(B+C)K_h}\\ T=(B+C)(K_h-K_t) &~~~~~~(1) \end{aligned}

(1)(1)에서의 결론으로 다음과 같이 A=CA=C 의 유도가 가능하다.

T=A+B+(B+C)Kt             (B+C)  TT=(B+C)(KhKt)             (B+C)  A+B+(B+C)Kt             (B+C)  A+BA=C \begin{aligned} &T=A+B+(B+C) K_t\\ &~~~~~~~~~~~~~\Downarrow\\ &(B+C) ~\vert~ T&\because T=(B+C)(K_h-K_t)\\ &~~~~~~~~~~~~~\Downarrow\\ &(B+C) ~\vert~ A+B+(B+C)K_t\\ &~~~~~~~~~~~~~\Downarrow\\ &(B+C) ~\vert~ A+B \\\\ &\therefore A=C \end{aligned}

즉, 결론적으로 ssuu에서 각각 한 칸씩 동일하게 이동하는 것으로 ee에 같은 시점에 도달할 수 있다는 것이다.

코드Permalink

image.png

그래프를 구현한 것이다.

vi f(6);  
  
f[0] = 1;  
f[1] = 2;  
f[2] = 3;  
f[3] = 4;  
f[4] = 5;  
f[5] = 1;  
  
int t = 0, h = 0;  
do {  
   t = f[t], h = f[f[h]];  
} while (t != h);  
  
t = 0;  
while (t != h) t = f[t], h = f[h];  
  
cout << t; // 1

시간 복잡도Permalink

공간 복잡도는 다른 자료구조를 사용하지 않으므로 O(1)O(1) 인건 알겠는데, 왜 시간복잡도가 O(N)O(N) 일까?

일단 tthh둘다 싸이클에 들어왔다고 생각해보자. tt는 그 시점에서 싸이클을 한 바퀴 돌기전에 무조건 hh와 만나게 된다.

그러므로 노드의 수에 비례해 O(N)O(N)이란 시간복잡도가 나온다.

1부터 N까지 수들 중 중복된 수 찾기Permalink

위에 구현한 것은 그래프였고, 이제 앞에서 언급했던 이 문제를 풀어보자.

0N10 \sim N-1의 수들로 이루어진 N+1N+1 개의 수들 중, 어떤 한 수가 두 번 쓰였다고 할 때, 그 수를 찾아라

image.png

여기서, 이걸 배열로 나타내보자.

image.png

이제 f(0)=1,f(1)=5,f(0)=1,\,f(1)=5,\, \dots 와 같이 표현을 할 수 있는데, 이걸 그대로 알고리즘으로 적용하면 된다.

vi f(8);  
  
f[0] = 1;  
f[1] = 5;  
f[2] = 2;  
f[3] = 0;  
f[4] = 3;  
f[5] = 2;  
f[6] = 4;  
f[7] = 6;  
  
int t = 0, h = 0;  
do {  
   t = f[t], h = f[f[h]];  
} while (t != h);  
  
t = 0;  
while (t != h) t = f[t], h = f[h];  
  
cout << t; // 2

정답인 22가 잘 나왔다.

ReferencesPermalink

Comments