Closed Jewan1120 closed 1 week ago
O(N*2^N)
비트마스킹
이용dp[fm][msk]
: 현재 fm도시에서 msk만큼 도시들을 방문한 상태일때, 방문하지 않은 나머지 도시를 돌아 처음도시로 갈때의 최소값.msk
FULLBIT = (1 << N) - 1;
로 모든 도시 N개를 방문한 값의 크기로 선언시작점 고정
가능dfs
dp[fm][msk] = INF
후 진행
if ((msk & (1 << to)) != 0)
(msk & (1 << to)) == 1)
은 안됨!!!msk | (1 << to)
DP + 비트마스크
-> 16 2^16 => `O(N 2 ^N)` 가능가장 적은 비용을 들이는 외판원의 순회 여행 경로를 구하는 프로그램
각 도시의 방문 상태가 리스트로 표현됨 -> 변수 1개로 표현 -> bit(이진수 표현)
점화식
D[c][v] = Math.min(D[c][v], D[i][v | (1<<i)] + W[c][i])
D[c][v] : 현재 도시(c)가 현재까지 방문한 모든 리스트가 v일 때 앞으로 남은 모든 도시를 경유하는데 필요한 최소 비용
D[i][v | (1<<i)] : i번째 도시 방문 저장
W[c][i] : 현재 도시(c) ~ 다음 도시(i)로 가기 위한 비용
public static int tsp(int c, int v) {
if(v==(1<<N)-1){ // 모든 노드를 방문할 경우
return W[c][0] == 0 ? INF : W[c][0]; // 시작 도시(0)로 돌아감 (돌아갈 수 없으면 INF 반환)
}
if(d[c][v] !=0){ // 이미 방문한 노드라면
return d[c][v]; // 바로 반환
}
int min_val = INF;
for(int i=0; i<N; i++){
if((v&(1<<i))==0 && W[c][i] !=0){ // 방문한 적 없고 갈 수 있는 노드라면
min_val = Math.min(min_val, tsp(i, (v | (1 << i))) + W[c][i]); // 현재도시(c) -> 다음 도시(i) 최소 비용 계산
}
}
d[c][v] = min_val;
return d[c][v];
}
1) 완전탐색으로 모든 도시 탐색하는 경우 => N! == 16! : 10억 -> 시간초과
2) DP, 비트마스킹 활용 => 도시 방문 : O(N N) => 방문처리 : O(2^N) => O(NN * 2^N) 가능
<<
연산: 비트를 왼쪽으로 이동 예) (1<<5) == 100000(2) == 32(10)
1) 최대 크기 정하기
visited
: 방문처리 비트마스킹 변수visited | (1 << i)
: i번째 노드를 방문처리하기visited & (1 << i)
: i번쨰 노드를 방문하지 않았다면 0 (0이상은 방문한 노드임)// now: 현재 탐색 노드
// visited: 방문한 노드 체크하는 비트마스킹 변수
1. 0번쨰 노드부터 시작 : now=0, visited = 1(10)
2. 모든 노드를 방문했다면 w[now][0]을 반환하고 종료
3. dp[now][visited]가 이미 방문한 곳이라면 해당 값 반환
4. 그렇지 않다면 now에서 다음 방문할 노드 찾음
5. 방문하지 않고 갈 수 있는 i 번째 노드가 있다면 방문
`dp[now][visited] = min(dp[now][visited], dfs(i + next) + w[now][i])`
- 이때, next = viisted | (1<< i) : i를 방문처리한 비트마스킹
INF 값을 Ingteger.MAX_VALUE 로 하니깐 오버플로우나서 헤맸습니다,,,ㅎㅎㅎㅎ
O(n * 2^n)
dp[now][visitedCities]
now
: 현재 위치하고 있는 도시 번호visitedCities
: 방문한 도시들 번호를 이진수로 변환해서 or
한 결과값
visitedCities
에 대한 정보를 정수를 통해 나타내면 boolean
배열을 사용하지 않아도 효율적으로 방문 처리를 할 수 있음int[][] W
: 도시를 이동하는데 걸리는 비용 정보int[][] dp
dp[i][j]
값은 모두 -1
로 초기화INF
로 초기화할 경우, 한 지점에서 다른 지점을 가지 못하는 경우 방문 여부를 확인해줄 수 없음 (갈 수 없어서 INF
인지 방문하지 않아서 INF
인지 구분 불가)dp[now][visitedCities]
: 현재 now
위치에 있을 있으며, visitedCities
에 해당되는 도시들을 다녔을 때 비용의 최솟값 visitedCities
사용 예시 (N
: 5일 경우)00001(2)
: 1번 도시 방문00010(2)
: 2번 도시 방문00100(2)
: 3번 도시 방문01000(2)
: 4번 도시 방문01101(2)
: 1, 3, 4 도시 방문(십진수 변환: 13)11111(2)
: 모든 도시 방문(1 << 5 - 1
)외판원 순회 문제 꼭 풀어봐야지,,, 하다가 일년이 넘은거 같은데,, 이제야 풀어보네요!!! 좋습니다👍 그리고 이 블로그에 비트마스킹 정리가 아주 잘 되어 있더라구요! 이거 보면 비트마스킹에 대한 내용에 도움 많이 될 것 같습니다!
외판원 순회 알고리즘
외판원 알고리즘
→ DFS
의 일종DP
의 메모이제이션을 이용해서 모든 경로를 다시 체크하는 것이 아닌 계산된 값을 가져와서 사용DP
배열 초기화: -1로 초기화 하여 갱신되지 않았음을 표시
dp = new int[n][1 << n];
for (int i = 0; i < n; i++)
Arrays.fill(dp[i], -1);
city → nextCity
로 가는 가중치를 구해주며 최솟값 찾기비트마스킹으로 어떤 도시를 방문했는지 확인하며 해당 도시에서 가지는 방문값(비트마스킹)을 최소로 갱신
// 외판원 순회
// @param city 현재 도시
// @param visited 비트마스킹 방문처리
private static int tsp(int city, int visited) {
// 모든 도시를 방문했다면
if (visited == (1 << n) - 1)
// 원래 도시로 돌아가는 거리를 반환
return distance[city][0];
// 미리 계산된 최솟값이 있다면 반환
if (dp[city][visited] != -1)
return dp[city][visited];
int result = INF; // 해당 도시를 방문했을 때 최솟값을 구하기 위한 변수
for (int nextCity = 0; nextCity < n; nextCity++) {
// 방문한 도시라면 건너뜀
if ((visited & (1 << nextCity)) != 0)
continue;
// 다음 도시를 방문
int temp = distance[city][nextCity] + tsp(nextCity, visited | (1 << nextCity));
// 해당 도시에서 가질 수 있는 최솟값 갱신
result = Math.min(result, temp);
}
// 계산된 값으로 메모이제이션
dp[city][visited] = result;
return result;
}
방문처리에 비트연산은 생각보다 많이 쓰인답니돠!
2 ≤ N ≤ 16
출발점 부터 모든 N개의 도시를 재귀적으로 탐색하면서 방문 가능한 모든 도시를 순회하므로 O(N*2^N)
시간복잡도 소요DFS 탐색 현재 지점에서 다음 지점까지의 최단거리를 찾으며 다시 처음 도시로 돌아와야 한다 (단순 DFS 만으로는 마지막 도시 > 처음도시로 순환 불가)
아무곳에서 출발해도 모든 구간을 순회하고 다시 출발지점으로 돌아와야 하는데, 어느곳에서 시작하든 출발 지점 > 모든 도시 순회 > 출발지점
의 비용은 동일하다
DP DFS 로 탐색하면서 중복된 순서로 도시을 순회하는 경우가 생기는데, 똑같은 연산을 할 필요가 없기 때문에 메모리제이션을 통해 이미 계산된 값을 가져온다
2^N
개로 1<<N
만큼의 dp 배열을 가지고 1,0 로 각 도시에 대한 방문체크를 한다비트마스킹
방문한 도시는 1
, 방문하지 않은 도시는 0
으로 표현
// 방문 처리: visited 의 next 번째의 자릿수에 1로 방문 표시
visited | (1<<next)
// 방문 유무 확인: visited 의 next 번째의 자릿수가 1 이라면 십진수로 변환 시 0 이상이 됨
visited & (1<<next) > 0