백준: N과 M〈1〉(15649번)

알고리즘 문제 해결 과정을 기록합니다

  ·  2 min read

문제 개요 #

  • Source: 백준 링크
  • Difficulty: Silver 3
  • Key Concept: 백트래킹

접근 방법 #

해당 문제는 std::next_permutationstd::set을 이용해서 길이가 m인 순열을 std::set에 넣어 중복을 제거하는 로직으로 처음 접근하였습니다.

시행착오 #

std::next_permutation을 이용하여 전체 숫자가 아닌 일부만 이용하는 로직을 헷갈려 다시 std::next_permutation 구현을 보면서 학습했습니다.1

template<class BidirIt>
bool next_permutation(BidirIt first, BidirIt last)
{
    auto r_first = std::make_reverse_iterator(last);
    auto r_last = std::make_reverse_iterator(first);
    auto left = std::is_sorted_until(r_first, r_last);

    if (left != r_last)
    {
        auto right = std::upper_bound(r_first, left, *left);
        std::iter_swap(left, right);
    }

    std::reverse(left.base(), last);
    return left != r_last여
}

std::upper_bound는 이전 N-Queen 문제에서 사용된 std::lower_bound와 반대되는 로직으로 해당 값 이상이 되는 값의 위치를 가리키는 iterator를 반환합니다. std::is_sorted_until은 첫번째 매개변수 위치와 두 번째 매개변수 위치 사이에서 비내림차순이 유지되는 마지막 위치를 리턴합니다.2

이후 비내림차순 순열의 가장 큰 값(left)값이 마지막 값이 아니라면, 즉 전체 수열이 이미 비내림차순으로 정렬되어 있지 않았다면 left가 가리키는 값보다 큰 값중 최소값을 선택한 다음 right로 할당합니다. 이 때 left를 pivot이라 할 수 있습니다. pivot은 뒤에서부터 내림차순이 끝나는 원소를 가리키며 이는 바꾸어야 할 최소 위치를 의미합니다.

leftright를 바꾼다음 교체된 left 뒷 부분을 std::reverse로 정렬합니다. 이러한 로직을 이용하여 오름차순으로 정렬된 수열을 넣어주면 사전순으로 구성된 수열들을 얻을 수 있습니다.

해결 방법 #

vector<vector<int>> solution() {
    int n, m;
    std::cin >> n >> m;
    // 길이가 1부터 N까지 자연수 중에서 중복 없이 M개를 고른 수열
    vector<int> num(n);
    set<vector<int>> s;
    for (int i = 0; i < n; i++) {
        num[i] = i + 1;
    }
    do {
        vector<int> temp;
        for (int i = 0; i < m; i++) {
            temp.emplace_back(num[i]);
        }
        s.insert(temp);
    } while (std::next_permutation(num.begin(), num.end()));
    vector<vector<int>> answer;
    for(const vector<int>& v: s){
        answer.emplace_back(v);
    }
    return answer;
}

알고리즘 설명 #

std::set을 이용하여 중복 수열들을 제거하는 방식을 사용하였습니다. 앞서 설명했듯이 std::next_permutation을 이용하여 순열들을 사전순으로 생성되게 한 다음 길이가 m이 된 이후에는 끊고 set에 넣어 보관하도록 하였습니다. 이후 set에 담긴 중복되지 않은 길이가 m인 순열들의 집합을 모두 vector로 옮겨 리턴하였습니다.

성능 분석 #

  • Time Complexity: $O(N!\times M)$

std::next_permutation의 경우 $O(N!)$의 시간복잡도를 가지고 M개씩 복사하는데 또 $O(M)$이 걸리므로 매우 비효율적이지만 N과 M의 크기가 8이하라는 조건이 있어서 해당 문제의 시간 제한 조건을 맞출 수 있었습니다.

핵심 정리 #

std::next_permutation의 구현 코드를 자세히 살펴볼 기회가 없었는데 해당 문제를 풀 때 그 기회를 가질 수 있어 순열 생성의 방식을 더 깊게 이해할 수 있었습니다. 또한 iterator와 관련된 여러 코드들을 보면서 STL 구조에서의 iterator의 사용 방식에 대해서도 고민해볼 수 있었습니다.

참고자료 #