순열


순열

  • 하나의 집합을 다른 순서로 뒤섞는 것
  • n개의 집합에서 순열의 총 개수는 n!
    ex) n = 5, 5x4x3x2x1 = 순열의 총 개수
  • nPr 수식 (P = Permutation 약자)
    3P2 = 3! / (3-2)!
            = 6 / 1
            = 6   (1, 2) / (1, 3) / (2, 1) / (2, 3) / (3, 1) / (3, 2) 총 6개
  • 재귀로 구현 가능
    자주 사용되는 swap, visited 방법으로 구현
  • 완전탐색 문제에서도 자주 나올 수 있음

 

swap을 이용한 순열

  • 배열의 값을 직접 교환해서 구함
  • Depth를 이용한 풀이
  • Depth와 r의 값이 같으면 모두 다 뽑았다는 것이므로 결과값 출력
  • 순열 함수가 종료됐을 때 배열의 값을 다시 원상복구 해주어야 함
    (그 다음 수와 다시 순열을 계산해야 하므로)

구현
주석으로 설명했으나, 제일 빠른건 직접 디버깅하면서 확인해보면 된다.

public static void swapPermutation(int[] arr, int depth, int n, int r) {
    // depth와 r이 같으면 다 뽑았다는 것이므로
    if (depth == r){
        for(int i=0; i<r; i++){
            System.out.print(arr[i] + " ");
        }
        System.out.println();
        return;
    }
    else {
        /*
            1, 2, 3 세 개의 숫자를 순열로 구한다고 했을 때,
            재귀의 특성 상
            depth가 0일 경우 1XX, 2XX, 3XX을 구해야 함
            depth가 1일 경우 12X, 13X 를 구해야 함
            depth가 2일 경우 123, 132 를 구해야 함
            depth == r에서 return 되면서 배열을 원상복구 시킨 후
            depth가 0일 경우 2XX, 3XX 를 구하는 과정을 반복
         */
        for(int i=depth; i<n; i++){
            swap(arr, i, depth);
            swapPermutation(arr, depth + 1, n, r);
            swap(arr, i, depth);
        }
    }
}

public static void swap(int[] arr, int i, int j){
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

 

visited을 이용한 순열

  • 방문했는지 확인할 수 있는 visited 배열과, 결과를 저장할 수 있는 result 배열이 필요함
  • Depth를 이용한 풀이
  • Depth와 r의 값이 같으면 모두 다 뽑았다는 것이므로 결과값 출력
  • visited를 통해 result에 데이터 저장 시 중복값이 들어가지 않도록 함

구현

public static void visitedPermutation(int[] arr, boolean[] visited, int[] result, int depth, int n, int r) {
    if (depth == r){
        for(int i : result){
            System.out.print(i + " ");
        }
        System.out.println();
        return;
    }
    else {
        for(int i=0; i<n; i++){
            if (!visited[i]){
                visited[i] = true;
                result[depth] = arr[i];
                visitedPermutation(arr, visited, result, depth + 1, n, r);
                visited[i] = false;
            }
        }
    }
}

 

결과비교

결과를 살펴보면 아래와 같다.

swap을 이용한 순열과 visited를 이용한 순열의 데이터를 비교해보면, 

swap에서는 3, 2, 1 / 3, 1, 2 순으로 출력이 되는데 visited에서는 3, 1, 2 / 3, 2, 1 순으로 출력이 된다.

따라서 swap을 이용해서 순열을 구했을 때는 순서대로 순열이 구해지지 않는다는 차이점을 발견할 수 있다. 

[Level 2] 단체사진 찍기 (Java)


프로그래머스의 Level2 문제인 단체사진 찍기를 풀었다. 나같은 경우 해당 문제를 아래와 같이 풀었었다.

  1. 알파벳 8자리에서 나올 수 있는 모든 경우의 수를 모두 구한다. (8! = 40320)
  2. 구한 경우의 수들을 조건에 맞는지 확인 후 조건에 만족할 경우 +1 해준다.
import java.util.*;

public class 단체사진_찍기 {
    public static void main(String[] args) {
        int n = 0;
        // String[] data = {
        //     "N~F=0", 
        //     "R~T>2"
        // };

        String[] data = {
            "M~C<2", 
            "C~M>1"
        };

        System.out.println(solution(n, data));
    }

    
    static char[] friendsAlphabet = new char[] { 'A', 'C', 'F', 'J', 'M', 'N', 'R', 'T' };
    public static int solution(int n, String[] data){
        int answer = 0;
        createAllFriendsRow(0);
        for (String cs : createFriendsRowList) {
            if (checkCondition(cs, data)){
                answer++;
            }
        }
        return answer;
    }

    static boolean[] visited = new boolean[8];
    static List<String> createFriendsRowList = new ArrayList<String>();
    static char[] createFriendsRowArray = new char[8];
    public static void createAllFriendsRow(int index){
        if (index == visited.length){
            createFriendsRowList.add(String.valueOf(createFriendsRowArray));
        }
        else{
            for(int i=0; i<visited.length; i++){
                if (!visited[i]){
                    visited[i] = true;
                    createFriendsRowArray[index] = friendsAlphabet[i];
                    createAllFriendsRow(index + 1);
                    visited[i] = false;
                }
            }
        }
    }

    public static boolean checkCondition(String friendsArray, String[] data){
        String[] splits;
        String startFriend, endFriend;
        String condition;
        int conditionCount = 0;
        int gap;
        int indexGap = 0;
        int endIndex, startIndex;

        for (String c : data) {
            splits = c.split("");

            startFriend = splits[0];
            endFriend = splits[2];
            condition = splits[3];
            gap = Integer.parseInt(splits[4]);

            startIndex = friendsArray.indexOf(startFriend);
            endIndex = friendsArray.indexOf(endFriend);
            indexGap = Math.abs(endIndex - startIndex) - 1;

            switch (condition) {
                case "=":
                    if (indexGap != gap){
                        return false;
                    }
                    conditionCount++;
                    break;
                
                case ">":
                    if (indexGap <= gap){
                        return false;
                    }
                    conditionCount++;
                    break;

                case "<":
                    if (indexGap >= gap){
                        return false;
                    }
                    conditionCount++;
                    break;
            }
        }

        if (conditionCount == data.length){
            return true;
        }
        else{
            return false;
        }
    }
}

그 결과...

8.6초라는 어마어마한 성능을 가지고 있었다;

8! 만큼 2번 (프렌즈의 모든 경우의 수 구할 때, 조건에 맞는지 구할 때)을 돌아서 성능이 저렇게 나온건지 해서 아래와 같이 수정했었다.

  1. 모든 경우의 수를 구함과 동시에 조건에 맞는지 확인한다.
  2. 조건에 맞다면 +1 해준다.

위와 같이 작성해봐도 동일하게 8초 이상이 출력되었다. 그래서 다시 확인한 결과 checkCondition 함수에서 split 메소드로 문자열을 자르는 부분이 있었는데, 이 부분을 split 대신 charAt 메소드를 사용하여 구현해 보았다.

class Solution {
    char[] friendsAlphabet = new char[] { 'A', 'C', 'F', 'J', 'M', 'N', 'R', 'T' };
    public int solution(int n, String[] data){
        int answer = 0;
        createAllFriendsRow(0, data);
        answer = correctCount;
        return answer;
    }

    boolean[] visited = new boolean[8];
    char[] createFriendsRowArray = new char[8];
    int correctCount = 0;
    public void createAllFriendsRow(int index, String[] data){
        if (index == visited.length){
            if (checkCondition(String.valueOf(createFriendsRowArray), data)){
                correctCount++;
            }
        }
        else{
            for(int i=0; i<visited.length; i++){
                if (!visited[i]){
                    visited[i] = true;
                    createFriendsRowArray[index] = friendsAlphabet[i];
                    createAllFriendsRow(index + 1, data);
                    visited[i] = false;
                }
            }
        }
    }

    public boolean checkCondition(String friendsArray, String[] data){
        char startFriend, endFriend;
        char condition;
        int conditionCount = 0;
        int gap;
        int indexGap = 0;
        int endIndex, startIndex;

        for (String c : data) {
            startFriend = c.charAt(0);
            endFriend = c.charAt(2);
            condition = c.charAt(3);
            gap = c.charAt(4) - '0';

            startIndex = friendsArray.indexOf(startFriend);
            endIndex = friendsArray.indexOf(endFriend);
            indexGap = Math.abs(endIndex - startIndex) - 1;

            switch (condition) {
                case '=':
                    if (indexGap != gap){
                        return false;
                    }
                    conditionCount++;
                    break;
                
                case '>':
                    if (indexGap <= gap){
                        return false;
                    }
                    conditionCount++;
                    break;

                case '<':
                    if (indexGap >= gap){
                        return false;
                    }
                    conditionCount++;
                    break;
            }
        }

        if (conditionCount == data.length){
            return true;
        }
        else{
            return false;
        }
    }
}

위와 같이 작성 한 후 다시 제출했더니 아래와 같은 결과가 나왔다.

 

속도와 메모리가 9배 이상 차이가 남을 확인할 수 있었다. 

 

결론.

  - split 대신 charAt을 사용할 수 있는 경우 charAt을 사용하는 것이 성능상에 유리하다. 

+ Recent posts