Brute Force / 순열 / 조합 / 멱집합

2023. 12. 17. 19:34
728x90

1. Brute Force 

: 무작정 가능한 모든 경우를 다 검사하는 알고리즘.

 

ex. 4자리의 숫자로 이뤄진 비밀번호를 구해야 한다.... > 0000~9999까지 모두 해본다...?

 

방법은 간단하다. 

모든 경우의 수를 확인해 보는 것...! 

그러나, 경우의 수가 늘어나기 시작하면 수행속도가 느려지기 시작한다...

그렇지만 해답을 못 찾은 가능성이 적으므로 절! 대! 모르겠으면 일단 접근해볼 수 있는 방법이다.

 

2. 순열

: 서로 다른 것들 중 몇 개를 뽑아서 한줄로 나열하는 것. 

서로 다른 n개 중 r개를 꺼낼 때 만들 수 있는 순열의 갯수는.......? (n개 중에서 r개 고르기)

 

- r = n인 경우 (n개 중에서 n개 고르기) > n 팩토리얼 (n!)

- nPr을 팩토리얼을 이용해 표현

~> n과 r의 크기가 커지면 연산 과정이 폭발적으로 증가하게 된다..

 

 

- 일단 간단하게 순열을 제작해보았다. (0부터 2까지의 숫자로 만들 수 있는 숫자나열을 전부 호출하는 메서드.)

더보기
public class Permutation {
    public static void main(String[] args) {
        permSimple(3);
    }

    // 0-2의 숫자로 만들 수 있는 숫자 나열 전부 출력하는 메서드
    public static void permSimple(int n) {
        int first;
        int second;
        int third;

        // 0부터 2사이의 숫자를 차례대로 골라본다.
        for(int i=0; i<n; i++) {
            // 첫번째 숫자를 골랐음.
            first = i;
            for (int j=0; j<n; j++) {
                // 이미 고른 숫자라면 나머지는 실행하지 않는다.
                if (j==first) continue;
                second = j;

                // 0에서 2사이의 숫자 중, 아직 고르지 않은 숫자를 골라본다.
                for (int k=0; k<n; k++) {
                    if (k==first || k==second) continue;
                    third = k;
                    System.out.println(first+" "+second+" "+third);
                }
            }
        }
    }
}

그런데, 여기서 숫자를 더 골라보고 싶다면....? 즉, nPr에서 r이 늘어난다면 

그만큼 반복문(for문)이 점점 더 늘어나게 된다. > 이 반복문의 갯수가 r에 따라 달라지기 때문에 for문을 동적으로 만들어주어야 한다.

 

>> 그러면 이 때 반복문을 대신하여 재귀함수를 사용한다면 훨씬 좋다!!

 

더보기
import java.util.Arrays;

public class Permutation {
    public static void main(String[] args) {
        permRecursive(5,3);
    }

    // 재귀함수로 더 많은 원소를 선택하는 순열을 만들어보자.
    public static void permRecurHelper(
            // 순열을 구할때 필요한거: 고르는 대상, 고르는 갯수 (nPr)
            // n개 원소 중에서 r개를 골라 만드는 순열 조합.
            int n, int r,
            // 내가 지금 몇번째 원소를 고르고 있는지 (for문 반복횟수)
            int depth,
            // 어떤 요소들을 사용했는지 저장하는 배열(즉 선택했다 안했다 표시)
            boolean[] used,
            // 결과를 저장하기 위한 배열 (조합결과) > 얘를 출력할 것이다.
            int[] result
    ) {
        // 재귀함수 탈출문 : r번 반복된다면? = depth가 r과 같아지면 중단.
        if (depth == r) {
            System.out.println(Arrays.toString(result));
        } // 그렇지않으면 계속 반복할 것이다.
        else {
            // n개의 원소중 하나를 선택하는 for
            for (int i = 0; i < n; i++) {
                // 이미 선택했다면 스킵
                if (used[i]) continue;

				// 선택 안했다면?
                // 선택을 할때 저장하는 것 결과 배열에 저장
                result[depth] = i;
                // 내가 이번에 i를 선택했다는걸 기록
                used[i] = true;
                // 중첩된 for 대신 재귀 호출
                this.permRecurHelper(n, r, depth + 1, used, result);
                // 이 i에서 출발하는 순열을 다 찾으면, 다음 i를 쓰기위해 기록 변경
                used[i] = false;
            }
        }
    }

    // 사용성을 위해 실제 메서드를 분리한다. (n과 r만 있어도 실행이 되게끔)
    public static void permRecursive(int n, int r) {
        permRecurHelper(n, r, 0, new boolean[n], new int[r]);
    }
}

 

- 혹은 "주어진 배열"에서 몇가지 원소를 골라 순열 생성하는 것도 가능하다.

private void permArray(
        // 순열을 만들 대상 요소들 배열 numbers
        int[] numbers, 
        // 고를 요소의 갯수
        int r,
        // 이번 재귀 호출이 몇번째 요소를 고르는 것인지
        int k,
        // 결과 저장용 배열
        int[] perm,
        // 사용한 요소 확인용 배열
        boolean[] used
) {
    // k == r 이면 순열 완성
    if (k == r) {
        // 결과 출력
        System.out.println(Arrays.toString(perm));
    }
    else {
        for (int i = 0; i < numbers.length; i++) {
            // i가 이미 사용되었으면 건너뛴다.
            if (used[i]) continue;
            // 이번에 선택한 결과를 저장한다.
            perm[k] = numbers[i];
            // 사용했다고 기록하고,
            used[i] = true;
            // k를 하나 증가해 재귀 호출한다.
            this.permArray(numbers, r, k + 1, perm, used);
            // 재귀 호출이 끝나면 다른 숫자를 고르기 위해 기록을 원복
            used[i] = false;
        }
    }
}

 

 

3. 조합

: 순열은 원소들 중 일부를 골라서 나열하는 것이면, 조합은 "순서 상관없이" 원소를 고르는 것! 

= 서로 다른 n개의 것들 중 r개를 순서 상관없이 고르는 경우이다. 

 

>> n==0이거나 n==r일 때, 순서를 고려하지 않기 때문에 모든 원소를 고르는 경우의 수는 단 한가지가 된다.

 

- 이번에도 간단한 조합을 제작해본다. (0~5까지의 숫자 중 3개의 숫자를 선택하는 경우)

더보기
int n=5;
int first;
int second;
ind third;

// i는 0부터 n-2까지 반복 (그래야 안쪽 반복문에서 선택할 요소가 남기때문에)
for(int i=0; i<n-2; i++) {
	first = i; 
    // j는 i+1부터 n-1까지 반복 
    for(int j=i+1; j<n-1; j++) {
		second = j;
        // k는 j+1부터 n까지 반복
        for (int k=j+1; k<n; k++) {
        	third = k; 
            System.out.println(first+" "+second+" "+third);
        }
    }
}

>> 이번에도 재귀함수의 형태로 조합을 만들 수 있다. . . (훨씬 효율적이겠죠...?)

더보기
public class Combination {
	public static void main(String[] args) {
    	combiRecursive(5,3);
    }

public static void combiRecurHelper(
	//몇개(n) 중에서 몇개(r)를 고를지
    int n, int r,
    //여태까지 몇개의 원소를 골랐는지
    int depth,
    // 이번에 고를지말지를 판단하는 숫자
    int next,
    //조합 결과를 담을 배열
    int[] comb
) {
	// 일단 재귀함수 탈출문 : r개를 골랐을 때.
    if(depth == r) {
    	System.out.println(Arrays.toString(comb));
    } 
    // next == n : 만약 내가 고르려고 고려할 애가 범위를 벗어나려고 한다면..
    else if (next == n) {
    	return; //반환하는 값 없이 원래 실행되었던 위치로 간다. 
    }
    else {
    	comb[depth] = next; //이번에 판단중인 숫자를 우선 할당한다.
        // depth번째인 원소를 골랐으니까 depth+1번째 원소를 고르러 간다.
        combiRecurHelper(n, r, depth+1, next+1, comb);
        // next를 선택하지 않고 depth번째 원소를 판단하러 넘어간다.
        // next를 선택하지 않으면 comb[depth]에 할당한 next는 덮어씌워진다. 
        combiRecurHelper(n, r, depth, next+1, comb);
    }
}

// 사용성을 위해 실제 메서드를 분리한다. (n과 r만 있어도 실행이 되게끔)
public static void combiRecursive(int n, int r) {
	combiRecurHelper(n, r, 0, 0, new int[r]);
}

>> 순열에서는 순서가 중요하지만, 얘는 next에 해당하는 값을 넣을지 말지만 판단하면 된다. 그래서 boolean[] used 배열을 굳이 사용할 필요가 없음.

- 마찬가지로 특정 배열에서 선택할 때는 이렇게 표현 가능하다.

private void combArray(
        int[] numbers, int r, int k, int next, int[] comb
) {
    // k == r: 고를 만큼 원소를 골랐다
    if (k == r) {
        System.out.println(Arrays.toString(comb));
    }
    // next == n: 고르기 전 고를 대상이 떨어졌다
    else if(next == numbers.length) {
        return;
    }
    else {
        // comb[k]에 이번에 판단중인 숫자를 우선 할당한다.
        comb[k] = numbers[next];
        // next를 선택하고, k + 1번째 원소를 판단하러 넘어간다.
        combArray(numbers, r, k + 1, next + 1, comb);
        // next를 선택하지 않고 k번째 원소를 판단하러 넘어간다.
        // next를 선택하지 않으면 comb[k]에 할당한 next는 덮어씌워진다.
        combArray(numbers, r, k, next + 1, comb);
    }
}

 


** 조합이 이해가 안가 추가 설명을 찾아보다가 넣었다. 

<조합 경우의 수 구하기>

더보기

>> 조합을 이렇게도 표현하는데, 왜 그렇게 표현하는지 이해가 안됐다.

살펴보자면, 

조합은 하나의 원소를 뽑는 경우와 뽑지 않는 경우의 합을 나타낸다. 

 

ex) 5C3이라고 하자.

5개 중에서 3개를 뽑는데,

- 만약 1을 무조건 포함하고 나머지 2개를 뽑는다고 생각하면 > 4개 중에 2개 뽑기 4C2가 된다.

- 만약 1을 포함하지 않곡 뽑는다면? > 1을 빼고, 4개 중에 3개 뽑기 4C3이 된다. 

>> 이 두 경우를 합치면 결국 5개중 3개를 뽑는 경우의 수가 된다. 그래서 저렇게 표현이 된 것!

 

그러면 이것을 코드로 구현해본다.  재귀호출에 대해 너무 이해가 안됐었다... 

재귀호출은 탈출문을 먼저 생각해본다! 

public class Combination {
	public static void main (String[] args) {
    	System.out.println(combination(5,3)); //5개 중 3개 뽑는 조합의 경우의 수
    }
    
    public static int combination(int n, int r) { 
    	if (n==r || r==0) {
        	return 1;
        }
        else {
        	return combination(n-1, r-1) + combination(n-1, r); 
        }
    }
}

 

요 재귀함수 코드가 어떻게 돌아가는지 이해가 안되어서 그려보았다. 하나하나 계속 재귀함수를 거쳐 지나가다보면 

n==r일 때 도는 r==0일 때가 끝이고, 이때 1을 리턴해서 하나하나 더해보면 결과 값이 나온다.

 


4. PowerSet (멱집합)

* 부분집합? : 어떤 원소의 모음인 집합에서 일부 또는 전부의 원소를 선택해 새로운 집합을 만드는 것. 

- 멱집합 ? : 어떤 집합에 대하여, 해당 집합이 가질 수 있는 모든 부분집합들의 집합. 

ex) 어떤 집합이 n개의 원소 가진다면 > 각각의 n에 대해서 부분집합에 포함할지 말지 결정 > 각 n개의 원소마다 2가지 경우의 수가 생김 > 총 2^n개의 부분집합이 생김!! 

 

1) 반복문 사용

더보기
// 몇번째 원소를 선택할지 여부를 0과 1로 구분
int[] target = new int[]{2,3,5};
int[] select = new int[]{0,0,0};

for(int i=0; i<2; i++) {
	//0일 경우 미선택, 1일 경우 선택
    select[0] = i;
    for(int j=0; j<2; j++) {
    	select[1] = j;
        for(int k=0; k<2; k++) {
        	select[2] = k; 
            // select의 3번째 원소가 결정되면 부분집합이 완성된다.
            for (int l = 0; l<3; l++) {
            	if(select[l] ==1) {
                	System.out.print(target[l]+" ");
                }
                System.out.println();
            }
        }
    }
}

>> 이 방법의 경우 순열/조합과 마찬가지로 대상 원소의 갯수가 많아질수록 for문도 많아지게 된다....

== 재귀함수 호출을 사용하자!! 

더보기
public class PowerSetRecursive {
	public static void main(String[] args) {
    	int[] target = new int[]{2,3,5}; 
        new PowerSetRecursive().powerSet(target, 0, new int[target.length]);
    }
    
    public void powerSet(
    	int[] target, //부분집합을 생성할 대상 배열
        int next,     // 다음에 선택할 배열 요소의 인덱스
        int[] select  // 각 요소가 부분집합에 속하는지 나타내는 배열 (0이면 미선택, 1이면 선택)
    ) {
    	if (next == target.length) {  //next가 배열의 길이와 같아지면
        	for(int i=0; i<target.length; i++) {
            	if (select[i] ==1) System.out.print(target[i]+" "); //1일경우 출력
            }
            System.out.println();
        }
        else {  // 그렇지 않으면 현재요소를 선택하지 않고 다음 요소로 이동한 후 
        	powerSet(target, next+1, select); //재귀함수 호출 
            select[next] = 1; // 현재요소를 선택하고 다음 요소로 이동한 후
            powerSet(target, next+1, select); // 재귀함수 호출
            select[next] = 0; // 다시 원상복귀
        }
   }
}

 

728x90

'Coding test > Algorithm' 카테고리의 다른 글

이항 계수  (0) 2023.12.29
DP(Dynamic Programming)  (0) 2023.12.28
Queue 큐 / LinkedList / BFS  (3) 2023.12.07
정렬 Sorting (버블/선택/카운팅/이진 탐색)  (2) 2023.12.05
알고리즘  (3) 2023.12.04

BELATED ARTICLES

more