코딩테스트

순열과 조합 (C++)

코앤미 2023. 1. 16. 15:08

 

[ n개의 숫자를 전부 사용해서 만들 수 있는 모든 순열의 갯수]

 

ex)

1,2,2 가 input으로 들어오면

 

1,2,2

2,1,2

2,2,1

 

3가지가 나온다. 여기서 중요한 점은 "중복된 순열은 제거" 된다는 점이다.

1,2( 1th ) , 2( 2th )   

1, 2( 2th ) , 2( 1th ) 

이런식으로 사실 상 같은 순열이 2번 추출되면 안된다는 뜻이다.

 

이때 사용하는게 바로 C++ 헤더 중 #include<algorithm> 으로 사용할 수 있는 next_permutation이다.

우리가 원하는 결과 값은

 

1,2,2

2,1,2

2,2,1

 

이렇게 3개인데, next_permuation( vec.begin(),vec.end() ) 이 메소드가 하는 역할은 vec를 중복되지 않은 다음 순열 값으로 변경해주는 것이다.

처음 next_permutation이 호출되면 vec의 값이

1,2,2 -> 2,1,2 로

두번째로 next_permuation이 호출되면 vec의 값이

2,1,2 -> 2,2,1 로 바뀐다.

우리는 첫 순열인 1,2,2 도 필요하기에 do - while문으로 다음과 같이 구현해주면 된다.

 

아래는 string이 input의 데이터 타입으로 들어왔지만, arr, string, vector 등 전부 사용 가능하다.

#include <algorithm>
#include <iostream>
using namespace std;

//vec :   1,2,3
//next_permutation( vec.begin(), vec.end() ) ->  1,2,3 의 모든 순서쌍을 순차적으로 탐색 (nPn), 단 중복은 자동제거
//  미리 오름차순 정렬이 되어있어야한다!!
// ( 중복제거기에 1,0,0 -> 1,0(1th),0(2th)  1,0(2th),0(1th)
//1,2,3
//1,3, 2



int main()
{
    std::string s = "122";
    std::sort(s.begin(), s.end());
    do {
        std::cout << s << '\n';
    } while (std::next_permutation(s.begin(), s.end()));
}

/* OUTPUT */
/* 112    */
/* 121    */
/* 211    */

 

 

[n개의 수 로 만들 수 있는 모든 숫자]

n개의 수를 통해 만들 수 있는 모든 숫자의 경우의 수는 어떻게 구할 수 있을까?

(ex)

1,2,3 으로 만들 수 있는 모든 숫자는?

 

1

2

3

1,2

1,3

........

3,2,1

 

이런식으로 모든 경우의 수를 고려하는 것인데,  위에서 배운 것을 활용하면 금방 해결할 수 있다.

"n개의 숫자로 만들 수 있는 모든 숫자" 를 수식적으로 표현하면 nP1 , nP2, ..... nPn 까지 모든 경우의 수를 찾는 것이다.

따라서 next_permutation을 사용하되, 그 순열에서 1자리만, 2자리만,..... n자리만 사용하되, 중복을 제거하면 된다.

중복을 제외하는 이유는 다음과 같다

 

next_permutation을 통해 나온 2개의 순열

4,3,2,1

4,2,3,1

에서, 1자리만 뽑으면 각각

1이 나오고, 이는 중복된다.

 

따라서 set이나 visit 배열을 통해 중복값을 검출해주면 해결할 수 있다.

 

 

string str="312";
sort(str.begin(),str.end() ); //next_permutation은 정렬 필수
set<string> nums;
int n = str.size();
string sub;
do {
    for (int i = 1; i <= n; i++)
    {
        sub = str.substr(0, i);   // 정해진 n개의 숫자를 1,2,3,....n개 추출
        nums.insert(sub);
    }
} while (next_permutation(str.begin(), str.end())); // n개의 숫자로 가능한 모든 순열 탐색

 


[순서 상관 없이 선택하기]

흔히 수학에서 조합, Combination으로 불리며 nCr 의 기호로 사용된다. n개 중 순서를 고려하지 않고 3개를 뽑을 때의 경우의 수를 모두 구하는 것인데,

마찬가지로 next_permutation 메소드로 구현 가능하다.

next_permutation은 중복된 순열을 자동 제거해주는데, 이 특성을 활용해보자.

5개 중 3개를 순서 상관없이 선택하는 5C3의 경우 [0,0,1,1,1]의 배열을 next_permutation으로 탐색하면 된다.

여기서 0은 선택하지 않을 2개를 고르는 것, 그리고 1은 선택할 3개를 고르는 것이다.

(ex)

[0,0,1,1,1] 는 0,1번째 idx의 수 는 선택하지 않고, 2,3,4번째 idx의 수를 선택한다는 의미이다.

 

만약 1,2,3,4,5 중 3개를 고르고 싶을 때, 1,2,3,4,5를 next_permutation으로 돌린다면

1,2,3 != 3,2,1 과 같이 순서 쌍이 고려되지만, 고르는 3가지를 1이라고 가정하고 해결한다면 

11100 == 11100 처럼 순서를 무시할 수 있다.

int main()  //nCr 구하기 -> 중복값 제거되는 것을 활용
{
    int arr[5] = { 0, 0, 1, 1, 1 };   //ex) 5개중 3개 뽑기.
    sort(arr, arr + 5);
    do {
        for (int elem : arr) {  
            std::cout << elem;
        }
        std::cout << '\n';
    } while (next_permutation(arr, arr + 5));
}

// OUTPUT
// 00111
// 01011
// 01101
// 01110
// 10011
// 10101
// 10110
// 11001
// 11010
// 11100

 

 

 

 

[실전 문제]

번외로, 이번에 순열과 조합에서 next_permutation을 사용한 풀이만 공개했는데, 아래와 같이 재귀로 조합을 구현할 수 도 있다. 로직을 직접 구현하는 것이기에 유동적인 상황에서 응용하기는 쉽지만, 코드 자체는 더 길어 시간이 지체될 수 있기에 마지막에 소개한다.

재귀함수를 통해 n째 값을

뽑을 때, 뽑지 않을 때

2가지로 나누어 가지를 나누어가며 재귀를 돌면 해결할 수 있다.

#include <iostream>
#include <vector>

using namespace std;
vector<vector<char>> combs;
void Combination(vector<char> arr, vector<char> comb, int r, int index, int depth)
{//depth: n개의 목록들 중 몇번째꺼 탐색중?
//index: 현재 몇번째 까지 선택했는가 (총 r개 선택) 
    if (r == 0)
    {
        combs.push_back(comb);
       
    }
    else if (depth == arr.size())  // depth == n // 계속 안뽑다가 r 개를 채우지 못한 경우는 이 곳에 걸려야 한다.
    {
        return;
    }
    else
    {
        // arr[depth] 를 뽑은 경우
        comb[index] = arr[depth];
        Combination(arr, comb, r - 1, index + 1, depth + 1);

        // arr[depth] 를 뽑지 않은 경우
        Combination(arr, comb, r, index, depth + 1);
    }
}

int main()
{//nCr

    vector<char> vec = { 'a', 'b', 'c', 'd', 'e' };  //뽑을 데이터 목록 (n 의 데이터 목록)

    int r = 3;
    vector<char> comb(r); //nCr 에서 선택한 r을 누적하며 넘겨줄 자료구조  (r 을 구하는 곳)

    //
    Combination(vec, comb, r, 0, 0);  // {'a', 'b', 'c', 'd', 'e'}의 '5C3' 구하기 
   
    return 0;
}



예제 문제

https://school.programmers.co.kr/learn/courses/30/lessons/1835

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

next_permutation을 통한 완전탐색으로 해결할 수 있는 문제다.

#include <string>
#include <vector>
#include<iostream>
#include<algorithm>
#include<map>
using namespace std;

// 전역 변수를 정의할 경우 함수 내에 초기화 코드를 꼭 작성해주세요.
string str = "ACFJMNRT"; //이미 사전순으로 정렬됨.
map<string, bool> ma; //bool 디폴트= false
int solution(int n, vector<string> data) {
    int answer = 0;
    ma.clear();
    str = "ACFJMNRT";
    vector<map<char, int> >v;
    // sort(str.begin(),str.end());
    int cnt = 0;
    do {
        map<char, int> tmp;
        for (int i = 0; i < str.size(); i++)
            tmp[str[i]] = i;
        v.push_back(tmp);
    } while (next_permutation(str.begin(), str.end()));
    int cnt2 = 0;
    for (int j = 0; j < v.size(); j++) {
        bool chk = true;
        for (int i = 0; i < data.size(); i++) {
            char a = data[i][0], b = data[i][2], cmd = data[i][3];
            int num = (int)data[i][4] - 48;
            int dif = v[j][a] - v[j][b];
            if (dif < 0) dif = (-1) * dif;
            dif--;//차이가 i ==간격이 i-1
            if (cmd == '=') {
                if (dif != num) {

                    chk = false;
                    break;
                }
            }
            else if (cmd == '>') {

                if (dif <= num) {
                    chk = false;
                    break;
                }
            }
            else if (cmd == '<') {

                if (dif >= num) {
                    chk = false;
                    break;
                }
            }
        }
        if (chk == true)
            answer++;
    }
    return answer;
}