순열, 조합, 부분집합

Bennie97·2022년 2월 8일
0

순열

서로 다른 원소들 중 중복없이 순서에 상관있게 몇개를 뽑아서 나열하는것을
순열이라고 한다.

n개중 r개를 중복없이 순서 상관있게 뽑아서 나열하는 가짓수
nPr = n×(n−1)×(n−2)×⋯⋯×(n−r+1) = n!/(n-r)!

A, B, C 세개의 문자를 중복없이 순서에 상관있게 두개만 나열하면 아래 그림과 같다.

3P2 = 3 * 2 = 6

두 자리가 있는데 첫번째 자리에는 A, B, C세가지가 올수 있고
두번째 자리에는 중복허용을 하지 않으니 첫번째 자리의 문자를 뺀 나머지 두가지가 올수 있다.
따라서 3*2 = 6 이다.


[A, B], [A, C], [B, A], [B, C], [C, A], [C, B]

이걸 이제 자바코드로 짜보자

import java.util.Arrays;
import java.util.Scanner;

public class PermutationTest {

	public static int N, R; 	// nPr 즉 n개의 원소중 r개를 중복 제거, 순서있게 나열하고 싶다.
	public static char[] element, selected; // element는 n개의 원소를 담을 배열, selected는 n개의 원소중 r개를 선택한것을 담을 배열
	public static boolean[] isSelected;	// element의 각원소가 선택이 되었는지 알아보는 배열 선택이 되었으면 true 아니면 false
	
	public static int totalcnt = 0; // 순열의 개수 3P2 = 6에서 6에 해당하는 값
	
	public static void main(String[] args) {

		Scanner sc = new Scanner(System.in);
		System.out.print("nPr에서 n의 개수와 r의 개수가 될것을 입력해주세요(한칸씩 띄어써서 다 썼으면 엔터 눌러주세요) : ");
		N = sc.nextInt(); 
		R = sc.nextInt();
		
		element = new char[N];	
		selected = new char[R];
		isSelected = new boolean[N];
		
		System.out.print("nPr에서 n개의 원소를 입력해주세요(한칸씩 띄어써서 다 썼으면 엔터 눌러주세요) : ");
		for (int i = 0; i < N; i++) {
			element[i] = sc.next().charAt(0);
		}
		
		Permutation(0);
		System.out.println("순열의 총 가짓수 : " + totalcnt);
		
	}
	
	public static void Permutation(int cnt) {	
		
		if(cnt == R) {
			System.out.println(Arrays.toString(selected));
			totalcnt++;
			return;
		}
		
		for (int i = 0; i < N; i++) {
			
			if(isSelected[i] == true) continue;
			selected[cnt] = element[i];
			isSelected[i] = true;
			Permutation(cnt + 1);
			isSelected[i] = false;
		}
	}
}

element배열은 A, B, C를 가지고 있는 char 배열이고
isSelected배열은 element배열과 길이가 똑같은 boolean 배열인데
그 이유는 isSelected 배열의 각 원소가 true냐 false냐에 따라서
element배열의 원소가 선택되었느냐 아니냐가 나타나져있기 때문이다.
당연히 처음엔 A, B, C 아무것도 선택되지 않았으므로 false이고
하나씩 선택하면 true로 변한다.
저 코드에서 [A, A]가 선택되지 않은 이유도

if(isSelected[i] == true) continue;

위코드 덕분이다.

그리고 맨 밑에

isSelected[i] = false;

이 코드는 [A, B], [A, C]를 출력후 B로 넘어갈때 A가 선택되었던것을 지워주는 역할을 하는데
그래서 [A, B], [A, C]를 출력하고 바로 [B, A]가 출력이 가능하게 된것이다.
만일 저 코드가 없었다면 [B, A]는 출력되지 못했을것이다.

중복순열

서로 다른 원소들 중 중복있고 순서에 상관있게 몇개를 뽑아서 나열하는것을
순열이라고 한다.

중복순열과 일반 순열의 가장 큰 차이점은 중복을 허용한다는것이다.
n의 r승으로 표현한다. nΠr = n^r

A, B, C 세개의 문자를 중복있이 순서에 상관있게 두개만 나열하면 아래 그림과 같다.

3^2 = 9

두 자리가 있는데 첫번째 자리에는 A, B, C세가지가 올수 있고
두번째 자리에는 중복허용을 하니 첫번째 자리에 있던 문자도 올 수 있다.
따라서 3*3 = 3^2 = 9 이다.


[A, A], [A, B], [A, C], [B, A], [B, B], [B, C], [C, A], [C, B], [C, C]

이걸 이제 자바코드로 짜보자

import java.util.Arrays;
import java.util.Scanner;

// 중복순열 
public class PermutationWithRepetiton {
	
	public static int N, R; 	// nΠr 즉 n개의 원소중 r개를 중복 있이, 순서있게 나열하고 싶다. n^r승
	public static char[] element, selected; // element는 n개의 원소를 담을 배열, selected는 n개의 원소중 r개를 선택한것을 담을 배열
	
	public static int totalcnt = 0; // 중복순열의 개수 3Π2 = 3^2 = 9 에서 9에 해당하는 값
	
	public static void main(String[] args) {

		Scanner sc = new Scanner(System.in);
		System.out.print("nΠr에서 n의 개수와 r의 개수가 될것을 입력해주세요(한칸씩 띄어써서 다 썼으면 엔터 눌러주세요) : ");
		N = sc.nextInt(); 
		R = sc.nextInt();
		
		element = new char[N];	
		selected = new char[R];
		
		System.out.print("nΠr에서 n개의 원소를 입력해주세요(한칸씩 띄어써서 다 썼으면 엔터 눌러주세요) : ");
		
		for (int i = 0; i < N; i++) {
			element[i] = sc.next().charAt(0);
		}
		
		permutation_with_repetiton(0);
		System.out.println("중복순열의 총 가짓수 : " + totalcnt);
		
	}
	
	public static void permutation_with_repetiton(int cnt) {
		
		if(cnt == R) {
			System.out.println(Arrays.toString(selected));
			totalcnt++;
			return;
		}
		
		for (int i = 0; i < N; i++) {
			selected[cnt] = element[i];
			permutation_with_repetiton(cnt + 1);
		}
		
		
	}
}

위의 일반순열 코드에서
isSelected 파트만 지워주면 된다. 굉장히 간단하다.
왜냐하면 중복순열은 중복을 허용하기 때문에 전체 원소중에 어떤것이 선택이 되어서 그 선택된것은 다시 선택하면 안되겠다. 같은 고민을 할 필요가 없기 때문이다.

조합

서로 다른 원소들 중 중복없이 순서에 상관없게 몇개를 뽑는것을 조합이라고 한다.

n개중 r개를 중복없이 순서 상관없게 뽑는 가짓수
nCr = nPr/r! = n!/(n-r)!r! (단 n >= r)

A, B, C 세개의 문자를 중복없이 순서에 상관없게 두개만 뽑아보면 아래 그림과 같다.

3C2 = 3P2/2! = 3
조합은 이렇게 생각하면 편하다.
A, B, C 세가지 중 두개를 뽑는 조합의 수는
일단 먼저 세가지 중 두개를 순서대로 나열하는 순열인 3P2를 구한다.
그런데 조합은 순서가 없기 때문에 거기에 2! 만큼을 나눈다.
따라서 3P2/2! = 3, 3개이다.


[A, B], [A, C], [B, C]

이걸 이제 자바코드로 짜보자

key point는 [A, B], [A, C], [B, C]에서
항상 오른쪽은 왼쪽보다 (인덱스가)크다는것을 명심하면 된다.

A, B, C 중 두개를 뽑는 문제에서
입력배열은 A, B, C 순서대로 받는다 가정해보자.
그러면 A의 index는 0, B의 index는 1, C의 index는 2이다.
[A, B], [A, C], [B, C]
항상 오른쪽 원소가 왼쪽 원소보다 인덱스가 크다.

만일 입력배열을 C, A, B 순서대로 받는다 가정해보자.
그러면 C의 index는 0, A의 index는 1, B의 index는 2이다.
[C, A], [C, B], [A, B]
항상 오른쪽 원소가 왼쪽 원소보다 인덱스가 크다...

느낌이 오는가..?
이느낌을 살려서 자바로 코드를 짜보자.

import java.util.Arrays;
import java.util.Scanner;

public class CombinationTest {

	public static int N, R; 	// nCr 즉 n개의 원소중 r개를 중복 없이, 순서없게 선택하고 싶다. nPr/r!
	public static char[] element, selected; // element는 n개의 원소를 담을 배열, selected는 n개의 원소중 r개를 선택한것을 담을 배열
	
	public static int totalcnt = 0; // 조합의 개수 3C2 = 3*2/2! = 3 에서 3에 해당하는 값
	
	public static void main(String[] args) {
		
		Scanner sc = new Scanner(System.in);
		System.out.print("nCr에서 n의 개수와 r의 개수가 될것을 입력해주세요(한칸씩 띄어써서 다 썼으면 엔터 눌러주세요) : ");
		N = sc.nextInt(); 
		R = sc.nextInt();
		
		element = new char[N];	
		selected = new char[R];
		
		System.out.print("nCr에서 n개의 원소를 입력해주세요(한칸씩 띄어써서 다 썼으면 엔터 눌러주세요) : ");
		
		for (int i = 0; i < N; i++) {
			element[i] = sc.next().charAt(0);
		}
		
		combination(0, 0);
		System.out.println("조합의 총 가짓수 : " + totalcnt);
	}
	
	public static void combination(int cnt, int start) {
		
		if(cnt == R) {
			System.out.println(Arrays.toString(selected));
			totalcnt++;
			return;
		}
		
		for (int i = start; i < N; i++) {
			selected[cnt] = element[i];
			combination(cnt + 1, i + 1);
		}
		
	}

}

앞에서 말했던 항상 오른쪽 원소의 인덱스가 왼쪽 원소의 인덱스보다 크게 하는 식으로 출력하려면
따로 변수(여기선 start)를 두어서 재귀시 써먹어야한다.

중복조합

서로 다른 원소들 중 중복있고 순서에 상관없게 몇개를 뽑는것을 조합이라고 한다.

n개중 r개를 중복있고 순서 상관없게 뽑는 가짓수
nHr = r+(n-1)Cr = n+r-1Cn-1 (단 n >= r)

A, B, C 세개의 문자를 중복있고 순서에 상관없게 두개만 뽑아보면 아래 그림과 같다.

중복조합은 일반 조합에 중복을 허용하는 경우이다.
일반 조합이 주머니에 A, B, C 구슬 세개를 넣어서 한번에 두개를 뽑는것이라면
중복조합은 주머니에 A, B, C 구슬 세개를 넣어서 하나를 먼저뽑은후 다시 주머니에 넣는다. 그리고 다시 뽑는것이다. 이때 순서를 고려하지 않는다면 그것은 중복조합이된다. (만약 이때 순서를 고려했다면 그건 중복순열이다.)
어찌되었건 저 위에 그림에서 A, B, C 세가지 중 두개를 뽑는 중복조합의 수는 공식에 의해
3+2-1C3-1 = 4C2 = 6, 6가지이다.

[A, A], [A, B], [A, C], [B, B], [B, C], [C, C]
이렇게 표현해볼수 있을것이다.

이걸 이제 자바코드로 짜보자.

import java.util.Arrays;
import java.util.Scanner;

public class CombinationWithRepetiton {

	public static int N, R; 	// nHr 즉 n개의 원소중 r개를 중복 있게, 순서없게 선택하고 싶다. 
	public static char[] element, selected; // element는 n개의 원소를 담을 배열, selected는 n개의 원소중 r개를 선택한것을 담을 배열
	
	public static int totalcnt = 0; // 중복조합의 개수 3H2 = 3+2-1C3-1 = 4C2 = 6 에서 6에 해당하는 값
	
	public static void main(String[] args) {
		
		Scanner sc = new Scanner(System.in);
		System.out.print("nHr에서 n의 개수와 r의 개수가 될것을 입력해주세요(한칸씩 띄어써서 다 썼으면 엔터 눌러주세요) : ");
		N = sc.nextInt(); 
		R = sc.nextInt();
		
		element = new char[N];	
		selected = new char[R];
		
		System.out.print("nHr에서 n개의 원소를 입력해주세요(한칸씩 띄어써서 다 썼으면 엔터 눌러주세요) : ");
		
		for (int i = 0; i < N; i++) {
			element[i] = sc.next().charAt(0);
		}
		
		combinationwithrepetiton(0, 0);
		System.out.println("중복조합의 총 가짓수 : " + totalcnt);
	}
	
	public static void combinationwithrepetiton(int cnt, int start) {
		
		if(cnt == R) {
			System.out.println(Arrays.toString(selected));
			totalcnt++;
			return;
		}
		
		for (int i = start; i < N; i++) {
			selected[cnt] = element[i];
			combinationwithrepetiton(cnt + 1, i);
		}
		
	}

}

중복조합코드는 조합코드와 거의 99.9프로 일치한다.
유일한 차이점은 중복조합은 중복을 허용하기 때문에
마지막 combinationwithrepetiton(cnt + 1, i); 에 i + 1 대신 i를 넣는것이다.

부분집합

한 집합의 원소들로만 구성한 집합

집합의 원소가 총 n개일때, 공집합을 포함한 부분집합의 개수는 2^n개이다.
즉, 각각의 원소들을 부분집합에 포함시키거나 포함시키지 않는 두가지의 경우를 모든 원소에 적용한것의 경우의 수와 같다.

A, B, C 세개의 문자의 부분집합을 구해보면 다음과 같다.

위에 설명에도 적어 놨듯이 부분집합은
각강의 원소들을 부분집합에 포함시키거나 포함시키지 않는 두가지 경우의 수를 모든 원소에 적용한것과 같고
따라서 부분집합의 총 개수는 2^n개이다.
따라서 위 사진에서는

{A,B,C}, {A, B}, {A, C}, {A}, {B, C}, {B}, {C}, { }

으로 총 8개이다.

이걸 이제 자바코드로 짜보자.

import java.util.Scanner;

public class Subset {

	public static int N;	// N은 부분집합을 구하고 싶은 집합의 총 원소의 개수
	public static char[] element;// element는 부분집합을 구하고 싶은 집합의 원소를 담을 배열
	public static boolean[] isSelected;	// element의 각 원소들이 선택되었는지 아닌지를 판단해주는 배열 (위 사진에서) true 이면 O, false이면 X
	
	public static int totalcnt = 0; // 부분집합의 개수
	
	public static void main(String[] args) {
		
		Scanner sc = new Scanner(System.in);
		System.out.print("(부부분집합을 구하고 싶은)집합의 총 원소의 개수를 입력해주세요(다 썼으면 엔터 눌러주세요) : ");
		N = sc.nextInt();
		
		element = new char[N];
		isSelected = new boolean[N];
		System.out.print("(부부분집합을 구하고 싶은)집합의 원소를 입력해주세요(한칸씩 띄어써서 다 썼으면 엔터 눌러주세요) : ");
		
		for (int i = 0; i < N; i++) {
			element[i] = sc.next().charAt(0);
		}
		
		subset(0);
		System.out.println("부분집합의 총 개수 : " + totalcnt);
	}
	
	public static void subset(int cnt) {
		
		if(cnt == N) {
			totalcnt++;
			System.out.print("{");
			for (int i = 0; i < N; i++) {
				
				if(isSelected[i] == true)
					System.out.print(element[i] + " ");
			}
			System.out.println("}");
			return;
		}
		
		isSelected[cnt] = true;
		subset(cnt+1);
		isSelected[cnt] = false;
		subset(cnt+1);
	}

}

isSelected배열을따로 둬서
isSelected배열이 true이면 선택된것, false이면 선택이 되지 않은것을 의미한다.

cnt가 (부분집합을 구하고 싶은)집합의 총 원소의 개수에 다다르면
재귀를 중단시키고
isSelected배열을 순회해서 true이면 선택되었으므로 동일한 인덱스의 element배열의 원소를
출력하고
그렇지 않으면 출력하지 않는다.

부분집합은 주로 knapsack문제에 이용된다. Knapsack문제란
배낭의 용량이 주어지고 각 물건들의 무게와 가격이 주어지고
그 배낭의 용량을 넘지 않는 선에서 어떤 물건을 담아야 가장 가격을 높게 할 수 있냐라는 식이다.

그런데 부분집합을 구할수 있는 코드는
꼭 위와 같은 방식으로 풀지 않아도 된다.

예를들면 아까 위에서 다뤘던
집합 {A, B, C}의 부분집합 {A,B,C}, {A, B}, {A, C}, {A}, {B, C}, {B}, {C}, { }
3개짜리 조합({A,B,C}) + 2개짜리 조합({A, B}, {A, C}, {B, C}) + 1개짜리 조합({A}, {B}, {C}) + 0개짜리 조합({ }) 이고
이는 위에서 다루었던 조합코드를 이용해서 코드를 짤 수도 있다.

그 방식대로 코드를 짜보겠다.

import java.util.Arrays;
import java.util.Scanner;

public class Subset2 {

	public static int N;	// N은 부분집합을 구하고 싶은 집합의 총 원소의 개수
	public static char[] element, selected;// element는 n개의 원소를 담을 배열
	public static boolean[] isSelected;
	
	public static int totalcnt = 0; // 부분집합의 개수
	
	public static void main(String[] args) {
		
		Scanner sc = new Scanner(System.in);
		System.out.print("(부부분집합을 구하고 싶은)집합의 총 원소의 개수를 입력해주세요(다 썼으면 엔터 눌러주세요) : ");
		N = sc.nextInt();
		
		element = new char[N];
		isSelected = new boolean[N];
		System.out.print("(부부분집합을 구하고 싶은)집합의 원소를 입력해주세요(한칸씩 띄어써서 다 썼으면 엔터 눌러주세요) : ");
		
		for (int i = 0; i < N; i++) {
			element[i] = sc.next().charAt(0);
		}
		
		for (int i = 0; i <= N; i++) {
			selected = new char[i];
			combination(0, 0, i);
		}
		System.out.println("부분집합의 총 개수 : " + totalcnt);
		
	}
	public static void combination(int cnt, int start, int R) {
		if(cnt == R) {
			totalcnt++;
			System.out.println(Arrays.toString(selected));
			return;
		}
		
		for (int i = start; i < N; i++) {
			
			selected[cnt] = element[i];
			combination(cnt+1, i+1, R);
		}
		
	}

}

위의 부분집합 코드에서 nCr에 해당하는 r을 0부터 n까지 증가시켜서 각각의 조합을 구하면 된다.

profile
현명한개발자가되자

0개의 댓글