Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

41-gjsk132 #195

Merged
merged 2 commits into from
Sep 11, 2024
Merged

41-gjsk132 #195

merged 2 commits into from
Sep 11, 2024

Conversation

gjsk132
Copy link
Member

@gjsk132 gjsk132 commented Jul 1, 2024

🔗 문제 링크

백준2098 : 외판원 순회

✔️ 소요된 시간

2시간

✨ 수도 코드

🎯 문제 이해

문제 요약
도시의 수와 각 도시 간에 이동하는데 드는 비용이 행렬의 형태로 주어진다.
1번 도시에서 출발해서 모든 도시를 모두 거쳐서 1번 도시로 다시 돌아올 때의 최소 비용을 구하는 문제이다.
( 마지막에 1번 도시로 돌아올 때를 제외하고는, 한 번 갔던 도시는 갈 수 없다. )

해당 문제는 TSP의 가장 일반적인 형태의 문제이다.

TSP란?

모든 정점이 서로 연결되어 있고, 그 사이에 가중치를 가지는 완전 그래프가 주어진다.
그리고, 그래프의 출발 정점에서 모든 정점들을 방문하고, 원래의 출발 정점으로 되돌아오는 최소 비용(또는 경로)를 찾는 문제이다.

외판원 순회 Traveling Salesman problem (TSP) 문제로 조합 최적화 문제의 일종으로
NP-난해 집합에 속하는데...
쉽게 말해서, 어떠한 다항식을 정의해서 풀 수 있는게 아니라
모든 경우의 수를 확인해야지 해결할 수 있다고 한다.

사용하는 알고리즘

모든 경우의 수를 확인해야 하니까 브루트포스로 푼다면 시간초과가 난다.
모든 정점이 연결되어 있다보니, n개의 점들을 순회하는 모든 경우의 수는 n!이 된다.
n이 20만 되더라도 2,432,902,008,176,640,000의 경우의 수를 따져야 하니... 다른 방법을 사용한다.

DP를 사용한다.
-> 현재 정점의 위치와 현재까지 방문한 정점을 이용하면 해당 문제를 해결할 수 있다.

여기서 추가로 현재까지 방문한 정점을 표현하는데 비트마스킹을 사용해주면 됩니다.

🔍코드 설명

input = open(0).readline

N = int(input())

cost = [list(map(int, input().split())) for _ in range(N)]

INF = float('inf')
dp = [[0]*((1 << N) - 1) for _ in range(N)]

def DFS(now, visited):
    if visited == (1<<N)-1:
        if cost[now][0]:        
            return cost[now][0]
        else:
            return INF
    
    if dp[now][visited] != 0:
        return dp[now][visited]

    dp[now][visited] = INF

    for i in range(1, N):
        if cost[now][i] == 0:
            continue
        if visited & (1<<i):
            continue
        dp[now][visited] = min(dp[now][visited], cost[now][i]+DFS(i,visited | (1<<i)))

    return dp[now][visited]

print(DFS(0,1))

📚 새롭게 알게된 내용

외판원 순회로 인해 굴러가는 스노우볼

이것저것 참고사이트
외판원 순회 개념
외판원 순회
NP-hard

@gjsk132 gjsk132 self-assigned this Jul 1, 2024
Copy link
Collaborator

@9kyo-hwang 9kyo-hwang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

악명 높은 외판원 순회... 저도 공부하고 나서야 이해가 되네요....
기본적으로 DP는 Stateless 하기 떄문에, 비트마스킹을 이용해 i번 도시에서 j번 도시로 가는 정보를 기억하고 사용할 수 있도록 하는 기법이 상당히 새롭네요...

제 경우에도 top-down approach가 직관적이어서 이렇게 풀어봤습니다.

전체 코드(파이썬)

input = open(0).readline

INF = 987654321

N = int(input())
W = [list(map(int, input().split())) for _ in range(N)]
dp = [[-1] * (1 << N) for _ in range(N)]


def TSP(i: int = 0, visited: int = 1) -> int:
    if (1 << N) - 1 == visited:
        if W[i][0] == 0:
            return INF
        else:
            return W[i][0]
            
    if dp[i][visited] > -1:
        return dp[i][visited]
        
    dp[i][visited] = INF
    for j in range(N):
        if W[i][j] == 0 or visited & (1 << j):
            continue
        
        dp[i][visited] = min(dp[i][visited], TSP(j, visited | (1 << j)) + W[i][j])
        
    return dp[i][visited]
    
    
print(TSP())

전체 코드(c++)

#include <iostream>
#include <vector>
#include <cmath>

using namespace std;

constexpr int INF = 1e9;

int N, VisitedAllCities;
vector<vector<int>> W, DP;

inline bool HasVisitedAllCities(const int CurrentVisited)
{
    return VisitedAllCities == CurrentVisited;
}

int TSP(int From = 0, int CurrentVisited = 1)
{
    if(HasVisitedAllCities(CurrentVisited))  // 모든 노드 방문
    {
        if(W[From][0] == 0) return INF;
        else return W[From][0];
    }
    else if(DP[From][CurrentVisited] > -1)  // 중복
    {
        return DP[From][CurrentVisited];
    }
    else
    {
        DP[From][CurrentVisited] = INF;  // 방문 Mark
        for(int To = 0; To < N; ++To)
        {
            if(W[From][To] == 0 || (CurrentVisited & (1 << To))) continue;
            
            int NextVisited = CurrentVisited | (1 << To);
            DP[From][CurrentVisited] = min(DP[From][CurrentVisited], TSP(To, NextVisited) + W[From][To]);
        }
        return DP[From][CurrentVisited];
    }
}

int main()
{
    cin.tie(nullptr)->sync_with_stdio(false);
    
    cin >> N;
    W.assign(N, vector(N, 0));
    VisitedAllCities = (1 << N) - 1;
    
    for(int i = 0; i < N; ++i)
    {
        for(int j = 0; j < N; ++j)
        {
            cin >> W[i][j];
        }
    }
    
    DP.assign(N, vector(VisitedAllCities, -1));
    
    cout << TSP();

    return 0;
}

bottom-up 방식의 풀이는 이 블로그가 해설이 괜찮아 보이네요 :)

@mjj111 mjj111 self-requested a review July 24, 2024 10:52
mjj111

This comment was marked as abuse.

Copy link
Collaborator

@mjj111 mjj111 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set으로 풀었다가 메모리 초과해서 당황했네요..ㅎ
어쩔 수 없이 비트마스킹을 제대로 학습하게 된 계기가 됐어요 ㅋㅋㅋ ㅠ

  • (비트마스킹 학습한다고 하루 온종일 잡았네요 멍청한..)
import java.io.*;
import java.util.*;

public class Main {

    static int N;
    static final int INF = 16000000;
    static int[][] W, dp;

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        N = Integer.parseInt(br.readLine());
        W = new int[N][N];
	
        for(int i = 0; i < N; i++) {
            StringTokenizer st  = new StringTokenizer(br.readLine());
            for(int j = 0; j < N; j++) W[i][j] = Integer.parseInt(st.nextToken());
        }

        dp = new int[N][(1<<N)-1];
        for(int i = 0; i < N; i++) Arrays.fill(dp[i], -1);

        System.out.println(dfs(0, 1));
    }

    static int dfs(int now, int visit) {

        if(visit == (1<<N)-1) {
            if(W[now][0] == 0) return INF;
            return W[now][0];
        }

        if(dp[now][visit] != -1) return dp[now][visit];
        dp[now][visit] = INF;

        for(int i=0;i<N;i++) {
            if((visit & (1<<i)) == 0 && W[now][i] != 0) {
                dp[now][visit] = Math.min(dfs(i, visit | (1 << i)) + W[now][i], dp[now][visit]);              }
        }
        return dp[now][visit];
    }
}

@gjsk132
Copy link
Member Author

gjsk132 commented Sep 4, 2024

음 작성 중이던 PR을 2달만에 다시보니... 제가 뿌린 똥을 다시 맞는 기분이군요...

@9kyo-hwang
Copy link
Collaborator

@xxubin04 리뷰를 다셔야 합니다 😒

Copy link
Member

@xxubin04 xxubin04 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2달 넘은 시점에서 리뷰 달아서 죄송합니다..😭😭
매번 직접 풀어보고 리뷰 달아야지 하다가 못 달고 이 사태가 일어났습니다..

'외판원 순회'라는 이름을 어디선가 들어본 듯 한데, 여기서 보니 신기하네요..
모든 경우의 수를 확인해야한다는 점에서 브루트포스로 풀면 되는구나 싶었는데 그건 또 시간초과가 나서 DP로 풀어주는 거군요.
꼭 풀어보고 싶었는데.. 다음에 꼭 풀어보겠습니다!!

@gjsk132 gjsk132 merged commit 2621907 into main Sep 11, 2024
@gjsk132 gjsk132 deleted the 41-gjsk132 branch September 11, 2024 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants