DFS로 사이클 탐지하는 알고리즘 탐구하기2 -사이클의 길이-

1. 연습문제1

 

9466번: 텀 프로젝트 (acmicpc.net)

 

문제를 보면 u가 A[u]를 향하는 그래프이고 이 그래프에서 사이클을 모두 찾아 각 사이클에 속하는 노드의 개수를 구하고

 

전체 노드 수에서 사이클에 속하는 노드의 개수를 빼면 된다

 

dfs로 사이클을 탐지할 수는 있는데... 사이클에 속하는 노드의 개수는 어떻게 알 수 있을까?

 

노드 u1부터 u2,u3,...,un을 차례대로 방문한다고 생각해보자

 

u1 > u2 > u3 > .... > un

 

그러다가 다시 un에서 u1으로 온다면?

 

u1 > u2 > u3 > ... > un > u1으로 오면 (u1,u2,.u3,...,un)이 하나의 사이클이 된다.

 

따라서 dfs로 노드를 방문할때마다, 방문한 노드의 번호를 1,2,3,.. 순서대로 매겨준다.

 

현재 u에서 A[u]로 이동했더니, 사이클이 탐지되었다면...

 

u의 번호와 A[u]의 번호 차이에 1을 더하면 사이클의 크기가 된다.

 

 

마지막에 도달한 A[u]의 숫자가 더 크니까, A[u]의 번호에 u의 번호의 차이 + 1을 해야하는거 아닌가?

 

라고 생각했는데

 

예를 들어 다음과 같은 그래프를 보자

 

etc-image-0

 

 

 

1번부터 시작해서 7번까지 순회하여 dfs를 수행하여 사이클을 찾을 건데

 

1번에는 1을 표시하고 다음 3번으로 간다

 

etc-image-1

 

 

 

그러면 3번에서는 2로 표시하고 다음 3번으로 이동

 

그러면 3번으로 오면서 사이클이 탐지될거고, 3번에 표시된 숫자 2와 3번에 표시된 숫자 2의 차이 + 1 = 1이 사이클의 길이

 

그리고 dfs가 끝나면서 아직 방문하지 않은 2번을 방문할것

 

2번에는 1을 표시하고 1번으로 이동하는데, 이미 방문이 끝났으니까 종료

 

etc-image-2

 

 

이제 4번을 방문할 차례인데, 4번에는 1을 표시

 

다음 7번에는 2를 표시, 6번에는 3을 표시하게 된다.

 

etc-image-3

 

 

 

그러면 6번 노드에서 4번 노드로 이동하는데, 사이클이 탐지되었다.

 

u = 6이고 A[u] = 4이며, u = 6에 표시된 숫자는 3이고 A[u] = 4에 표시된 숫자는 1이다.

 

따라서 이 사이클의 길이는 3 - 1 + 1 = 3이 된다

 

from sys import stdin,setrecursionlimit
setrecursionlimit(10**5)

def dfs(u,order):
    
    global count

    if visited[u]:
        
        if visited[u] == -1:

            return True
        
        return False
    
    visited[u] = -1
    
    #u에 order을 표시하고,
    ordering[u] = order
    
    #다음 A[u]-1로 이동(zero index)
    if dfs(A[u]-1,order+1):
        
        #사이클이 탐지되었다면, u~A[u]까지 사이클이고
        #u와 A[u]에 표시된 번호 차이에 1을 더한 것이 사이클의 길이
        count += (ordering[u] - ordering[A[u]-1]+1)

    visited[u] = 1
    
T = int(stdin.readline())

for _ in range(T):
    
    n = int(stdin.readline())

    A = list(map(int,stdin.readline().split()))

    visited = [0]*(n+1)
    ordering = [0]*(n+1)

    count = 0

    for i in range(n):

        if visited[i] == 0:

            dfs(i,1)
    
    print(n - count)

 

 

 

2. 실제 사이클을 찾는 방법

 

사실 코드만 보면... 직관적이지 않다

 

사이클이 뭔지 다시 생각해보면 

 

노드 u1부터 u2,u3,...,un을 차례대로 방문한다고 생각해보자

 

u1 > u2 > u3 > .... > un

 

그러다가 다시 un에서 u1으로 온다면?

 

u1 > u2 > u3 > ... > un > u1으로 오면 (u1,u2,.u3,...,un)이 하나의 사이클이 된다.

 

u1에서 dfs를 시작한다고 할때,

 

u1 > u2 > u3 > u4 > u5 > ... > un까지 잘 왔는데, un > u4로 이동한다고 해보자.

 

그러면 u4 > u5 > ... > un > u4가 하나의 사이클을 형성한다.

 

 

따라서 u1에서 dfs를 수행하면서, 이동한 노드 번호를 리스트에 저장한 다음...

 

u에 왔는데 사이클이 탐지된 순간 해당 노드 u가 리스트의 어디 인덱스에 있는지 찾아내고...

 

그 인덱스부터 리스트의 끝까지가 하나의 사이클이 된다

 

 

def dfs(u,path):
    
    global count

    if visited[u]:
        
        if visited[u] == -1:
            
            #u로 왔는데, 사이클이 탐지되었다
            
            #그동안 저장된 path는 도달한 노드를 순서대로 
            #u > u1 > u2 > .... > un이 저장되어 있다
            
            #path를 차례대로 순회해서 u의 위치를 알아낸다.
            
            #[u1,u2,u3,...,un]에서 j번이 u라면...
         
            #path[j:]가 사이클이다
            
            for j in range(len(path)):
                
                if path[j] == u:

                    break

            count += (len(path) - j)

            return True
        
        return False
    
    visited[u] = -1
    
    #도달한 노드 u를 path에 저장
    path.append(u)
    
    #다음 노드 A[u]로 이동
    dfs(A[u]-1,path)
    
    visited[u] = 1

 

 

etc-image-4

 

 

 

여기서 보면 1번부터 시작해서, path = [1,3] 저장되어있는데 3에 온 순간 사이클이 탐지되어서 

 

path에서 3을 찾으면 1번이니까, path[1:] = [3]이 하나의 사이클이다

 

다시 4번부터 시작하면 [4,7,6]을 방문하다가 4번을 방문할려하면 사이클이 탐지되는데

 

4번은 path에 0번에 있으니까 path[0:] = [4,7,6]이 하나의 사이클이다

 

실제로 속도도 그렇게 차이나지 않고 로직이나 코드가 조금 더 알아보기 쉽다고 해야하나?

 

이것보다 더 빠른 방법을 알게된다면..

 

나중에 기록할 기회가 있겠지

728x90