https://www.algospot.com/judge/problem/read/TSP1

 

algospot.com :: TSP1

Traveling Salesman Problem 1 문제 정보 문제 NP-Complete 문제의 가장 유명한 예 중 하나인 여행하는 외판원 문제 (Traveling Salesman Problem) 은, 여러 개의 도시와 그 도시 간의 거리가 주어졌을 때, 각 도시를

www.algospot.com

코드설명

완전탐색을 활용합니다.

 

여행하는 외판원 문제를 해결하는 재귀 호출 알고리즘입니다.

여행하는 외판원 문제는 알려졌듯이 모든 도시가 서로 연결되어 있는 상태에서 어떤 도시에서 시작하고, 어떤 경로로 가야 최단거리로 갈 수 있느냐를 구하는 문제입니다.

 

이 문제에서 유의해야할점 입니다.

1. 이 문제 역시 반환 타입을 double로 하며, 평소에 void로 하던 것보다 로직이 더 깔끔합니다.

급하게 풀다보면 void로 전역변수를 손쉽게 초기화하는 방식으로 진행하곤 하는데, 평소에 반환타입을 지키면서 푸는것이 코드가 훨씬 깔끔해지기에, 기분이 좋습니다.

 

이 문제의 시간복잡도는 N * (N-1)! 으로, N! 이 될 것 입니다.

시간복잡도는 O(N!)이 될 것 입니다.

 

만약 N = 10, 도시가 10개라면, 10 * 9! ( 도시의 경로를 선택하는 경우 ) 로 10! 이 될 것 입니다.

10! = 3,628,800 입니다.

 

현재까지는 괜찮지만, N이 더 커지면 시간초과가 발생할 것 이므로, 이후에 TSP를 더 발전시킨 방식으로 포스팅을 더 하겠습니다.

코드

package algorhythm;
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.StringTokenizer;
 
public class Main {
	public static int C, N, M;
	public static int answer = 0;
	public static double[][] dist;
	public static void main(String[] args) throws IOException{
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = new StringTokenizer(br.readLine());
		
		C = Integer.parseInt(st.nextToken());
		while(C-- > 0) {
			st = new StringTokenizer(br.readLine());
			N = Integer.parseInt(st.nextToken());
			dist = new double[N][N];
			
			for(int i=0;i<N;i++) {
				st = new StringTokenizer(br.readLine());
				for(int j=0;j<N;j++) {
					dist[i][j] = Double.parseDouble(st.nextToken());
//					System.out.println(dist[i][j]);
				}
			}
			double answer = Double.MAX_VALUE;
			for(int startCityIdx = 0; startCityIdx < N; startCityIdx++) {
				boolean[] visited = new boolean[N];
				visited[startCityIdx] = true;
				answer = Math.min(answer, shortestPath(new ArrayList<Integer>(Arrays.asList(startCityIdx)), visited, 0));
			}
			System.out.println(String.format("%.10f", answer));
		}
		
	}
	
	//path : 지금까지 만든 경로
	//visited : 각 도시의 방문 여부
	//currentLength : 지금까지 만든 경로의 길이
	//나머지 도시들을 모두 방문하는 경로들 중 가장 짧은 것의 길이를 반환한다.
	public static double shortestPath(ArrayList<Integer> path, boolean[] visited, double currentLength) {
		//기저 사례 : 모든 도시를 다 방문했을 때는 시작도시로 돌아가고 종료한다.
		if(path.size() == N) {
//			return currentLength + dist[path.get(0)][path.get(path.size() - 1)];
			return currentLength;
		}
		double ret = Double.MAX_VALUE; //매우 큰 값으로 초기화
		//다음 방문할 도시를 전부 시도해 본다.
		for(int next = 0; next < N; next++) {
			if(visited[next] == true) continue;
			int here = path.get(path.size() - 1);
			path.add(next);
			visited[next] = true;
			//나머지 경로를 재귀 호출을 통해 완성하고 가장 짧은 경로의 길이를 얻는다.
			double cand = shortestPath(path, visited, currentLength + dist[here][next]);
			ret = Math.min(ret, cand);
			visited[next] = false;
			path.remove(path.size() - 1);
		}
		return ret;
	}
	
}

 

 

코드2. (전역변수를 사용한 코드, ArrayList 사용 대신 인자로 현재 위치를 보낸다.)

가장 큰 차이점은 첫번째 코드는 인자로 currentLength를 모두 더해가며 인자로 계산하는 반면에

저는 상위 재귀호출 함수에서 하위재귀호출이 끝나면 더해가는 방식으로 구현했습니다. 사실 path ArrayList로 visited와 currentLength를 모두 구할 수 있습니다만, 연산속도를 줄이기 위해 (효율적으로 검색하기 위해) 함께 인자로 보냅니다.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;
 
public class Main {
	public static int C, N, M, H, W;
	public static int answer = Integer.MAX_VALUE;
	public static double[][] matrix;
	public static int[] visited;
	public static void main(String[] args) throws IOException{
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = new StringTokenizer(br.readLine());
		
		C = Integer.parseInt(st.nextToken());
		
		while(C-- > 0) {
			st = new StringTokenizer(br.readLine());
			N = Integer.parseInt(st.nextToken());
			matrix = new double[N][N];
			
			for(int i=0;i<N;i++) {
				st = new StringTokenizer(br.readLine());
				for(int j=0;j<N;j++) {
					matrix[i][j] = Double.parseDouble(st.nextToken());
				}
			}
			double answer = Double.MAX_VALUE;
			//시작점은 모든 시작점에서 최저거리를 찾을 것 이다.
			for(int i=0;i<N;i++) {
				visited = new int[N];
				Arrays.fill(visited, -1);
				visited[i] = 1;
				answer = Math.min(answer, getShortestPath(0, i));
			}
			System.out.println(String.format("%.10f", answer));
			
		}
		
	}
	
	public static double getShortestPath(int level, int cur) {
		//처음에 level == N으로 했다가 마지막 return ret = Double.MAX_VALUE이기에 계속해서 최대값이 출력되었었다. 이러한 것들이 사소해보이더라도 모두 로직에 관여한다.
		if(level == N - 1) { // N-1, 즉 시작점을 제외하고 모든 노드를 방문한다면, return 0. 
 
			return 0;
		}
		
		double ret = Double.MAX_VALUE;
		for(int next=0; next<N; next++) {
			if(cur == next) continue;
			if(visited[next] == -1) {
				visited[next] = 1;
				ret = Math.min(ret, getShortestPath(level + 1, next) + matrix[cur][next] ) ;
				visited[next] = -1; //-1로 바꿔줍니다. 처음에 0 으로 했다가 실수.
			}
		}
		
		return ret;
	}
	
}

 

+ Recent posts