본문 바로가기
알고리즘/개념정리

순열, 조합

by 새싹감자 2022. 9. 12.

 

오늘은 코딩테스트에서 자주 마주치는 순열과 조합에 대해서 정리해보려한다.

 

순열


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

이를 식으로 나타내면

nPn=n!(Factorial)

으로 나타낼 수 있다.

예시로 nP3을 구현해보자면, 순열의 반복적인 일은 숫자를 한개씩 뽑는일이다.

가능한 모든 수를 시도하면서 기존 수와 중복되지 않게 하나의 수를 선택한다.

이는 3중 for문을 사용해서 구현할 수 있다.

 

하지만 3처럼 숫자가 정해지지 않는다면, for문의 갯수를 알 수 없으므로 구현할 수 없다.

이 상황에서는 재귀를 사용해서 순열을 구현한다.

 

 

  • 재귀로 구현하기

p메소드에서는 순열에 포함될 수를 하나씩 뽑는다.

p메소드에서 해야될 일을 다음과 같다.

1. 가능한 모든 수를 시도

2. 다음수 뽑으러가기 ->여기서 재귀호출 일어남

달라지는 부분은 이 재귀의 결정요인 값이다. 즉, 이는 파라미터로 사용한다.

뽑힌 수가 저장될 자리(뽑는 순서)

i는 뽑을 숫자이고, cnt는 현재 뽑은 숫자의 갯수이다.

 

슈도 코드로 정리하면 아래와 같다.

numbers[] //순열 저장 배열

isSelected[] //인덱스에 해당하는 숫자가 사용 중인지 저장하는 배열perm(cnt) //cnt-현재까지 뽑은 순열 수의 개수

if cnt == 3
    순열 생성 완료

else
for i from 1 to 3

     if isSelected[i]  == true then continue //다음 i로 감 다른 숫자 뽑아

     numbers[cnt] <- i //몇번째 수 뽑을건지. 현재 뽑은 수를 사용해서 인덱스로 활용해서 숫자 뽑기

     isSelected[i]<-true //이수 뽑았어요

     perm(cnt+1) //재귀호출. 여기까지 뽑았어요

     isSelected[i]<-false // 초기화

 end for
package algorithm;

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

public class PermutationTest1 {
    static int N,R, totalCnt;
    static int[] numbers;
    static boolean[] isSelected;
    //nPn: 1부터 n까지의 수중에 n개를 모두 뽑아 순서적으로 나열한 것
    //nPr: 1부터 n까지의 수중에 r개를 모두 뽑아 순서적으로 나열한 것(1<=r<=n)
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        N = sc.nextInt();
        R = sc.nextInt();
        totalCnt=0;

        numbers = new int[R];//뽑힌애들
        isSelected = new boolean[N+1]; //수가 1부터 시작해서 인덱스도 1부터 논리적 매칭 사용
        perm(0);
        System.out.println("총 경우의 수:"+totalCnt);
    }

    private static void perm(int cnt) { 
    // cnt: 직전까지 뽑은 순열에 포함된 수의 개수, cnt+1 번째 해당하는 수를 뽑기
        //가능한 모든 수에 대해 시도
        if(cnt==R) {
            totalCnt++;
            System.out.println(Arrays.toString(numbers));
            return;
        }
        for(int i=1; i<=N; i++) {
        //시도하는 수가 선택되었는지 판단
            if(isSelected[i]) continue;
        //선택되지 않았다면 수를 사용
            numbers[cnt]=i;
            isSelected[i]=true;
        //다음수 뽑으러 가기
            perm(cnt+1);
        //사용했던 수에 대한 선택 되돌리기
            isSelected[i]=false;
        }
    }
}
import java.util.Arrays;  
import java.util.Scanner;  

public class PermutationTest2 {  
static int N,R, totalCnt;  
static int[] numbers, input;  
static boolean[] isSelected;  

//nPn: n개의 입력받은 수중에 n개를 모두 뽑아 순서적으로 나열한 것(입력수 1~100000)  
//nPr: n개의 입력받은 수중에 r개를 모두 뽑아 순서적으로 나열한 것(1<=r<=n)  
public static void main(String[] args) {  
Scanner sc = new Scanner(System.in);  
N = sc.nextInt();  
R = sc.nextInt();  
totalCnt=0;  

input = new int[N];  
numbers = new int[R];//뽑힌애들  
isSelected = new boolean\[N\];  

for (int i = 0; i <N; i++) {  
input[i]=sc.nextInt();  
}  
perm(0);  
System.out.println("총 경우의 수:"+totalCnt);  
}  

private static void perm(int cnt) { 
// cnt: 직전까지 뽑은 순열에 포함된 수의 개수, cnt+1 번째 해당하는 수를 뽑기  
//가능한 모든 수에 대해 시도  
if(cnt==R) {  
totalCnt++;  
System.out.println(Arrays.toString(numbers));  
return;  
}  

//  
for(int i=0; i<N; i++) {  
//시도하는 수가 선택되었는지 판단  
if(isSelected[i]) continue;  
//선택되지 않았다면 수를 사용  
numbers[cnt]=input[i];  
isSelected[i]=true;  
//다음수 뽑으러 가기  
perm(cnt+1);  
//사용했던 수에 대한 선택 되돌리기  
isSelected[i]=false;  
}  
}  
}

순열로 문제를 풀기 위해서는 10!정도 까지만 가능하다.

n의 크기가 10정도 일때까지 가능하다.

그거보다 크다면 순열로 푸는 문제가 아닐 가능성이 높다.

순열과 같이 나오는 개념에는 조합이 있다.

 

 

조합


서로다른 n개의 원소 중 r개를 순서 없이 골라낸 것

nCn/2개의 경우의 수가 가장 크다.

N->30, R->5정도면 조합으로 풀 수없다.

 

nCr=n-1Cr-1+n-1Cr

nCr=n! / r!(n-r)!

->n부터 1씩 줄어들면서 r개를 곱함/1부터 1씩 늘어나면서 r개를 곱함

 

슈도코드로 나타내면 아래와 같다.

for i from 1 to 4
    for j from i+1 to 4
        for k from j+1 to 4
            print i,j,k
        end for
     end for
end for
input[] : n개의 원소를 가지고 있는 배열

numbers[] : r개의 크기의 배열, 조합이 저장될 배열

comb(cnt, start)
    if cnt == r
        조합 생성 완료
    else
        for i from start to n-1 //이전 수 다음부터
            numbers[cnt]<-input[i];
            comb(cnt+1, i+1);
        end for
package algorithm;

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

public class CombinationTest {
    static int N,R, totalCnt;
    static int[] numbers, input;


    //nCr: n개의 입력받은 수중에 r개를 모두 뽑아 순서없이 나열한 것(1<=r<=n)
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        N = sc.nextInt();
        R = sc.nextInt();
        totalCnt=0;

        input = new int[N];
        numbers = new int[R];//뽑힌애들

        for (int i = 0; i <N; i++) {
            input[i]=sc.nextInt();
        }

        comb(0,0);
        System.out.println("총 경우의 수:"+totalCnt);
    }

    //cnt+1번째 해당하는 조합에 포함도리 수를 뽑기
    private static void comb(int cnt,int start) { 
        // cnt: 직전까지 뽑은 순열에 포함된 수의 개수, start: 시도할 수의 시작 위치
        if(cnt==R) {
            totalCnt++;
            System.out.println(Arrays.toString(numbers));
            return;
        }

        //가능한 모든 수에 대해 시도(input배열의 가능한 수 시도)
        //start부터 처리시 중복 수 추출 방지 및 순서가 다른 조합 추출 방지
        for(int i=start; i<N; i++) { //선택지
        //start위치부터 처리했으므로 중복체크 필요없음!!
        //선택되지 않았다면 수를 사용
            numbers[cnt]=input[i];
        //다음수 뽑으러 가기
            comb(cnt+1, i+1);
        }
    }
}

중복 관련 로직이 다 사라짐

그렇다면 순열과 조합의 차이는 무엇일까?

 

  • 순열, 조합 차이

순서가 있나?

있다->순열

없다->조합

 

 

활용


주사위던지기

-주사위를 3번 던져서 나올 수 있는 모든 경우

순서고려 중복순열. 6ㅠ3

 

-주사위를 3번 던져서 모두 다른 수가 나올 수 있는 모든 경우

단, 123 132 다른경우

순서고려 순열 6P3

 

-주사위를 3번 던져서 중복되는 경우 제외하고 나올수 있는 모든 경우

중복되는 경우, 112,121,211 ->순서 무관

중복조합

nHr

6H3=6+3-1C3=3C3

 

-주사위를 3번 던져서 모두 다른수가 나올 수 있는 모든 경우

단,123,132,321은 중복되는 경우이다.

조합

6C3

package algorithm;

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

public class Ex_DiceTest {
    static int N, totalCnt;
    static int[] numbers;
    static boolean[] isSelected;

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Scanner sc = new Scanner(System.in);
        N=sc.nextInt();//던지는 주사위 수
        int mode = sc.nextInt();//던지기 모드

        totalCnt=0;
        numbers = new int[N];

        switch (mode) {
        case 1:
            dice1(0); //중복 순열
            break;
        case 2:
            isSelected = new boolean[7]; //1~6 주사위눈 선택여부
            dice2(0); //순열
            break;
        case 3:
            dice3(0,1); //중복조합
            break;
        case 4:
            dice4(0,1); //조합
            break;

        default:
            System.out.println("잘못된 입력");
            return;
        }
        System.out.println("총 경우의 수"+totalCnt);
    }

    //중복순열
    private static void dice1(int cnt) {
        if(cnt==N) {
            totalCnt++;
            System.out.println(Arrays.toString(numbers));
            return;
        }
        //가능한 모든 수 시도
        for(int i=1; i<=6; i++) {
            //수의 중복 선택이 가능하므로 중복 체크 필요 없음!
            //해당 수 선택
            numbers[cnt]=i;
            //다음 주사위 던지러 가기
            dice1(cnt+1);
        }
    }

    //순열
    private static void dice2(int cnt) {
        if(cnt==N) {
            totalCnt++;
            System.out.println(Arrays.toString(numbers));
            return;
        }
        //가능한 모든 수 시도
        for(int i=1; i<=6; i++) {
            //중복 체크 필요!
            if(isSelected[i]) continue;
            //해당 수 선택
            numbers[cnt]=i;
            isSelected[i]=true;
            //다음 주사위 던지러 가기
            dice2(cnt+1);
            isSelected[i]=false;
        }
    }

    //중복조합
    private static void dice3(int cnt,int start) {
        if(cnt==N) {
            totalCnt++;
            System.out.println(Arrays.toString(numbers));
            return;
        }

        for(int i=start; i<=6; i++) {
            numbers[cnt]=i;
            dice3(cnt+1,i);
        }
    }

    //조합
    private static void dice4(int cnt, int start) {
        if(cnt==N) {
            totalCnt++;
            System.out.println(Arrays.toString(numbers));
            return;
        }

        for(int i=start; i<=6; i++) {
            numbers[cnt]=i;
            dice4(cnt+1,i+1);
        }

    }
}

 

부분집합


다수의 중요 알고리즘들이 원소들의 그룹에서 최적의 부분집합을 찾는 것이다.

시간복잡도는 2의 n승. n이 30정도면 10억,,택도 없다

선택한 후 다음원소로 가기 (재귀)

선택안한 후 다음 원소로 가기 (재귀)

input[]
inSelected[]

generateSubset(cnt)
if(cnt==N)
    부분집합 완성
else
    isSelected[cnt]<-true
    generateSubSet(cnt+1)
    isSelected[cnt]<-false
    generateSubSet(cnt+1)
package algorithm;

import java.util.Scanner;

//n개의 수를 입력받고 가능한 모든 부분집합 생성
public class SubSetTest {

    static int N,totalCnt;
    static int[] input;
    static boolean[] isSelected;

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        N=sc.nextInt();
        totalCnt = 0;
        input=new int[N];
        isSelected = new boolean[N];
        for(int i=0; i<N; i++) {
            input[i] = sc.nextInt();
        }
        subset(0);
        System.out.println("총 경우의 수:"+totalCnt);
    }

    private static void subset(int index) { //cnt: 직전까지 고려한 원소 수
        if(index==N) { //더이상 고려한 원소가 없다면 부분집합의 구성이 완성
            totalCnt++;
            for(int i=0; i<N; i++) {
                System.out.print(isSelected[i]? input[i]:"x");
                System.out.print("\t");
            }
            System.out.println();
            return;
        }
        //원소 선택
        isSelected[index]=true;
        subset(index+1);
        //원소 미선택
        isSelected[index]=false;
        subset(index+1);
    }
}

위 코드는 부분집합에서 원소를 모두 더한 값이 0이 되는 경우가 있는지 알아낼 때 활용될 수 있다.

'알고리즘 > 개념정리' 카테고리의 다른 글

순열조합 코드정리  (0) 2023.02.17
서로소 집합 만들기  (1) 2022.09.20
자바 정렬하기  (0) 2022.09.12
BFS,DFS  (0) 2022.08.11
객체 배열  (0) 2022.08.09

댓글