티스토리 뷰
이번이 드디어 알고리즘 마지막 시간이다. 마지막 답게 아직 풀리지 않고 있는 알고리즘에 대해 소개하는 시간을 갖겠다. 근사 알고리즘이라는 말에서도 알 수 있듯이 해당 알고리즘들은 아직 풀리지 않고 있기에 정확한 해를 구하지 못해 최대한 근사한 해를 구하는 방법을 알아내는 것이다. 수학 혹은 컴퓨터공학에서 가장 유명한 밀레니엄 문제 중 하나 인 P-NP 문제에 대해 소개하고 대표적인 NP 문제에 대한 코딩을 해보며 마지막 알고리즘 포스팅을 마쳐볼까 한다.
필자가 어릴적에 했던 재밌는 게임 중에서 '한붓그리기' 게임이 있었다. 그 게임은 간선과 정점으로 이루어진 그래프가 주어졌을 때, 각각의 간선을 딱 한번씩만 방문해서 모든 정점을 방문 할 수 있는 경로를 찾는 게임이었다. 이를 이산수학에서 오일러 회로 (Euler Circle)라고 한다. 한붓그리기 문제를 풀기 위해서는 모든 정점을 시작점으로 잡고 주위에 있는 경로들을 탐색해가며 모든 정점을 방문 할 수 있는지 찾아내야 한다. 만약 정점의 개수가 적은 숫자라면 쉽게 풀 수 있겠지만, 만약 정점의 개수가 100개라고 생각해보면, 해당 문제를 쉽게 풀 수 없다. 만약 우리가 풀고자 하는 문제가 다항식 시간으로 떨어지게 되는 문제를 우리는 P(Polynomial) 문제라고 하고, 다항식 시간안에 해결 할 수 없는 한붓그리기 같은 문제를 NP(Non-deterministic Polynomial) 문제라고 한다.
1. P-NP 문제
2. NP-완전 문제
NP 문제는 해를 찾는 방법이 아닌 해를 추측하는 방식이다. 무슨 말인가 하면 위의 NP 문제를 살펴보자. NP 문제를 풀기 위해서 우리는 2가지 단계를 거치게 된다. 첫번째는 해를 추측하기 위해 집합 A의 부분집합 하나를 찾아내야 한다. 여기서 추측을 하는 이유는 해당 문제를 다항식 시간안에 풀어낼 수 없기 때문에 하나의 해를 임의로 정하는 것이다. 하나의 해를 정했다면 문제를 검산하는 작업은 어렵지 않다. 부분 집합을 모두 더하면 된다. 이 과정을 용어로 풀어서 설명하면 처음에 해를 찾아내는 것을 '추측 (Guessing)', 검산하는 작업을 '검증 (Verification)'이라고 한다. NP 문제는 이 두가지 과정을 통해 해를 구하게 된다. 부분집합의 해 뿐만 아니라 오일러 회로 문제에도 똑같이 대입해서 문제를 풀면 된다. 회로 하나를 '추측' 하고 이 경로가 한 붓 그리기가 가능한지 '검증'한다.
위의 설명처럼 다항식 시간안에 구할 수 없는 문제들을 다른 다항시간안에 풀수 있는 다른 문제로 환원해서 푸는 문제들을 우리는 특별하게 NP-난해(NP-Hard) 문제라고 한다. 다항식 시간안에 도저히 풀 수 없기에 난해라는 이름을 붙였고, 이런 문제들을 위 처럼 추측과 검증 과정을 통해서만 결정 해를 구할 수 있다. 그리고, NP문제 이면서 NP-난해 문제를 우리는 NP-완전 문제(NP-complete) 라고 한다. 즉, P-NP 문제는 'NP-완전 문제가 P문제가 맞느냐 아니냐'를 증명하는 것이다.
NP-완전 문제의 가장 큰 특징은 하나의 문제를 다항식 시간 안에 풀 수 있으면 모든 문제를 다항식 시간안에 풀 수 있게 된다. NP문제 같은 경우 NP-완전 문제는 우리가 다항식 시간안에 풀 수 없는 문제를 다항식 시간 안에 풀 수 있도록 변환하는 것이다. 따라서 A 문제를 다항식 문제로 풀기 위해 입력 변환을 하고 새로운 알고리즘을 사용하고 출력 변환 작업을 거쳐 해를 구하게 된다. 부분 집합의 해를 구하는 문제로 설명해보자.
<부분 집합의 해>
부분 집합의 해(NP) -> 입력 변환 : 분할 작업(P) -> 합계 계산 -> 출력 변환 : 정답 여부 판단 -> NP 문제 해결
아까 설명 했던 추측과 검증 과정을 다르게 설명하면 위와 같을 것이다. 우리는 NP 문제를 다항식 시간안에 다른 문제로 환원해 문제를 해결 한다. 이 구조에서 결국 합계 계산 부분이 다항식 시간 안에 해결 된다면 이 문제는 더이상 NP 문제가 아니게 된다. 결국 하나의 NP 문제에 대해 다항식 알고리즘을 구한다면, 입력 변환 시간이 다항식 시간에 가능하므로 모든 NP 문제가 다항식 시간 안에 풀 수 있게 되는 것이다.
아직 증명은 되지 못했으나 많은 수학자들이 직관적으로 P≠NP라고 생각하는 이유도 위와 같다. 하나의 NP 알고리즘을 다항식으로 해결하면 모든 NP 문제는 P문제가 되겠지만, 수많은 NP 문제들이 아직까지도 풀리지 않는 다는 것은 결국 NP가 P문제가 아니기 떄문이지 아닐까라고 추측한다. 물론, 일부 학자들이 위와 같은 의견에 반발할 수 도 있는 점은 분명하다. 어쩃든 위의 가정이 맞다는 전제하에 P 문제와 NP 문제의 다이어그램은 아래와 같이 표현된다.
3. 근사 알고리즘 (Approximation Algorithm)
- 분기 한정법 (Branch and Bound)
- 이분법(Bisection Method)
다항 방정식에서 우리가 구하고자 하는 루트해는 f(x) = 0 이되는 저 빨간 점이 될 것이다. 하지만, 3차 이상의 다차 함수 혹은 지수함수 등 기타 복잡한 함수들은 쉽게 루트해를 구하지 못하는 경우도 있다. 그렇기에 우리가 임의로 a, b를 설정하고 f(a)와 f(b)의 부호가 다르다면 범위를 점차 좁혀나가서 우리가 원하는 해를 구할 수 있다. 근사 알고리즘의 종료를 위해 우리는 허용 오차 혹은 최대 반복 횟수가 필요하다. 둘 중 하나를 설정해 우리가 좁힌 이분 거리의 길이가 허용 오차 이내라면 이분 법을 종료한다. 마찬가지로 횟수를 설정해 해당 횟수에 도달하면 이분법을 종료 시킬 수 도 있을 것이다.
매우 직관적이고 간단한 근사 알고리즘이지만 매우 느리다는 것이 단점이다. 만약 우리가 해를 알고 있다면 대입법을 사용해 쉽게 구한다. 1차 함수였다면 대입법으로, 2차 함수였다면 근의 공식으로, 삼각함수라면 우리가 알고있는 상식으로 너무나 쉽게 풀 수 있기 때문이다.
이분법의 Pseudo Code는 아래와 같다.
4. 해밀턴 경로 (Hamiltonian Cycle)
해밀턴 경로 역시 대표적인 NP 문제이다. 앞서 처음에 설명했던 한붓그리기와 비슷한데, 정점과 간선으로 이루어진 그래프 상에서 간선을 딱 한번씩만 이용해 모든 정점을 방문 해야한다. 단, 오일러 회로와는 차이점이 분명 존재하는데 오일러 회로는 모든 간선을 방문해야 하는 대신 정점의 경우 여러번 방문해도 상관없다. 하지만, 해밀턴 회로는 반드시 모든 간선을 지나야 할 필요는 없지만, 모든 정점을 딱 한번씩만 거쳐야한다. 아래 예시를 살펴보자.
오일러 회로 : 모든 간선을 방문해야 하나 간선을 한번만 방문하고 정점을 여러번 거쳐도 가능 하는 경로
해밀턴 경로 : 모든 정점을 방문해야 하나 정점을 한번만 방문하고 지나치지 않는 간선은 발생 할 수 있음
즉, 오일러는 간선 중심으로 도형을 보는 것이고 해밀턴 경로는 정점을 중심으로 도형을 보는 것이다.
해밀턴 경로 프로그램은 우리가 도형을 입력하면 해밀턴 경로가 존재하는 경우 해당 경로를 노출 시켜주고 그렇지 않으면 해밀턴 경로가 존재하지 않다는 메세지를 보내주어야 한다.
우선 Input에 대해 정의해보자. 해밀턴 경로를 찾으려는 그래프의 정점의 개수가 N개라고 생각 했을때, N X N 크기의 매트릭스로 표현 가능하다. 입력하려는 그래프의 모양과 이를 행렬로 표현하면 아래와 같다.
{ {0, 1, 0, 1, 0},
{1, 0, 1, 1, 1},
{0, 1, 0, 0, 1},
{1, 1, 0, 0, 1},
{0, 1, 1, 1, 0} }
이제 이 과정을 코드로 살펴보자.
<Solution.java>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import java.util.*; public class Solution { public static int size; public static boolean map[][]; public static void main(String[] args) { // TODO Auto-generated method stub // Input Scanner sc = new Scanner(System.in); System.out.println("Input the size:"); size = sc.nextInt(); map = new boolean[size][size]; int temp; System.out.println("Input the graph:"); for(int i=0;i<size;i++){ for(int j=0;j<size;j++){ temp = sc.nextInt(); if(temp==0) map[i][j]=false; if(temp==1) map[i][j]=true; } } //System.out.println(map.length); HamiltonCycle hamiltonCycle = new HamiltonCycle(); hamiltonCycle.findHamiltonian(map); } } | cs |
<HamiltonCycle.java>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | public class HamiltonCycle { public static int size; boolean isConnect(boolean map[][], int path[], int j, int pos) { if (!map[path[pos - 1]][j]) return false; for (int i = 0; i < pos; i++) if (path[i] == j) return false; return true; } public boolean findCycle(boolean[][] map, int[] path, int pos){ if(pos == size){ if(map[path[pos-1]][path[0]]) return true; else return false; } for(int j=1; j<size; j++){ if(isConnect(map, path, j, pos)){ path[pos] = j; if (findCycle(map, path, pos+1)) return true; path[pos] = -1; } } return false; } public void printCycle(int path[]) { System.out.println("Solution Exists: Following" + " is one Hamiltonian Cycle"); for (int i = 0; i < size; i++) System.out.print(" " + path[i] + " "); System.out.println(" " + path[0] + " "); } public void findHamiltonian(boolean[][] map){ size = map.length; int[] path = new int[map.length]; for (int i=0; i<size; i++) path[i]=-1; // backtracking을 위한 변수 path[0]=0; if(findCycle(map, path, 1)==false){ System.out.println("\nSolution does not exist"); System.exit(0); } printCycle(path); System.exit(0); } } | cs |
(참고 코드 : https://www.geeksforgeeks.org/hamiltonian-cycle-backtracking-6/)
'Skill > Programming - Basic' 카테고리의 다른 글
22. 허프만 부호화(Huffman Coding) (0) | 2019.01.25 |
---|---|
21. 트리 순회 (Tree Traversal) (0) | 2019.01.13 |
20. 이진 탐색 트리(Binary Search Tree) (0) | 2018.12.30 |
19. 최대 이분 매칭(Maximun Bipartite Matching) (0) | 2018.12.25 |
18. 깊이 우선 탐색 (DFS algorithm) (1) | 2018.11.25 |