본문 바로가기

Dot Algo∙ DS/PS

[BOJ] 백준 10451번 순열 사이클 (Java)

    #10451 순열 사이클

    난이도 : 실버 1

    유형 : 그래프 탐색(DFS)/ 순열 사이클 또는 Union-find

     

    10451번: 순열 사이클

    1부터 N까지 정수 N개로 이루어진 순열을 나타내는 방법은 여러 가지가 있다. 예를 들어, 8개의 수로 이루어진 순열 (3, 2, 7, 8, 1, 4, 5, 6)을 배열을 이용해 표현하면 \(\begin{pmatrix} 1 & 2 &3&4&5&6&7&8 \\  3

    www.acmicpc.net

    ▸ 문제

    1부터 N까지 정수 N개로 이루어진 순열을 나타내는 방법은 여러 가지가 있다. 예를 들어, 8개의 수로 이루어진 순열 (3, 2, 7, 8, 1, 4, 5, 6)을 배열

     

    을 이용해 표현하면 (1234567832781456) 와 같다. 또는, Figure 1과 같이 방향 그래프로 나타낼 수도 있다.

    순열을 배열을 이용해 (1…i…nπ1…πi…πn) 로 나타냈다면, i에서 πi로 간선을 이어 그래프로 만들 수 있다.

    Figure 1에 나와있는 것 처럼, 순열 그래프 (3, 2, 7, 8, 1, 4, 5, 6) 에는 총 3개의 사이클이 있다. 이러한 사이클을 "순열 사이클" 이라고 한다.

    N개의 정수로 이루어진 순열이 주어졌을 때, 순열 사이클의 개수를 구하는 프로그램을 작성하시오.

     입력

    첫째 줄에 테스트 케이스의 개수 T가 주어진다. 각 테스트 케이스의 첫째 줄에는 순열의 크기 N (2 ≤ N ≤ 1,000)이 주어진다. 둘째 줄에는 순열이 주어지며, 각 정수는 공백으로 구분되어 있다.

     출력

    각 테스트 케이스마다, 입력으로 주어진 순열에 존재하는 순열 사이클의 개수를 출력한다.

     

     

    문제 풀이 

    처음에 싸이클 찾으래서 잘 안 사용하는 서로소집합 알고리즘 Union-find을 연습할겸 사용했다.  그런데 이상하게 예제는 다 풀리는데 9%에서 막혀서 풀이를 바꿨다. DFS탐색으로는 가볍게 풀리는 문제이다.

     

    그리고 아래에 Union-find의 풀이도 추가하였다. (해결법 찾음!)

     

    조건 

    1) 순열 크기 N  : 2<= N <=1,000

    2) 1부터 N까지 정수 N개로 이루어진 순열

     

    순열 싸이클의 개수를 구하는 문제이므로 당연히 인접탐색 BFS보다는 내가 좋아하는 깊이탐색 DFS를 사용했다.

    *유의할 점은 방문여부를 전역으로 잘 체크해줘야한다.

     

    풀이 코드  

    import java.io.*;
    import java.util.*;
    
    public class Main {
    	static int[] map;
    	static boolean[] check;
    	static int cycle;
        
    	public static void main(String[] args) throws IOException{
    		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    		StringTokenizer st = null;
            
    		int t = Integer.parseInt(br.readLine());
    		for(int test=0; test<t; test++) {
    			int n = Integer.parseInt(br.readLine());
    			map = new int[n+1];
    			cycle=0;
    			
    			st = new StringTokenizer(br.readLine());
    			for(int i=1; i<n+1; i++) {
    				map[i] = Integer.parseInt(st.nextToken());
    			}
    			
    			check = new boolean[n+1];
    			for(int i=1; i<n+1; i++) {
    				if(!check[i]) {
    					dfs(i);
    					cycle++;
    				}
    			}
    			System.out.println(cycle);
    		}
    	}
    	
    	static void dfs(int start) {
    		check[start]= true;
    		
    		int next= map[start];
    		if(!check[next]) {
    			dfs(map[start]);
    		}
    		
    	}
    }
    

     

     

    Union-find 풀이 

    처음에 안풀린 풀이과정 더보기 클릭

    더보기

    처음에는 union-set배열에서 ranks를 추가하여 풀이를 해보았다. 

     

     ∙  parents 배열 : i의 부모노드를 저장

     ∙  ranks 배열 : i를 부모로 삼는 자식노드의 수를 저장

     

    예시

    1
    1
    3 4 2 1

     

    만약 위의 예제를 실행한다면 풀이는 아래와 같이 진행됩니다.

     

    node Parents (x,y) 1 2 3 4 Ranks (x,y) 1 2 3 4
    1 -> 3 x = 1, y = 3 1 2 1 4 ranks[1] = 0
    ranks[3] = 0
    1 0 0 0
    2 -> 4 x = 2, y = 4 1 2 1 2 ranks[2] = 0
    ranks[4] = 0
    1 1 0 0
    3 -> 2 x = 1, y = 2 1 1 1 2 ranks[1] = 1
    ranks[2] = 1
    3 0 0 0
    4 -> 1 x = 1, y = 1 1 1 1 2 ranks[1] = 2
    ranks[1] = 2
    4 0 0 0

     

    * 3->2 일 때는 자식이 1이상인 부모노드가 만나 두 개의 싸이클에서 하나로 합쳐져 ranks[2]를 감소시키고 ranks[1]을 증가시켰습니다.

     

    실패한 풀이 ✔︎ 

    import java.io.*;
    import java.util.*;
    
    public class Main {
    
    	static int[] parents;
    	static int[] ranks;
    	static int cycle;
        
    	public static void main(String[] args) throws IOException{
    		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    		StringTokenizer st = null;
    		StringBuilder sb = new StringBuilder();
            
    		int t = Integer.parseInt(br.readLine());
    		for(int test=0; test<t; test++) {
    			int n = Integer.parseInt(br.readLine());
                
                // --------------초기화--------------------
    			parents = new int[n+1];
    			ranks = new int[n+1];
    			cycle=0;
    			for(int i=1; i<n+1; i++) {
    				parents[i] = i;
    			}
    			
    			st = new StringTokenizer(br.readLine());
    			
                // -------------- union-set --------------
    			for(int i=1; i<n+1; i++) {
    				int a = i;
    				int b = Integer.parseInt(st.nextToken());
    				union(a,b);
    			}
                
                // -------------- 싸이클 찾기 --------------
    			for(int k=1; k<n+1; k++) {
    				if(ranks[k]!=0) {
    					cycle++;
    				}
    			}
    			sb.append(cycle+"\n");
    		}
    		System.out.println(sb.toString());
    	}
    	
    	static int find(int x) {
    		if(parents[x] == x) return x;
    		
    		int parent= find(parents[x]);
    		return parent;
    		
    	}
    	
    	static void union(int x, int y) {
    		x = find(x);
    		y = find(y);
            
    		// y가 부모 
    		if(ranks[x] < ranks[y]) {   
    			parents[x]= y;
    			ranks[y]++; // 자식 추가 
    			
    		}else {
    			parents[y] = x;  
    			
    			if(ranks[x]==ranks[y]) {
    				ranks[x]++; // 자식 추가 
    				
    				// 길이가 1이상인 두 싸이클이 하나로 합쳐질 때  (x부모, y자식)
    				if(ranks[y]!=0 && x!=y) { 
    					ranks[x]++; 
    					ranks[y]--;  
    				}
    			}
    		}
    	}
    }

     

    위의 시행착오를 겪고 다시 단순히 생각해보았다. ranks의 배열은 싸이클을 판별하기에는 문제가 있는 것은 아닐까? 만약 수열의 한 조합이 꼬리가 있지만 싸이클이 되지 않는 경우(1->2->3)에는 ranks의 값은 2이지만 싸이클로 판별될 것이다. 이렇게 반례를 찾았다. 그렇다면 싸이클이 생성될 때만 카운트하면 되는데 이는 사실 Union-find만으로 쉽게 알아낼 수 있었다. 위상정렬 없이 그냥 Union-find로만 해결했다. union로직에서 x와 y가 일치하는 순간이 싸이클이 발생하는 순간이다.

     

    예제로 과정을 보면 다음과 같다.

     주어진 순열 그래프 (3 2 7 8 1 4 5 6)

     

    i) 1 → 3 , x=1 y=3

                  ☛ parents[3] =1

     

    ii) 3 → 7 , x= find(3)= 1,  y=7

                  ☛ parents[7] =1

     

    iii) 7 → 5 , x= find(7) = 1, y=5

                  ☛ parents[5] =1

     

    iv) 5 → 1 , x= find(5) = 1, y=1

                        x==y, 싸이클 생성!

     

     

    위와 같은 방식으로 싸이클을 count하여 출력해주면 된다.

    import java.io.*;
    import java.util.*;
    
    public class Main {
    
    	static int[] parents;
    	static int cycle;
    	public static void main(String[] args) throws IOException{
    		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    		StringTokenizer st = null;
    		StringBuilder sb = new StringBuilder();
    		int t = Integer.parseInt(br.readLine());
    		for(int test=0; test<t; test++) {
    			int n = Integer.parseInt(br.readLine());
    			parents = new int[n+1];
    			cycle=0;
    			for(int i=1; i<n+1; i++) {
    				parents[i] = i;
    			}
    			
    			st = new StringTokenizer(br.readLine());
    			for(int i=1; i<n+1; i++) {
    				int a = i;
    				int b = Integer.parseInt(st.nextToken());
    				union(a,b);
    			}
    			sb.append(cycle+"\n");
    		}
    		System.out.println(sb.toString());
    	}
    	
    	
    	static int find(int x) {
    		if(parents[x] == x) return x;
    		
    		int parent= find(parents[x]);
    		return parent;
    		
    	}
    	
    	static void union(int x, int y) {
    		x = find(x);
    		y = find(y);
    
    		if(x==y) {
    			cycle++; // 싸이클 생성
    		}
    		else if(x>y) {   
    			parents[x]= y;
    			
    		}else {
    			parents[y] = x;  
    			
    		}
    	}
    }