deck:: Logseq/coding tip

-
- ### **외판원 순환 문제 (Treveling Salesman Problem) 개요**
	- **정의:** 여러 도시들이 있고 도시들 사이의 이동 비용이 주어졌을 때, 어느 한 도시에서 출발해서 {{c1 모든 도시를 딱 한 번씩만 방문하고 다시 출발 도시로 돌아오는}} 최소 비용을 구하는 문제.
	  id:: bb52496e-ccb4-40fe-a1bc-20a77831c2d6
	- **특징** : 출발 도시는 {{c1 아무 곳에서 시작해도 상관 없다.}}
	  extra:: 어느 도시에서 출발하든 모든 곳을 한번씩 방문하고 출발도시로 돌아오는 경로가 존재한다면 사이클을 이루기 때문에 어느 곳에서 시작하든지 그 결과는 항상 동일하다. 따라서 보통 0번째 도시부터 시작하는 식으로 문제를 푼다.
	  id:: 364f96a1-15dd-44e5-940e-ca7c5d27650f
	- **시간 복잡도**
		- 완전 탐색을 수행 할 경우 모든 경우를 전부 다 계산해봐야 하기 때문에 도시의 개수가 늘어날 수록 기하급수적으로 탐색 횟수가 늘어난다.
			- 완전 탐색 알고리즘 : {{c1 DFS, 순열}}
			  시간 복잡도 : {{c1 $O(N!)$}}
			  extra:: N개의 도시를 순서를 고려해서 고르는 방법(순열)과 동일하므로 N!개의 경우가 존재한다.
			  id:: 69ee0bf9-9b84-4c5d-8556-8532006bc7fc
		- 따라서 문제를 최적화 하기 위해서 {{c1 비트 마스킹}}과 {{c1 다이나믹 프로그래밍}}을 사용하고, 이때 시간복잡도는 {{c1 $O(N^2 \times 2^N)$}}
		  extra:: dp 테이블은 도시숫자 * 비트마스킹 크기의 2차원 배열이므로 이것을 전부 다 도는데에 $N \times 2^N$이 소모된다. 또한 각 도시에 방문한 뒤 그 도시와 연결된 다음 도시를 탐색하므로 이것도 $N$ 만큼이 소비되므로 시간 복잡도는 이 두개를 곱한 것이 된다.
		  id:: 69ee0c5e-9f3c-46b5-907a-ea1c2f856045
-
- ### **코드(python)**
	- 예시 문제 : 도시의 갯수 N, 비용 행렬 W가 주어졌을 때 모든 도시를 딱 한 번씩만 방문해서 다시 출발 도시로 돌아오는 최소 비용을 구하여라. 비용 행렬 W는 $N \times N$ 크기의 2차원 배열이고 W[i][j]의 의미는 i도시에서 j도시로 가는 비용이다. 또한 W[i][j]와 W[j][i]가 반드시 같지는 않으며, 값이 0인 경우 가는 길이 없다는 것을 의미한다.
		- 방문여부는 {{c1 비트마스킹}}을 통해 기록한다.
		  extra:: 4개의 도시가 있고 1번과 3번 도시를 방문한 상태라면 1010, 0번 도시만 방문한 상태라면 0001 이런식으로 2진수로 표시한다. 즉 모든 방문 경우의 수는 {{c1 $2^N$}}개 이다.
		  id:: 69ee0f02-5d43-43ae-b534-8674191ac58c
		- dp 배열 정의
			- dp[now][visited]의 뜻은? #card
			  id:: 69ee0fa3-05f4-4487-89b1-3188e26e8ed9
				- 현재 now 도시에 있고, 지금까지 방문한 도시 상태가 visited 일 때, 남은 도시들을 모두 방문하고 출발지로 돌아가는 최소비용.
		- 코드 #card
		  id:: 69ee100a-72a0-4698-8f71-539c0c28637d
			- id:: 69ee108e-5aba-41db-97d4-8ad64ce1cd1a
			  ```python
			  # 예시 입력: N = 4, W = 비용 행렬
			  # 0 10 15 20 / 5 0 9 10 / 6 13 0 12 / 8 8 9 0
			  N = 4
			  W = [[0, 10, 15, 20],
			       [5, 0, 9, 10],
			       [6, 13, 0, 12],
			       [8, 8, 9, 0]]
			  
			  INF = float("inf")
			  # dp 배열: 현재 노드 N개 x 방문 상태 2^N개
			  # 초기값은 계산되지 않았음을 의미하는 None이나 -1로 설정
			  dp = [[None] * (1 << N) for _ in range(N)]
			  
			  def dfs(now, visited):
			      if visited == (1 << N) - 1:
			          # 현재 도시에서 출발지(0번)로 가는 길이 있다면 그 비용 반환, 없으면 무한대 반환
			          if W[now][0] != 0:
			              return W[now][0]
			          else:
			              return INF
			              
			      # 2. 메모이제이션: 이미 계산된 상태라면 그대로 반환
			      if dp[now][visited] is not None:
			          return dp[now][visited]
			          
			      # 3. 최소 비용 계산
			      min_cost = INF
			      for next_city in range(N):
			          # 다음 도시로 가는 길이 없고(0) 거나, 이미 방문한 도시(비트마스크 확인)라면 패스
			          if W[now][next_city] == 0 or (visited & (1 << next_city)) != 0:
			              continue
			              
			          # 핵심 점화식: (now -> next_city 비용) + (next_city에서 남은 도시 방문하는 최소 비용)
			          # 재귀를 통해 '남은 도시 방문 비용'을 가져오고, visited 상태에 next_city를 비트 OR 연산으로 추가!
			          cost = W[now][next_city] + dfs(next_city, visited | (1 << next_city))
			          min_cost = min(min_cost, cost)
			          
			      dp[now][visited] = min_cost
			      return min_cost
			      
			  # 0번 도시에서 출발, 방문 상태는 0번 비트만 켠 상태(1)
			  print(dfs(0, 1))
			  ```