Polynomial time

다항 시간

다항 시간이란 알고리즘의 실행 시간이 입력 크기 n에 대한 다항식으로 표현되는 경우를 말합니다. 이는 O(n^k ) (k는 양의 상수)와 같은 형태로 나타나는 시간 복잡도를 의미한다.

 

O(n), O(n²), O(n³ + n² + 3), O(log n), O(1) 등은 모두 다항 시간으로 분류되는데 O(log n), O(1)의 경우 엄밀히 말하면 다항식은 아니지만 다항 시간보다 훨씬 효율적이므로 넓은 의미에서 다항 시간 범주에 포함된다.

 

반면 O(2ⁿ), O(n!)과 같은 복잡도는 다항 n이 커질수록 기하급수적으로 실행시간이 폭증하여 다항 시간이 아니며, 이런 알고리즘은 대개 비실용적으로 간주한다.

 

어떤 문제에 대해 다항 시간 내에 항상 정답을 찾을 수 있는 알고리즘이 존재한다면, 그 문제는 Class P에 속한다. 

NP

비결정적 다항 시간(Nondeterministic Polynomial time)이란, 어떤 문제에 대해 정답이 주어졌을 때, 그 정답이 올바른지 다항 시간 내에 검증이 가능한 문제들을 말한다.

 

즉, 해를 찾는 과정은 어려울 수 있지만, 해가 주어졌을 때 그 해를 확인하는 것은 쉽고 빠르다.

 

예를 들어 아주 큰 숫자를 소인수분해하는 것은 시간이 오래 걸리는 어려운 문제이다. 하지만 누군가 소인수의 정답을 알려준다면, 그 답들을 곱해서 원래 숫자가 나오는지 확인하기만 하면 되며 이 곱셈 연산은 다항 시간 안에 완료할 수 있으므로 이런 문제를 NP에 속한다고 한다.

 

NP-complete

NP에 속하는 문제들 중에서 가장 어려운 문제들의 집합이다. 

- NP에 속한다 : 정답이 주어졌을 때 다항 시간 내에 검증이 가능하다.

- NP-환원성 : NP에 속하는 모든 다른 문제들을 다항 시간 내에 NP-complete 문제로 변환할 수 있다.

 

NP-환원성은 핵심적인 특징으로 이는 NP-complete 문제 중 하나라도 다항 시간 내에 해결할 수 있는 알고리즘을 찾는다면, 이론적으로 NP에 속하는 모든 문제도 다항 시간 내에 해결할 수 있게 된다는 의미를 가진다.

 

예) SAT 만족 가능성 문제, 3-CNF SAT, TSP(결정형), 그래프 색칠 문제

 

NP-hard

NP-complete보다 더 넓은 개념으로, NP의 모든 문제만큼 어렵거나 그 이상일 수 있는 문제들이다. NP-complete와 유사하게 NP에 속하는 모든 문제를 다항 시간 내에 자기 자신으로 환원할 수 있어야 하지만 반드시 NP에 속할 필요는 없다. 즉, 해가 주어져도 그 검증조차 다항 시간 안에 불가능할 수 있다. 주로 최적값 찾기 문제나 결정 불가능한 문제들이 NP-hard에 속한다.

 

예) TSP(최적 경로 길이 구하기), 체스 문제, 할터 문제(Halting Problem)

 

┌─── P (다항 시간에 풀 수 있는 문제들)

├─── NP (정답 검증이 다항 시간인 문제들)
│          │

│          └─ NP-complete (NP 중 가장 어려운 문제들)

└─── NP-hard (NP보다 어려울 수도 있는 문제들)
            │

            └─ NP-complete ⊆ NP-hard

 

 

P vs NP

밀레니엄 문제 중 하나로, P와 NP 사이의 관계는 아직도 해결되지 못한 문제이다.

 

- P : 다항 시간 내에 문제를 풀 수 있는 문제들의 집합이다.

- NP : 정답이 주어졌을 때, 그 정답이 다항 시간 내에 검증될 수 있는 문제들의 집합이다.

 

다항 시간 안에 해를 찾을 수 있는 문제라면 당연히 그 해가 맞는지도 다항 시간 안에 검증할 수 있기 때문에 P ⊆ NP는 항상 참이다. 진정한 문제는 P = NP인가 P ≠ NP인가에 대한 질문으로, 정답을 빠르게 검증할 수 있다면 답을 빠르게 구할 수도 있는가?이다.

728x90
반응형

'Coding > Algorithm' 카테고리의 다른 글

O(2^n) - 지수 시간  (1) 2025.07.12
NP - TSP  (1) 2025.07.11
O(n!) - 팩토리얼 시간  (2) 2025.07.08
빅-오(Big-O) 표기법  (1) 2025.07.08
시간 복잡도  (1) 2025.07.08

O(n!) - 팩토리얼 시간

주어진 배열의 원소들을 가능한 모든 순서로 배열하는 것을 순열 생성의 모든 경우의 수이다. 여기서 순열은 일반적인 순열인 직순열로 일직선상의 배열이다.

 

이러한 연산에서 n개 원소는 n! 개의 순열이 존재하고, 각 순열을 생성하는 데 걸리는 시간을 포함한 전체 시간 복잡도는 n! * n = O(n!)이다.

 

순열 생성

배열 [1, 2, 3]가 있을 때, 가능한 모든 순열은 다음과 같다.

 

[1, 2, 3], [1, 3, 2],

[2, 1, 3], [2, 3, 1],

[3, 1, 2], [3, 2, 1]

 

총 3! = 6가지 경우가 존재한다.

 

코드로 표현하면 다음과 같다.

def permutations(arr):
    if len(arr) <= 1:
        return [arr]
    result = []
    for i in range(len(arr)):
        # ':' 슬라이싱, :끝 : 처음부터 끝 인덱스 바로 전까지(끝 비포함), 시작: : 시작 인덱스부터 끝까지
        # => :i i인덱스를 제외한 그 앞의 집합, i+1: 다음 인덱스까지의 집합, 길이 초과하는 경우 빈 배열
        rest = arr[:i] + arr[i+1:] 
        for p in permutations(rest): # 
            result.append([arr[i]] + p) # append : 리스트 객체 맨 끝에 새로운 요소 추가
    return result

print(permutations([1, 2, 3]))

"""
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
"""


1. 하나의 인덱스를 고정한다.

2. 나머지 요소들의 순열을 재귀적으로 구한다.

3. 그 앞에 고정 요소를 붙여서 순열을 구성한다.

 

이 방식은 재귀 호출마다 n-1개의 원소로 순열을 생성하며, 전체 시간 복잡도는 O(n!)이다.

 

외판원 문제(TSP, Travelling Salesman Problem)

외판원이 n개의 도시를 모두 방문하고 출발점으로 돌아오는 최단 경로를 찾는 문제이다.

NP-hard 문제의 대표적인 예시로 자주 사용된다.

 

조건

- 각 도시는 정확히 한 번씩만 방문한다.

- 모든 도시 간의 거리가 주어진다.

 

이 문제는 다양한 해결법이 존재하지만 그중에서 브루트포스 접근법이 O(n!)의 시간 복잡도를 가진다.

 

브루트포스

4개 도시 A, B, C, D가 있을 때, 가능한 모든 경로를 확인한다.

A -> B -> C -> D -> A

A -> B -> D -> C -> A

...

 

다시 출발점으로 돌아오기 때문에 원순열로 취급할 수 있어 시작점을 A로 고정하고 모든 경로를 구한다.

나머지 n-1개 도시의 순열을 구한다. (n-1)!

4개 도시 -> (4 - 1)! = 6가지

 

import itertools

def tsp_bruteforce(distance):
    # distance : 2D 배열, distance[i][j] : 도시 i에서 j로의 거리
    n = len(distance)
    cities = list(range(1, n)) # 시작 도시(0번)를 고정한 상태에서 나머지 도시의 순열을 생성

    min_cost = float('inf')
    best_path = None

	# itertools.permutations : 순열을 생성해준다.
    # 내부적으로 최적화가 잘되어있어 메모리 효율이 좋다. 시간복잡도는 그대로 O(n!)
    for path in itertools.permutations(cities):
        current_cost = 0
        current_city =0

        for next_city in path:
            current_cost += distance[current_city][next_city]
            current_city = next_city

        current_cost += distance[current_city][0]

        if current_cost < min_cost:
            min_cost = current_cost
            best_path = [0] + list(path) + [0]

    return min_cost, best_path

distance = [
    [0, 10, 15, 20],
    [10, 0, 35, 25],
    [15, 35, 0, 30],
    [20, 25, 30, 0]
]

city_names = ["A", "B", "C", "D"]

cost, path = tsp_bruteforce(distance)
print(f"Min cost : {cost}")
print(f"Best path : {path}")

named_path = ' -> '.join(city_names[i] for i in path)
print(f"Best path : {named_path}")

"""
Min cost : 80
Best path : [0, 1, 3, 2, 0]
Best path : A -> B -> D -> C -> A
"""

 

순열생성

O((n-1)!), n-1개 도시의 모든 순열 생성 

 

각 경로 비용 계산

O(n), 각 순열마다 n개 도시를 거쳐가는 비용 계산

 

전체 시간복잡도

O(n * (n-1)!) = O(n!)

 

브루트포스의 가장 큰 문제점은 도시의 수가 조금만 늘어나도 실행 시간이 급격하게 증가하여 계산이 불가능해진다.

 

계산 시간 테스트 코드

도시의 개수를 추가하고 경과시간을 체크해 본다.

import time
import itertools
import random

def generate_distance_matrix(n):
    matrix = [[0]*n for _ in range(n)]
    for i in range(n):
        for j in range(i+1, n):
            dist = random.randint(1, 99)
            matrix[i][j] = dist
            matrix[j][i] = dist
    return matrix

def tsp_bruteforce(distance):
    n = len(distance)
    cities = list(range(1, n))

    min_cost = float('inf')
    best_path = None

    for path in itertools.permutations(cities):
        current_cost = 0
        current_city = 0

        for next_city in path:
            current_cost += distance[current_city][next_city]
            current_city = next_city

        current_cost += distance[current_city][0]

        if current_cost < min_cost:
            min_cost = current_cost
            best_path = [0] + list(path) + [0]

    return min_cost, best_path

# 테스트 도시 수: 4 ~ 10
for n in range(4, 11):
    print(f"\n=== TSP for {n} cities ===")
    distance = generate_distance_matrix(n)

    start = time.perf_counter()
    cost, path = tsp_bruteforce(distance)
    end = time.perf_counter()
    elapsed = end - start

    print(f"Min cost: {cost}")
    print(f"Best path (index): {path}")
    print(f"Elapsed time: {elapsed:.6f} seconds")

"""
=== TSP for 4 cities ===
Min cost: 209
Best path (index): [0, 1, 2, 3, 0]
Elapsed time: 0.000019 seconds

=== TSP for 5 cities ===
Min cost: 167
Best path (index): [0, 2, 1, 3, 4, 0]
Elapsed time: 0.000019 seconds

=== TSP for 6 cities ===
Min cost: 159
Best path (index): [0, 3, 1, 2, 5, 4, 0]
Elapsed time: 0.000077 seconds

=== TSP for 7 cities ===
Min cost: 186
Best path (index): [0, 5, 2, 1, 4, 3, 6, 0]
Elapsed time: 0.000502 seconds

=== TSP for 8 cities ===
Min cost: 161
Best path (index): [0, 3, 1, 2, 4, 6, 7, 5, 0]
Elapsed time: 0.019747 seconds

=== TSP for 9 cities ===
Min cost: 172
Best path (index): [0, 4, 5, 8, 2, 7, 3, 1, 6, 0]
Elapsed time: 0.157354 seconds

=== TSP for 10 cities ===
Min cost: 236
Best path (index): [0, 2, 3, 6, 1, 9, 4, 5, 8, 7, 0]
Elapsed time: 1.502671 seconds
"""

 

10개까지의 테스트는 실행이 되었지만 테스트를 위해 사용한 웹 컴파일러는 15초 이상의 작업은 실패로 처리하는데 11개의 도시 테스트에서 실패로 처리되었다.

 

수치를 추정해 보면,

10개의 도시인 경우 10! = 3,628,800개의 경우의 수를 처리하는데 1.5초의 소요 시간이 걸렸다.

11개의 도시는 11! = 39,916,800개로 약 11배가 증가했으므로 추정 소요 시간은 16.5초로 예상된다.

도시 수 순열 수 예상 시간 (초)
10 3,628,800 1.5
11 39,916,800 16.5
12 479,001,600 198+ 초 (약 3.3분)
13 6,227,020,800 약 27~30분
14 87,178,291,200 5시간 이상
 
사용법은 간단하지만 한계가 명확하기 때문에 실용적이지 않아 다른 효율적인 방법으로 대체된다.
 
동적 계획법 (O(n^2 * 2^n))
정확한 해를 구할 수 있는 방법으로, 방문 상태를 비트마스크로 표현하여 중복 계산을 줄인다.
 
근사 알고리즘(다항시간)
최적 해는 아니지만, 일정한 오차 범위 내에서 빠르게 좋은 해를 보장한다.
대표적으로 MST 기반 알고리즘이 있다.
 
휴리스틱 기법
엄밀한 최적 보장은 없지만, 실제 환경에서 유용하게 사용된다. 
유전 알고리즘(GA)이나 시뮬레이티드 어닐링(SA)들이 이에 해당한다.

 

728x90
반응형

'Coding > Algorithm' 카테고리의 다른 글

O(2^n) - 지수 시간  (1) 2025.07.12
NP - TSP  (1) 2025.07.11
P, NP  (0) 2025.07.08
빅-오(Big-O) 표기법  (1) 2025.07.08
시간 복잡도  (1) 2025.07.08

빅-오(Big-O) 표기법

빅-오(Big-O) 표기법은 알고리즘이 입력 크기(n)에 따라 수행 시간이나 연산 횟수가 어떻게 증가하는지를 나타내는 수학적 표기법이다. 보통 최악의 경우(Worst Case) 기준으로 시간 복잡도를 분석하며, 다양한 알고리즘의 효율성을 비교하는 데 활용된다.

 

https://en.wikipedia.org/wiki/File:Comparison_computational_complexity.svg

 

시간 복잡도는 입력 크기에 따른 실행 시간의 증가율에 따라 여러 등급으로 나뉘며 다음은 효율이 낮은 순서부터 정리한 목록이다.

O(n!) - 팩토리얼 시간

가능한 모든 순열을 탐색하는 경우

외판원 문제(TSP), 순열 생성 등

 

O(2^n) - 지수 시간

모든 부분 조합이나 경우의 수를 탐색할 때

부분집합 생성, 피보나치(재귀), DFS/백트래킹 등

 

O(n^2) - 이차 시간

이중 루프를 사용하는 비교 기반 알고리즘

버블 정렬, 선택 정렬 등

 

O(n log n) - 선형 로그 시간

효율적인 비교 기반 정렬 알고리즘의 평균/최선 시간

병합 정렬, 힙 정렬, 퀵 정렬(평균적 경우)

 

O(n) - 선형 시간

입력 전체를 한 번 순회하는 경우

배열을 전체 탐색

 

O(log n) - 로그 시간

입력이 절반씩 줄어드는 알고리즘

이진 탐색, 힘 삽입/삭제 등

 

O(1) - 상수 시간

입력 크기와 무관하게 항상 일정한 시간

배열 인덱스 접근, 해시맵 키 조회

 

 

 

 

 

 

 

728x90
반응형

'Coding > Algorithm' 카테고리의 다른 글

O(2^n) - 지수 시간  (1) 2025.07.12
NP - TSP  (1) 2025.07.11
P, NP  (0) 2025.07.08
O(n!) - 팩토리얼 시간  (2) 2025.07.08
시간 복잡도  (1) 2025.07.08

+ Recent posts