C++ 코딩테스트 강좌, 세일즈맨의 고민

세일즈맨의 고민은 전통적인 알고리즘 문제 중 하나로, “외판원 순회 문제” 또는 “여행하는 세일즈맨 문제”라고도 불린다.
이 문제는 여러 도시가 있을 때, 세일즈맨이 모든 도시를 방문한 후 다시 출발 도시로 돌아오는 가장 짧은 경로를 찾는 것이다.
이 문제는 조합 최적화(combinatorial optimization) 문제에 속하며 NP-hard로 분류된다.
따라서, 작은 데이터 셋은 브루트 포스(brute force) 알고리즘으로 해결할 수 있지만, 큰 데이터 셋은 더 효율적인 접근 방법이 필요하다.

문제 설명

N개의 도시가 주어지고, 각 도시 간의 거리 행렬이 주어진다. 목표는 세일즈맨이 각 도시에 한 번씩 방문한 후 출발 지점으로 돌아오는 가장 짧은 경로의 거리 합을 구하는 것이다.
다음은 문제의 형식이다.

        
        입력:
        N (도시의 수)
        거리 행렬 (nxn 행렬)
        
        출력:
        최소 경로의 길이
        
    

예제

입력 예시:

        4
        0 10 15 20
        10 0 35 25
        15 35 0 30
        20 25 30 0
    

출력 예시:
80

문제 해결 과정

문제를 해결하기 위한 여러 가지 접근법이 있으며, 여기서는 브루트 포스 알고리즘과 동적 프로그래밍(DP) 두 가지 방법을 사용하여 이 문제를 해결해보겠다.

1. 브루트 포스 방법

브루트 포스 방법은 모든 가능한 경로를 탐색하여 최단 경로를 찾는 방식이다.
모든 도시의 순열을 생성하여 각 경로 거리를 계산하고, 이 중에서 최소 거리를 찾는다.

알고리즘 설명

  1. 모든 도시의 순열을 생성한다.
  2. 각 순열에 대해 전체 거리 값을 계산한다.
  3. 계산된 거리 중 최소 거리 값을 찾는다.

C++ 코드 예시

        
        #include 
        #include 
        #include 
        #include 

        using namespace std;

        int calculateDistance(const vector>& dist, const vector& path) {
            int totalDistance = 0;
            for (int i = 0; i < path.size() - 1; i++) {
                totalDistance += dist[path[i]][path[i + 1]];
            }
            totalDistance += dist[path.back()][path[0]]; // Return to start
            return totalDistance;
        }

        int main() {
            int N;
            cin >> N;
            vector> dist(N, vector(N, 0));

            for (int i = 0; i < N; i++)
                for (int j = 0; j < N; j++)
                    cin >> dist[i][j];

            vector cities(N);
            for (int i = 0; i < N; i++) cities[i] = i;

            int minDistance = INT_MAX;

            do {
                int currentDistance = calculateDistance(dist, cities);
                minDistance = min(minDistance, currentDistance);
            } while (next_permutation(cities.begin() + 1, cities.end())); // Fix first city

            cout << minDistance << endl;

            return 0;
        }
        
    

2. 동적 프로그래밍(DP) 방법

동적 프로그래밍 방법은 메모리를 효율적으로 사용하는 방법이다.
도시를 비트마스크로 표현하여 각 도시의 방문 여부를 나타내고, 재귀 함수와 메모이제이션을 이용하여 최단 경로를 계산한다.
이 경우 복잡도는 O(n^2 * 2^n)로 줄어든다. 다음은 동적 프로그래밍 솔루션의 설명이다.

알고리즘 설명

  1. 비트마스크를 사용하여 각 도시의 방문 여부를 나타낸다.
  2. 최소 비용 테이블을 초기화하고 0으로 설정한다.
  3. 재귀 함수를 통해 가능한 경로를 탐색하며 최소 거리 값을 갱신한다.

C++ 코드 예시

        
        #include 
        #include 
        #include 

        using namespace std;

        int tsp(int pos, int visited, const vector>& dist, vector>& memo) {
            if (visited == ((1 << dist.size()) - 1)) {
                return dist[pos][0]; // Return to the start city
            }

            if (memo[pos][visited] != -1) {
                return memo[pos][visited];
            }

            int minCost = INT_MAX;

            for (int city = 0; city < dist.size(); city++) {
                if ((visited & (1 << city)) == 0) {
                    int newCost = dist[pos][city] + tsp(city, visited | (1 << city), dist, memo);
                    minCost = min(minCost, newCost);
                }
            }

            return memo[pos][visited] = minCost;
        }

        int main() {
            int N;
            cin >> N;
            vector> dist(N, vector(N, 0));

            for (int i = 0; i < N; i++)
                for (int j = 0; j < N; j++)
                    cin >> dist[i][j];

            vector> memo(N, vector((1 << N), -1));

            cout << tsp(0, 1, dist, memo) << endl;

            return 0;
        }
        
    

결론

이 강좌에서는 C++로 구현된 세일즈맨의 고민 문제를 통해 문제 정의, 예제,
그리고 문제 해결을 위한 브루트 포스 및 동적 프로그래밍 방법을 상세히 알아보았다.
주의할 점은 브루트 포스 방법은 도시의 수가 작을 때만 사용해야 하며, 도시의 수가 증가할수록 동적 프로그래밍 방법을 사용하는 것이 더 효율적이다.
이러한 알고리즘 문제들은 실제 코딩 테스트 및 면접에서 자주 등장하므로, 충분한 연습이 필요하다.