컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
algo · 20

P vs NP / NP-완전

이론복잡도NP

P와 NP는 결정 문제(예/아니오로 답하는 문제)를 풀거나 검증하는 데 드는 시간으로 문제를 나눈 복잡도 클래스입니다. P는 다항 시간에 풀 수 있는 문제의 집합이고, NP는 답이 주어졌을 때 그 답이 맞는지 다항 시간에 검증할 수 있는 문제의 집합입니다. 풀 수 있으면 검증도 할 수 있으므로 P는 NP에 들어갑니다.

NP-완전(NP-complete)은 NP에 속하면서 NP의 모든 문제가 다항 시간에 환원되는, NP에서 가장 어려운 문제들입니다. 하나라도 다항 시간에 풀면 NP 전체가 풀려 P = NP가 되는데, 이 둘이 같은지는 아직 증명되지 않았습니다. 이 토픽에서는 찾기와 검증의 비용 차이, 환원이 문제들을 잇는 모습, 클래스 사이의 포함 관계를 확인합니다.

핵심 개념

P

입력 크기 n의 다항식 시간 안에 풀 수 있는 결정 문제의 집합입니다. "배열이 정렬되어 있는가", "거리가 k 이하인 경로가 있는가(음수 사이클이 없는 그래프)" 같은 결정 문제가 속합니다.

핵심 개념

NP

답(인증서)이 주어졌을 때 다항 시간에 맞는지 확인할 수 있는 결정 문제의 집합입니다. 답을 찾는 일이 어렵더라도 확인이 쉬우면 NP입니다.

핵심 개념

인증서와 검증

인증서는 "예"라는 답이 맞다는 증거입니다. 부분집합 합에서는 합이 목표가 되는 부분집합 자체가 인증서이고, 더해 보면 검증됩니다.

핵심 개념

다항 시간 환원

A ≤p B는 A의 입력을 다항 시간에 B의 입력으로 바꿔 B의 답이 A의 답이 되게 하는 변환입니다. B가 쉬우면 A도 쉽습니다.

핵심 개념

NP-난해와 NP-완전

NP-난해는 NP의 모든 문제가 환원되는 문제이고, NP에도 속하면 NP-완전입니다. NP-난해 문제는 NP 밖에 있을 수도 있습니다.

핵심 개념

P = NP 문제

P와 NP가 같은 집합인지를 묻는 문제입니다. 증명되지 않은 열린 문제이며 NP-완전 문제 하나가 다항 시간에 풀리면 P = NP가 됩니다.

NP는 "비다항"이 아닙니다 — NP는 비결정적 다항 시간(Nondeterministic Polynomial time)의 약자입니다. 다항 시간에 풀리지 않는다는 뜻이 아니라 검증이 다항 시간이라는 뜻입니다.
NP-완전은 가장 어려운 NP입니다 — 어떤 NP 문제든 다항 시간에 NP-완전 문제로 바꿀 수 있어서, NP-완전 문제는 NP 안에서 가장 어려운 쪽에 있습니다.
환원은 방향이 중요합니다 — 새 문제 B가 NP-완전임을 보이려면 이미 NP-완전인 A를 B로 환원하고 B가 NP에 속함을 보입니다. 거꾸로 환원하면 증명이 되지 않습니다.
NP-완전이라고 포기하지는 않습니다 — 정확한 다항 시간 알고리즘을 기대하기 어렵다는 뜻이므로, 입력이 작을 때의 완전 탐색, 다항 시간에 풀리는 특수한 경우, 근사 알고리즘을 씁니다.
이 시각화에서 확인할 것 — "찾기와 검증" 탭에서 부분집합을 21개 확인한 끝에 {4, 5}를 찾는 일과 덧셈 한 번으로 검증하는 일의 차이를, "환원" 탭에서 CIRCUIT-SAT에서 시작해 NP-완전이 퍼지는 화살표를, "P · NP · NP-완전" 탭에서 P ≠ NP일 때와 P = NP일 때 영역이 어떻게 달라지는지를 STEP으로 확인하세요.

NP-완전 이론의 배경

1971년 스티븐 쿡(Stephen Cook)은 "The Complexity of Theorem-Proving Procedures"에서 논리식의 만족 가능성 문제(SAT)가 NP의 모든 문제가 환원되는 문제임을 보였습니다. 레오니드 레빈(Leonid Levin)도 독립적으로 비슷한 결과를 얻었습니다. 이듬해 리처드 카프(Richard Karp)는 "Reducibility Among Combinatorial Problems"에서 환원으로 SAT를 포함한 21개의 조합 문제가 NP-완전임을 보였습니다.

P = NP 문제는 클레이 수학연구소(Clay Mathematics Institute)가 2000년에 발표한 밀레니엄 문제 7개 중 하나이고, 해결에는 100만 달러의 상금이 걸려 있습니다. 가렛(Garey)과 존슨(Johnson)의 책 "Computers and Intractability"는 NP-완전 문제를 모아 정리한 대표적인 참고서입니다.

숫자로 확인하기

부분집합 합 찾기 — 집합 {3, 34, 4, 12, 5, 2}에서 합 9를 만드는 부분집합은 {4, 5}와 {3, 4, 2}입니다. 비트마스크 순서로 확인하면 21번째에서 {4, 5}가 나옵니다.
검증은 덧셈 한 번 — 인증서 {4, 5}는 4 + 5 = 9로 덧셈 한 번에 확인되고, {3, 34}는 37이라 거짓입니다.
"없음"은 모두 확인 — 합이 13인 부분집합은 없어서 부분집합 64개를 모두 확인해야 없다는 결론이 납니다.
2의 거듭제곱 — 원소가 6개이면 부분집합 64개, 20개이면 1,048,576개, 40개이면 1,099,511,627,776개입니다.
환원 사슬 — 교재의 환원 순서는 CIRCUIT-SAT에서 SAT, 3-CNF-SAT를 거쳐 CLIQUE, VERTEX-COVER, HAM-CYCLE, TSP로 이어지고 3-CNF-SAT에서 SUBSET-SUM으로 갈라집니다. 시각화는 이 8개 문제를 차례로 보여 줍니다.
코드 확인 — 파이썬과 자바가 같은 입력에서 [4, 5]를 21번째 확인에 찾고 합 13에서는 64개를 모두 확인합니다. 파이썬의 환원 예에서는 클리크 {0, 1, 2}에 대응하는 {3, 4}가 보완 그래프의 정점 덮개로 확인됩니다.

자주 하는 오해

NP는 Non-Polynomial의 약자다 — 비결정적 다항 시간의 약자입니다. P도 NP에 포함되므로 NP 문제가 모두 어렵다는 뜻이 아닙니다.
NP-난해와 NP-완전은 같다 — NP-완전은 NP에 속하는 NP-난해 문제입니다. NP-난해 문제 중에는 NP에 속하지 않는 것도 있습니다.
NP-완전 문제는 풀 수 없다 — 풀 수는 있습니다. 모든 부분집합을 확인하는 방법으로도 정답은 나오며, 다만 입력이 커지면 시간이 지수적으로 늘어납니다. 다항 시간 알고리즘이 있는지가 열린 문제입니다.
P ≠ NP가 증명되었다 — 아직 어느 쪽도 증명되지 않았습니다. 많은 연구자가 P ≠ NP를 예상할 뿐입니다.
검증이 쉬우니 풀기도 쉽다 — 검증이 다항 시간이어도 답을 찾는 데는 부분집합을 모두 확인하는 지수 시간밖에 모르는 문제가 많습니다. 이 차이가 P와 NP를 가르는 질문입니다.

면접에서 이렇게 물어요

면접 질문

P와 NP의 차이는?

P는 다항 시간에 풀 수 있는 결정 문제, NP는 답이 주어지면 다항 시간에 검증할 수 있는 결정 문제입니다. P는 NP에 포함됩니다.

면접 질문

NP-완전이란?

NP에 속하면서 NP의 모든 문제가 다항 시간에 환원되는 문제입니다. NP에서 가장 어려운 문제들입니다.

면접 질문

NP-완전 문제 하나를 다항 시간에 풀면?

NP의 모든 문제가 환원을 통해 다항 시간에 풀리므로 P = NP가 됩니다.

면접 질문

NP-완전 문제를 만나면 어떻게 하나요?

정확한 다항 시간 해법을 기대하기 어려우므로 입력이 작으면 완전 탐색, 다항 시간에 풀리는 특수한 경우의 활용, 근사 알고리즘을 고려합니다.

활용 분야

문제의 어려움 판단 — 알려진 NP-완전 문제를 새 문제로 환원할 수 있음을 보이면 새 문제도 NP-난해이므로, 다항 시간 알고리즘을 찾아 헤매기보다 근사나 작은 입력용 알고리즘으로 방향을 바꿀 근거가 됩니다.
최적화 문제의 모델 — 외판원 문제(TSP), 색 3가지 이상의 그래프 색칠, 0-1 배낭 문제의 결정 버전이 NP-완전이며, 스케줄링이나 경로 계획에 나오는 많은 문제가 이런 문제와 닮은 구조를 가집니다.
SAT 풀이기 — SAT는 NP-완전이지만, 소프트웨어 검증 같은 많은 실제 응용 입력에서는 효율적인 SAT 풀이기가 개발되어 있어 문제를 SAT로 바꿔 푸는 접근도 쓰입니다. 효율은 입력의 성격에 따라 다릅니다.

함께 보면 좋은 토픽

시간 복잡도 — 다항 시간과 지수 시간의 차이를 봅니다.
백트래킹 — 지수 탐색 공간을 가지치기로 줄이는 방법을 봅니다.
그래프 색칠 — 대표적인 NP-난해 문제를 봅니다.
그리디 — 최적을 보장하지 못해도 빠르게 근사해를 얻는 방법을 봅니다.

참고 자료

논문 — Cook, S. A., "The Complexity of Theorem-Proving Procedures", Proceedings of the 3rd ACM Symposium on Theory of Computing (STOC), 1971
논문 — Karp, R. M., "Reducibility Among Combinatorial Problems", in Complexity of Computer Computations, 1972
교재 — Cormen, Leiserson, Rivest, Stein, "Introduction to Algorithms", 3rd ed., Chapter 34 NP-Completeness
교재 — Garey, M. R., Johnson, D. S., "Computers and Intractability: A Guide to the Theory of NP-Completeness", 1979

Python — 부분집합 합: 찾기와 검증

from collections import Counter

def verify(nums, target, cert):                    # 인증서 검증: 다항 시간
    return not (Counter(cert) - Counter(nums)) and sum(cert) == target

def find(nums, target):                            # 답 찾기: 부분집합 2^n개를 확인
    n = len(nums)
    for mask in range(1 << n):
        sub = [nums[i] for i in range(n) if mask >> i & 1]
        if sum(sub) == target:
            return sub, mask + 1                  # (부분집합, 확인한 개수)
    return None, 1 << n

nums = [3, 34, 4, 12, 5, 2]
print(find(nums, 9))                           # ([4, 5], 21)
print(verify(nums, 9, [4, 5]), verify(nums, 9, [3, 34]))   # True False
print(find(nums, 13))                          # (None, 64)

NP의 정의를 코드로 옮긴 예입니다. NP는 답(인증서)이 주어졌을 때 그 답이 맞는지 다항 시간에 검증할 수 있는 결정 문제의 집합입니다. verify는 인증서가 nums의 원소로만 이루어졌는지 확인하고 합을 목표와 비교하는 것이 전부라 입력 크기의 다항식 시간이면 됩니다. find는 답을 직접 찾는 쪽으로, 원소가 n개이면 부분집합 2^n개를 하나씩 확인합니다(부분집합 하나를 만드는 O(n) 비용은 생략한 단순화입니다). 시각화의 순서로는 21번째에서 [4, 5]를 찾지만, 합이 13인 부분집합은 없어서 64개를 모두 확인하고서야 없다는 결론이 납니다. 여기서의 find는 가장 단순한 완전 탐색이며, 부분집합 합이 다항 시간에 풀리는지는 알려져 있지 않습니다. 시각화의 찾기와 검증 탭이 보여 주는 과정과 같습니다.

Python — 환원: 클리크를 정점 덮개로 바꾸기

from itertools import combinations

V = {0, 1, 2, 3, 4}
E = {(0, 1), (0, 2), (1, 2), (2, 3), (3, 4)}

def is_clique(E, C):
    return all((a, b) in E or (b, a) in E for a, b in combinations(sorted(C), 2))

def complement(V, E):                          # 입력 변환: 보완 그래프를 만든다
    return {(a, b) for a, b in combinations(sorted(V), 2) if (a, b) not in E and (b, a) not in E}

def is_vertex_cover(E, S):
    return all(a in S or b in S for a, b in E)

Ec = complement(V, E)
C = {0, 1, 2}                                    # 크기 3인 클리크
print(is_clique(E, C), sorted(Ec), is_vertex_cover(Ec, V - C), len(V) - len(C))
# True [(0, 3), (0, 4), (1, 3), (1, 4), (2, 4)] True 2
print(is_clique(E, {2, 3, 4}), is_vertex_cover(Ec, V - {2, 3, 4}))   # False False

환원 A ≤p B는 A의 입력을 다항 시간에 B의 입력으로 바꾸어 B의 답이 곧 A의 답이 되게 하는 변환입니다. 여기서는 클리크를 정점 덮개로 바꿉니다. 그래프 G에서 정점 집합 C가 클리크라는 것은 보완 그래프(G에 없는 간선을 모은 그래프)에서 V - C가 정점 덮개라는 것과 같습니다. 5개 정점 그래프에서 C = {0, 1, 2}는 클리크이고 V - C = {3, 4}는 보완 그래프의 모든 간선을 덮으며, 크기도 |V| - |C| = 2입니다. {2, 3, 4}는 클리크가 아니고 V - C도 덮개가 아닙니다. 보완 그래프를 만드는 일은 정점 쌍을 한 번씩 보는 다항 시간이라 이 변환은 다항 시간입니다. 어느 한쪽이 다항 시간에 풀리면 다른 쪽도 풀리므로, 한쪽이 NP-완전이면 다른 쪽이 NP에 속하는 한 같이 NP-완전이 됩니다. 시각화의 환원 탭에서 클리크 다음에 정점 덮개가 나오는 화살표가 이 변환입니다.

Java — 부분집합 합 찾기와 검증

static boolean verify(int[] nums, int target, int[] cert) {
    List<Integer> pool = new ArrayList<>();
    for (int x : nums) pool.add(x);
    int sum = 0;
    for (int c : cert) {
        if (!pool.remove((Integer) c)) return false;   // 집합에 없는 원소(또는 중복 사용)
        sum += c;
    }
    return sum == target;
}

static int[] find(int[] nums, int target, int[] checked) {
    int n = nums.length;
    for (int mask = 0; mask < (1 << n); mask++) {
        int sum = 0;
        for (int i = 0; i < n; i++) if ((mask >> i & 1) == 1) sum += nums[i];
        checked[0]++;
        if (sum == target) return pick(nums, mask);
    }
    return null;
}

static int[] pick(int[] nums, int mask) {          // 켜진 비트의 원소만 모은다
    return IntStream.range(0, nums.length).filter(i -> (mask >> i & 1) == 1).map(i -> nums[i]).toArray();
}

// nums = {3, 34, 4, 12, 5, 2}: find(target 9) → [4, 5] (확인 21번), verify(nums, 9, {4, 5}) → true
// find(target 13) → null (확인 64번)

같은 구조를 자바로 쓴 것입니다. 비트마스크의 각 비트가 원소를 넣을지 말지를 뜻하므로 1 << n개의 부분집합을 차례로 만들 수 있습니다. pick은 켜진 비트에 해당하는 원소를 배열로 모으는 보조 함수입니다. 검증 verify는 인증서 원소를 풀에서 하나씩 빼며 더해 비교하는 반복문이고, 찾기 find는 그 앞에 부분집합 개수만큼의 바깥 반복이 있습니다. n이 커질수록 find의 비용은 2^n으로 빠르게 늘지만 verify는 인증서 크기에 비례합니다. 이 차이가 NP 문제의 특징이며, 실무에서 NP-완전 문제를 만나면 근사 알고리즘, 휴리스틱, 입력이 작을 때의 완전 탐색 같은 방법을 고려합니다.

Q1

NP(Nondeterministic Polynomial) 문제의 정의는?

"풀기"가 아니라 "검증하기"가 다항 시간입니다.
NP는 어떤 답이 주어졌을 때 그 답이 정답인지 다항 시간 안에 확인할 수 있는 결정 문제들의 집합입니다. 답을 찾는 것이 어렵더라도 검증이 쉬우면 NP에 속합니다.

Q2

NP-완전(NP-Complete) 문제란?

"NP 안에서 가장 어려운 문제들"입니다.
NP-완전 문제는 NP에 속하면서, 모든 NP 문제가 다항 시간 환원으로 이 문제로 바뀔 수 있는 문제입니다. SAT, 외판원 문제의 결정 버전 등이 대표적입니다.

Q3

어떤 NP-완전 문제 하나를 다항 시간에 풀 수 있음이 증명된다면 의미하는 것은?

모든 NP 문제가 그 문제로 환원될 수 있다는 점을 떠올려 보세요.
모든 NP 문제는 NP-완전 문제로 다항 시간에 환원됩니다. 따라서 NP-완전 문제 하나라도 다항 시간에 풀리면 모든 NP 문제가 다항 시간에 풀려 P = NP가 됩니다.