
계산 불가능성은 컴퓨터로 절대 풀 수 없는 문제들이 존재한다는 놀라운 사실을 다룹니다.
계산 불가능(Undecidable)한 문제는 어떤 알고리즘으로도 모든 입력에 대해 올바르게 판단할 수 없는 문제입니다.
쉬운 비유:
마법의 수정구슬로 비유:
가능한 질문: "내일 비가 올까?" → 일기예보로 예측 가능 (완벽하지 않지만)
불가능한 질문: "이 프로그램은 언제까지 실행될까?" → 절대 정확히 알 수 없음! → 수정구슬도 못 봄 → 컴퓨터도 못 봄
중요한 구분:
어려운 문제 (NP-완전):
- 시간이 오래 걸림
- 하지만 원칙적으로는 풀 수 있음
- 예: 외판원 문제 (느리지만 정확한 답 가능)
불가능한 문제 (계산 불가능):
- 아무리 시간을 줘도 풀지 못함
- 원칙적으로 풀 수 없음
- 예: 정지 문제 (어떤 방법으로도 불가능)
차이:
NP-완전 = 느림
계산 불가능 = 절대 불가능
핵심 이유:
프로그램은 자기 자신에 대해 완벽히 분석할 수 없습니다. 이것이 튜링이 발견한 컴퓨터의 근본적 한계입니다.
거울 비유:
상황: 거울로 자기 눈을 보려고 함
문제:
- 왼쪽 눈으로 왼쪽 눈을 볼 수 없음
- 거울에 비친 눈을 보지만, 그건 "직접" 보는 게 아님
프로그램도 마찬가지:
- 프로그램 A가 프로그램 B를 분석: 가능
- 프로그램 A가 자기 자신을 완벽히 분석: 불가능
def predict_output_problem():
"""
출력 예측 문제
문제: 프로그램과 입력이 주어졌을 때, "이 프로그램이 'Hello'를 출력할까?"
왜 불가능한가?
프로그램이 자기 자신을 분석하려 하면 무한 재귀에 빠짐
"""
# 가상의 예측기가 있다고 가정
def will_print_hello(program, input_data):
"""
program이 input_data에 대해 "Hello"를 출력하는지 예측
이런 함수가 존재한다면?
"""
# 마법처럼 예측한다고 가정
pass
# 역설 만들기
def paradox_program(x):
"""
역설을 만드는 프로그램
논리:
1. will_print_hello가 "출력한다"고 예측하면 → 출력하지 않음
2. will_print_hello가 "출력 안 한다"고 예측하면 → 출력함
결과: will_print_hello는 항상 틀림!
"""
if will_print_hello(paradox_program, x):
# 예측이 "출력한다"면 출력하지 않기
return # 아무것도 출력 안함
else:
# 예측이 "출력 안 한다"면 출력하기
print("Hello")
# will_print_hello(paradox_program, None)을 실행하면?
# - Yes 답하면: 실제로는 출력 안 함 → 틀림
# - No 답하면: 실제로는 출력함 → 틀림
#
# 결론: will_print_hello는 존재할 수 없음!
# 이것이 계산 불가능성의 핵심!
# 프로그램이 자기 자신에 대해 완벽히 예측할 수 없음
왜 이런 일이 발생하는가?
프로그램이 자기 자신을 입력으로 받을 수 있기 때문입니다. 이것이 "자기 참조(self-reference)"를 만들고, 자기 참조는 역설을 만듭니다.
유명한 역설: "이 문장은 거짓이다" → 참이면 거짓, 거짓이면 참 → 모순!
프로그램도 마찬가지:
프로그램이 자기 자신에 대해 예측하면
→ 예측에 따라 행동을 바꿀 수 있음
→ 예측이 항상 틀리게 만들 수 있음
→ 완벽한 예측 불가능!
문제:
두 프로그램이 주어졌을 때, "이 두 프로그램은 같은 일을 하는가?"
예:
프로그램 A:
def sum_array_A(arr):
total = 0
for x in arr:
total += x
return total
프로그램 B:
def sum_array_B(arr):
return sum(arr)
질문: A와 B는 동치인가?
→ 사람은 "같다"고 알 수 있음
→ 하지만 일반적으로 컴퓨터는 판단 못함!
왜 불가능한가?
def program_equivalence_impossible():
"""
프로그램 동치 문제가 왜 불가능한가?
이유:
두 프로그램이 "같다"는 것을 증명하려면 모든 가능한 입력에 대해 결과가 같아야 함
문제:
1. 입력이 무한할 수 있음
2. 프로그램이 무한 루프일 수 있음
3. 프로그램의 행동이 복잡할 수 있음
결과:
모든 경우를 확인할 수 없음!
"""
# 예: 이 두 프로그램이 같은가?
def program1(n):
"""
콜라츠 추측 검증
모든 n에 대해 1에 도달하면 True
"""
while n != 1:
if n % 2 == 0:
n = n // 2
else:
n = 3 * n + 1
return True
def program2(n):
"""
항상 True 반환
"""
return True
# 질문: program1과 program2는 동치인가?
#
# 만약 모든 n에 대해 콜라츠 추측이 참이면: 동치
# 하지만 콜라츠 추측은 아직 증명 안됨!
#
# 따라서 동치 여부를 판단할 수 없음
Rice의 정리:
프로그램의 의미론적 특성은 계산 불가능하다
의미론적 특성이란?
- "이 프로그램이 짝수만 출력하는가?"
- "이 프로그램이 항상 종료하는가?"
- "이 프로그램이 'Hello'를 출력하는가?"
Rice의 정리:
이런 질문들에 대해 모든 프로그램을 정확히 분류하는 알고리즘은 존재하지 않는다!
실용적 예시:
def rice_theorem_example():
"""
Rice의 정리 예시
불가능한 질문들:
"""
# 질문 1: 이 프로그램이 항상 양수를 반환하는가?
def always_positive(program):
"""
계산 불가능!
이유:
모든 입력에 대해 확인해야 하는데 입력이 무한할 수 있음
"""
pass
# 질문 2: 이 프로그램이 배열을 정렬하는가?
def is_sorting_program(program):
"""
계산 불가능!
이유:
프로그램이 "의도적으로" 정렬하는지, 아니면 우연히 정렬된 결과를 내는지 판단할 수 없음
"""
pass
# 질문 3: 이 프로그램이 변수 x를 사용하는가?
def uses_variable_x(program):
"""
이건 가능!
이유:
프로그램 코드를 분석하면 됨 (구문론적)
실행 결과를 볼 필요 없음
Rice의 정리는 의미론적 특성에만 적용됨
"""
# 코드 텍스트에서 'x' 찾기
return 'x' in program
핵심:
가능 (구문론적 특성):
- 코드에 'x'가 있는가?
- 줄 수가 100줄 이상인가?
- for 루프를 사용하는가?
불가능 (의미론적 특성):
- 실행하면 'x'를 사용하는가?
- 100번 이상 반복하는가?
- 결과가 정렬되는가?
차이:
구문론적 = 코드만 봐도 알 수 있음
의미론적 = 실행해봐야 알 수 있음
불가능한 문제가 하나 있으면, 환원으로 다른 문제도 불가능함을 증명할 수 있습니다.
방법:
1. 알려진 불가능 문제: A (예: 정지 문제)
2. 증명하고 싶은 문제: B
3. A ≤ B 환원 만들기 (A를 B로 변환)
4. 결론:
만약 B를 풀 수 있다면
→ A도 풀 수 있다 (환원으로)
→ 하지만 A는 불가능
→ 모순!
→ 따라서 B도 불가능!
예시: 정지 문제 → 출력 예측 문제
def halting_to_output_reduction():
"""
정지 문제를 출력 예측 문제로 환원
목표:
출력 예측 문제도 불가능함을 증명
방법:
정지 문제를 풀 수 있다면 출력 예측 문제로 변환해서 풀 수 있음을 보임
"""
# 가정: 출력 예측기가 있다고 가정
def will_output_yes(program, input_data):
"""
program(input_data)가 "YES"를 출력하는가?
이게 가능하다고 가정
"""
pass
# 정지 문제 풀기
def solve_halting(program, input_data):
"""
정지 문제를 출력 예측으로 풀기
아이디어:
1. 원래 program을 변형
2. 정지하면 "YES" 출력하는 새 프로그램 만들기
3. 새 프로그램이 "YES" 출력하는지 확인
"""
# 1단계: 변환 - 새 프로그램 만들기
def modified_program(x):
"""
원래 프로그램을 감싼 프로그램
행동:
- program(input_data) 실행
- 정지하면 "YES" 출력
- 무한 루프면 출력 안함
"""
program(input_data) # 원래 프로그램 실행
print("YES") # 정지했으면 여기 도달
# 2단계: 출력 예측기로 확인
# modified_program이 "YES" 출력하는가?
# = program이 정지하는가?
return will_output_yes(modified_program, input_data)
# 결론:
# will_output_yes가 존재하면
# → solve_halting으로 정지 문제 풀 수 있음
# → 하지만 정지 문제는 불가능
# → 모순!
# → will_output_yes도 존재할 수 없음!
환원 그림:
정지 문제 (불가능)
↓ 환원
출력 예측 문제
↓
만약 풀 수 있다면
↓
정지 문제도 풀 수 있음
↓
모순! (정지 문제는 불가능)
↓
따라서 출력 예측도 불가능
크레타 사람:
"모든 크레타 사람은 거짓말쟁이다"
분석:
- 이 말이 참이면: 크레타 사람이 → 이 말도 거짓 → 모순!
- 이 말이 거짓이면: 크레타 사람이 정직 → 이 말이 참 → 모순!
결론: 자기 참조는 역설을 만듦
프로그램:
프로그램이 자기 자신에 대해 판단하면 → 비슷한 역설 발생 → 완벽한 판단 불가능
마을 규칙:
"이발사는 자기 자신을 면도하지 않는 모든 사람을 면도한다"
질문: 이발사는 자기를 면도하는가?
- 면도한다면:
→ "자기를 면도하지 않는 사람"이 아님 → 면도하면 안됨 → 모순!
- 면도하지 않는다면:
→ "자기를 면도하지 않는 사람"임 → 면도해야 함 → 모순!
결론: 이런 이발사는 존재할 수 없음
프로그램:
"자기를 분석하는 프로그램"도 비슷한 이유로 존재할 수 없음
1. 완벽한 도구는 불가능
불가능한 도구들:
- 완벽한 바이러스 검사기 → 모든 바이러스를 찾는 건 불가능
- 완벽한 버그 찾기 → 모든 버그를 자동으로 찾는 건 불가능
- 완벽한 최적화 컴파일러 → 항상 최적 코드 생성은 불가능
이유:
이런 도구들은 프로그램의 의미론적 특성을 완벽히 분석해야 하는데, Rice의 정리에 의해 불가능
2. 근사 (Approximation)와 휴리스틱 (Heuristic) 필요
def practical_approach():
"""
실무에서의 접근
완벽은 불가능하지만, "대부분의 경우"에 작동하는 도구는 가능!
"""
# 바이러스 검사
def virus_scanner_practical(program):
"""
완벽하진 않지만 실용적
접근:
1. 알려진 바이러스 패턴 확인
2. 의심스러운 행동 탐지
3. 샌드박스에서 실행
한계:
- 새로운 바이러스는 못 찾을 수 있음
- 교묘한 바이러스는 놓칠 수 있음
하지만:
- 대부분의 바이러스는 찾음
- 실무에서 충분히 유용
"""
pass
# 버그 찾기
def bug_finder_practical(program):
"""
완벽하진 않지만 도움됨
접근:
1. 정적 분석 (코드만 봄)
2. 동적 분석 (테스트 실행)
3. 알려진 패턴 확인
한계:
- 모든 버그를 못 찾음
- 거짓 양성 가능
하지만:
- 많은 버그를 찾아줌
- 코드 품질 향상
"""
pass
계산 불가능성
정의:
어떤 알고리즘으로도 모든 입력에 대해 올바르게 판단할 수 없는 문제
특징:
- 시간 문제가 아님 (아무리 기다려도 안 됨)
- 컴퓨터의 근본적 한계
- 자기 참조로 인한 역설
왜 불가능한가?
핵심 이유:
프로그램이 자기 자신에 대해 완벽히 분석할 수 없음
역설:
- 예측기가 예측하면
- 프로그램이 예측 반대로 행동
- 예측이 항상 틀림
- 완벽한 예측 불가능
대표 예시
- 정지 문제
- 프로그램 동치 문제
- 출력 예측 문제
- Rice의 정리 (의미론적 특성)
실무 의미
불가능:
- 완벽한 바이러스 검사
- 완벽한 버그 찾기
- 완벽한 최적화
가능:
- 대부분의 경우 작동
- 휴리스틱과 근사
- 실용적 도구
환원 활용
불가능성 증명:
1. 알려진 불가능 문제 A
2. A ≤ B 환원
3. B도 불가능 증명
예: 정지 문제 → 출력 예측 → 불가능
[08-08] 정지 문제 (Halting Problem)
이전 글: [08-06] 다항 시간 환원
다음 글: [08-08] 정지 문제
시리즈: P1. Computer Science