<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>road_to_ai.log</title>
        <link>https://velog.io/</link>
        <description>AI 전문가를 꿈꾸는 도전자 </description>
        <lastBuildDate>Tue, 31 Mar 2026 14:14:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>road_to_ai.log</title>
            <url>https://velog.velcdn.com/images/road_to_ai/profile/f44be66f-b4d2-47a0-9807-c4b025b09616/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. road_to_ai.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/road_to_ai" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[# [09-03] 컨텍스트 스위칭 (Context Switching)]]></title>
            <link>https://velog.io/@road_to_ai/09-03-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%8A%A4%EC%9C%84%EC%B9%AD-Context-Switching</link>
            <guid>https://velog.io/@road_to_ai/09-03-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%8A%A4%EC%9C%84%EC%B9%AD-Context-Switching</guid>
            <pubDate>Tue, 31 Mar 2026 14:14:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>컨텍스트 스위칭은 CPU가 한 프로세스(또는 스레드)에서 다른 프로세스로 전환하는 과정입니다.</p>
</blockquote>
<hr>
<h2 id="🎯-컨텍스트-스위칭이란-무엇인가">🎯 컨텍스트 스위칭이란 무엇인가</h2>
<h3 id="컨텍스트-스위칭-context-switching의-정의">컨텍스트 스위칭 (Context Switching)의 정의</h3>
<p><strong>컨텍스트 스위칭</strong>은 CPU가 현재 실행 중인 프로세스를 멈추고 다른 프로세스를 실행하는 과정입니다.</p>
<p><strong>쉬운 비유:</strong></p>
<pre><code>상황: 여러 권의 책을 번갈아 읽기

책 A 읽기:
1. 책 A 펼치기
2. 20페이지까지 읽음
3. 책갈피 꽂기 (20페이지)
4. 책 닫기

책 B로 전환:
5. 책 B 펼치기
6. 책갈피 위치 확인 (35페이지)
7. 35페이지부터 읽기

컨텍스트 스위칭:
- 책갈피 = 컨텍스트 (어디까지 읽었는지)
- 책 바꾸기 = 스위칭
- 책갈피 없으면 = 처음부터 다시 읽어야 함!</code></pre><h3 id="컨텍스트란">컨텍스트란?</h3>
<p><strong>컨텍스트(Context)</strong>는 프로세스의 <strong>현재 상태</strong>입니다.</p>
<pre><code>컨텍스트 포함 내용:

CPU 레지스터:
- PC (Program Counter): 다음 실행할 명령어 주소
- SP (Stack Pointer): 스택 최상단 위치
- 범용 레지스터: 계산 중인 값들

프로세스 상태:
- 실행(Running), 준비(Ready), 대기(Waiting)

메모리 정보:
- 코드 영역 위치
- 데이터 영역 위치
- 스택, 힙 위치

열린 파일:
- 파일 디스크립터(File Descriptor, FD) 목록
  * 프로세스가 파일에 접근할 때 사용하는 음수가 아닌 추상적인 정수값

기타:
- 우선순위
- 실행 시간 등

즉:
프로세스를 &quot;재개&quot;하는데 필요한 모든 정보!</code></pre><hr>
<h2 id="🔄-왜-컨텍스트-스위칭이-필요한가">🔄 왜 컨텍스트 스위칭이 필요한가?</h2>
<h3 id="멀티태스킹의-핵심">멀티태스킹의 핵심</h3>
<p>컴퓨터는 <strong>동시에 여러 프로그램이 실행되는 것처럼</strong> 보입니다. 하지만 CPU 코어는 한 번에 하나의 작업만 수행할 수 있습니다.</p>
<pre><code>CPU 코어 1개:
실제로는 한 번에 하나만 실행

하지만 사용자는:
- 음악 듣기 (음악 플레이어)
- 웹 브라우징 (브라우저)
- 문서 작성 (에디터)
→ 동시에 실행되는 것처럼 보임!

비밀:
매우 빠르게 전환!
→ 컨텍스트 스위칭</code></pre><p><strong>시간 분할 (Time Sharing)</strong></p>
<pre><code>타임라인 (매우 단순화):

0ms:   [프로세스 A 실행]
20ms:  컨텍스트 스위칭 → B
20ms:  [프로세스 B 실행]
40ms:  컨텍스트 스위칭 → C
40ms:  [프로세스 C 실행]
60ms:  컨텍스트 스위칭 → A
60ms:  [프로세스 A 실행]
...

각 프로세스:
- 짧은 시간 동안 실행 (타임 슬라이스)
- CPU 양보
- 다시 자기 차례 올 때까지 대기

사용자 입장:
너무 빨라서 &quot;동시에&quot; 실행되는 것처럼 보임!</code></pre><hr>
<h2 id="⚙️-컨텍스트-스위칭-동작-과정">⚙️ 컨텍스트 스위칭 동작 과정</h2>
<h3 id="단계별-설명">단계별 설명</h3>
<pre><code>초기 상태:
프로세스 A 실행 중

1단계: 현재 프로세스 상태 저장
   ┌─────────────────────────┐
   │ CPU 레지스터 값 저장    │
   │ → PCB_A에 기록         │
   │ - PC = 0x1000         │
   │ - SP = 0x2000         │
   │ - 기타 레지스터...      │
   └─────────────────────────┘

2단계: 다음 프로세스 선택
   ┌─────────────────────────┐
   │ 스케줄러 호출           │
   │ → 준비 큐 확인          │
   │ → 프로세스 B 선택       │
   └─────────────────────────┘

3단계: 다음 프로세스 상태 복원
   ┌─────────────────────────┐
   │ PCB_B에서 읽기         │
   │ → CPU 레지스터 복원     │
   │ - PC = 0x3000         │
   │ - SP = 0x4000         │
   │ - 기타 레지스터...      │
   └─────────────────────────┘

4단계: 실행 재개
   ┌─────────────────────────┐
   │ 프로세스 B 실행         │
   │ (PC가 가리키는 곳부터)   │
   └─────────────────────────┘

결과:
프로세스 A → 프로세스 B 전환 완료!</code></pre><h3 id="상세-동작">상세 동작</h3>
<pre><code class="language-python">def context_switch_simulation():
    &quot;&quot;&quot;
    컨텍스트 스위칭 시뮬레이션 (개념적)

    실제 OS 커널 코드는 아니지만 개념을 이해하기 위한 의사 코드
    &quot;&quot;&quot;

    # ===== 1단계: 인터럽트 발생 =====
    # 타이머 인터럽트, I/O 완료, 시스템 콜 등
    def timer_interrupt():
        &quot;&quot;&quot;
        타이머 인터럽트: 일정 시간마다 발생

        예: 10ms마다 → &quot;시간 다 썼어! 다음 프로세스 차례!&quot;
        &quot;&quot;&quot;
        pass

    # ===== 2단계: 현재 프로세스 컨텍스트 저장 =====
    def save_context(process):
        &quot;&quot;&quot;
        현재 실행 중인 프로세스의 컨텍스트 저장

        PCB (Process Control Block)에 저장:
        - CPU 레지스터 값들
        - 프로그램 카운터 (다음 실행할 명령어)
        - 스택 포인터
        - 프로세스 상태
        &quot;&quot;&quot;
        pcb = process.pcb

        # CPU 레지스터 → PCB로 복사
        pcb.program_counter = get_cpu_register(&quot;PC&quot;)
        pcb.stack_pointer = get_cpu_register(&quot;SP&quot;)
        pcb.registers = get_all_cpu_registers()

        # 프로세스 상태 변경
        pcb.state = &quot;READY&quot;  # 실행(RUNNING) → 준비(READY)

        print(f&quot;[저장] {process.name} 컨텍스트 저장&quot;)
        print(f&quot;  PC: {pcb.program_counter}&quot;)
        print(f&quot;  SP: {pcb.stack_pointer}&quot;)

    # ===== 3단계: 다음 프로세스 선택 (스케줄링) =====
    def scheduler():
        &quot;&quot;&quot;
        스케줄러: 다음 실행할 프로세스 선택

        방법:
        - Round Robin: 순서대로
        - Priority: 우선순위 높은 것
        - SJF: 짧은 작업 먼저 등등... (다음 글에서!)
        &quot;&quot;&quot;
        ready_queue = get_ready_processes()

        # 간단히 첫 번째 선택
        next_process = ready_queue[0]

        print(f&quot;[스케줄러] {next_process.name} 선택&quot;)
        return next_process

    # ===== 4단계: 다음 프로세스 컨텍스트 복원 =====
    def restore_context(process):
        &quot;&quot;&quot;
        선택된 프로세스의 컨텍스트 복원

        PCB에서 CPU 레지스터로 복사
        &quot;&quot;&quot;
        pcb = process.pcb

        # PCB → CPU 레지스터로 복사
        set_cpu_register(&quot;PC&quot;, pcb.program_counter)
        set_cpu_register(&quot;SP&quot;, pcb.stack_pointer)
        set_all_cpu_registers(pcb.registers)

        # 프로세스 상태 변경
        pcb.state = &quot;RUNNING&quot;  # 준비(READY) → 실행(RUNNING)

        print(f&quot;[복원] {process.name} 컨텍스트 복원&quot;)
        print(f&quot;  PC: {pcb.program_counter}&quot;)
        print(f&quot;  SP: {pcb.stack_pointer}&quot;)

    # ===== 5단계: 실행 재개 =====
    def resume_execution(process):
        &quot;&quot;&quot;
        프로세스 실행 재개

        PC가 가리키는 명령어부터 실행
        마치 멈춘 적이 없는 것처럼!
        &quot;&quot;&quot;
        print(f&quot;[실행] {process.name} 재개\n&quot;)
        # CPU가 명령어 실행...

    # ===== 전체 흐름 =====
    print(&quot;=== 컨텍스트 스위칭 시작 ===\n&quot;)

    current_process = get_current_process()  # 프로세스 A

    # 1. 타이머 인터럽트
    timer_interrupt()

    # 2. 현재 프로세스 저장
    save_context(current_process)

    # 3. 다음 프로세스 선택
    next_process = scheduler()

    # 4. 다음 프로세스 복원
    restore_context(next_process)

    # 5. 실행 재개
    resume_execution(next_process)

    print(&quot;=== 컨텍스트 스위칭 완료 ===&quot;)</code></pre>
<h3 id="실제-예시-프로그램-실행-중">실제 예시: 프로그램 실행 중</h3>
<pre><code class="language-python">import time

def program_A():
    &quot;&quot;&quot;
    프로그램 A

    실행 중간에 컨텍스트 스위칭 발생
    &quot;&quot;&quot;
    print(&quot;프로그램 A 시작&quot;)

    for i in range(5):
        print(f&quot;A: 카운트 {i}&quot;)
        time.sleep(0.1)  # 0.1초 대기

        # 이 시점에 컨텍스트 스위칭 가능!
        # OS가 프로그램 B로 전환할 수 있음

    print(&quot;프로그램 A 종료&quot;)

def program_B():
    &quot;&quot;&quot;
    프로그램 B
    &quot;&quot;&quot;
    print(&quot;프로그램 B 시작&quot;)

    for i in range(5):
        print(f&quot;B: 카운트 {i}&quot;)
        time.sleep(0.1)

    print(&quot;프로그램 B 종료&quot;)

# 실제로는 OS가 이렇게 전환:
&quot;&quot;&quot;
시간  | 실행 중인 프로그램
------|------------------
0ms   | A: 카운트 0
10ms  | 컨텍스트 스위칭 → B
10ms  | B: 카운트 0
20ms  | 컨텍스트 스위칭 → A
20ms  | A: 카운트 1
30ms  | 컨텍스트 스위칭 → B
30ms  | B: 카운트 1
...

두 프로그램 모두:
- 자기가 멈춘 줄 모름
- 계속 실행되는 것처럼 느낌
- 변수 값도 그대로 유지
&quot;&quot;&quot;</code></pre>
<hr>
<h2 id="⚡-컨텍스트-스위칭-발생-시점">⚡ 컨텍스트 스위칭 발생 시점</h2>
<h3 id="언제-발생하는가">언제 발생하는가?</h3>
<pre><code>1. 타임 슬라이스 만료
   → 할당된 시간 다 씀
   → 다른 프로세스 차례

2. I/O 요청
   → 파일 읽기, 네트워크 대기 등
   → CPU 놀고 있으니 다른 프로세스 실행

3. 시스템 콜
   → sleep(), wait() 등
   → 명시적으로 CPU 양보

4. 인터럽트(Interrupt) * 현재 프로세스 일시 중단, 긴급 요청 우선 처리, 원래 프로세스로 복귀
   → 하드웨어 신호 (키보드, 마우스 등)

5. 프로세스 종료
   → 다음 프로세스 실행</code></pre><p><strong>예시: I/O 대기</strong></p>
<pre><code class="language-python">import time

def io_bound_process():
    &quot;&quot;&quot;
    I/O 바운드 프로세스

    파일 읽기 중에 컨텍스트 스위칭 발생
    &quot;&quot;&quot;
    print(&quot;프로세스 시작&quot;)

    # CPU 작업
    result = 100 + 200
    print(f&quot;계산 결과: {result}&quot;)

    # I/O 작업: 파일 읽기
    print(&quot;파일 읽기 시작...&quot;)
    # 실제로는: open(), read() 시스템 콜
    # → 디스크 I/O 대기
    # → 이 시점에 컨텍스트 스위칭!
    # → 다른 프로세스 실행
    with open(&#39;data.txt&#39;, &#39;r&#39;) as f:
        data = f.read()

    # I/O 완료 후 재개
    print(&quot;파일 읽기 완료&quot;)
    print(f&quot;데이터 크기: {len(data)}&quot;)

# OS 입장:
&quot;&quot;&quot;
1. CPU 작업 실행 (계산)
2. I/O 요청 감지 (파일 읽기)
3. 컨텍스트 스위칭 → 다른 프로세스
4. I/O 완료 인터럽트
5. 컨텍스트 스위칭 → 원래 프로세스
6. 계속 실행 (I/O 완료 후)
&quot;&quot;&quot;</code></pre>
<hr>
<h2 id="🔀-프로세스-vs-스레드-스위칭">🔀 프로세스 vs 스레드 스위칭</h2>
<h3 id="비용-차이">비용 차이</h3>
<pre><code>프로세스 컨텍스트 스위칭:
1. PCB 저장/복원
2. 메모리 맵 전환 (가상 메모리)
3. 캐시/TLB 무효화
4. 많은 데이터 복사
→ 느림 (수 마이크로초)

스레드 컨텍스트 스위칭:
1. 스레드 정보만 저장/복원
2. 메모리 맵 그대로 (같은 프로세스)
3. 캐시 유지
4. 적은 데이터 복사
→ 빠름 (수백 나노초)

차이:
프로세스 스위칭이 스레드보다 10~100배 느림!</code></pre><h3 id="비교-표">비교 표</h3>
<pre><code>┌──────────────┬─────────────┬─────────────┐
│  구 분       │ 프로세스    │  스레드     │
├──────────────┼─────────────┼─────────────┤
│ 저장 데이터   │ 많음       │ 적음        │
│ 메모리 맵     │ 전환       │ 유지        │
│ 캐시         │ 무효화      │ 유지       │
│ 속도         │ 느림       │ 빠름        │
│ 시간         │ 수 μs      │ 수백 ns     │
└──────────────┴─────────────┴─────────────┘

μs = 마이크로초 (10⁻⁶초)
ns = 나노초 (10⁻⁹초)</code></pre><hr>
<h2 id="📊-성능-영향-오버헤드">📊 성능 영향: 오버헤드</h2>
<h3 id="오버헤드란">오버헤드란?</h3>
<p><strong>오버헤드(Overhead)</strong>는 컨텍스트 스위칭 자체에 소요되는 비용입니다.</p>
<pre><code>순수 작업 시간: 실제 프로그램 실행
오버헤드: 컨텍스트 스위칭 시간

전체 시간 = 순수 작업 + 오버헤드

오버헤드가 크면:
→ 실제 작업 시간 감소
→ 성능 저하</code></pre><p><strong>예시:</strong></p>
<pre><code>프로세스 A: 100ms 작업
컨텍스트 스위칭: 1ms

타임 슬라이스 10ms:
10ms 작업 → 1ms 스위칭 → 10ms 작업 → 1ms 스위칭...
100ms 작업에 10번 스위칭
→ 총 110ms (10% 오버헤드)

타임 슬라이스 1ms:
1ms 작업 → 1ms 스위칭 → 1ms 작업 → 1ms 스위칭...
100ms 작업에 100번 스위칭
→ 총 200ms (50% 오버헤드!)

결론:
타임 슬라이스가 너무 짧으면 오버헤드 증가!</code></pre><h3 id="최적화">최적화</h3>
<pre><code class="language-python">def context_switch_optimization():
    &quot;&quot;&quot;
    컨텍스트 스위칭 최적화 전략
    &quot;&quot;&quot;

    # 1. 타임 슬라이스 조정
    &quot;&quot;&quot;
    너무 짧으면: 오버헤드 증가
    너무 길면: 반응성 저하

    일반적: 10~100ms
    실시간 시스템: 1~10ms
    &quot;&quot;&quot;

    # 2. 스레드 사용
    &quot;&quot;&quot;
    프로세스 대신 스레드:
    → 스위칭 비용 감소
    → 10~100배 빠름
    &quot;&quot;&quot;

    # 3. 프로세스 수 제한
    &quot;&quot;&quot;
    너무 많은 프로세스:
    → 스위칭 빈도 증가
    → 오버헤드 증가

    적절한 수:
    CPU 코어 수 × 1~2
    &quot;&quot;&quot;

    # 4. CPU 친화도 (Affinity)
    &quot;&quot;&quot;
    프로세스를 특정 CPU 코어에 고정:
    → 캐시 효율 증가
    → 스위칭 비용 감소
    &quot;&quot;&quot;</code></pre>
<hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<h3 id="컨텍스트-스위칭-확인하기">컨텍스트 스위칭 확인하기</h3>
<pre><code class="language-python">import os
import time

def measure_context_switches():
    &quot;&quot;&quot;
    컨텍스트 스위칭 횟수 측정

    Linux: /proc/[pid]/status
    &quot;&quot;&quot;
    pid = os.getpid()

    # 초기 값
    with open(f&#39;/proc/{pid}/status&#39;, &#39;r&#39;) as f:
        for line in f:
            if &#39;voluntary_ctxt_switches&#39; in line:
                start_vol = int(line.split()[1])
            if &#39;nonvoluntary_ctxt_switches&#39; in line:
                start_nonvol = int(line.split()[1])

    # 작업 수행
    time.sleep(1)  # 프로그램을 1초 동안 멈추라는 의미

    # 최종 값
    with open(f&#39;/proc/{pid}/status&#39;, &#39;r&#39;) as f:
        for line in f:
            # voluntary_ctxt_switches: 자발적 문맥 교환 횟수를 찾아 end_vol 에 저장
            if &#39;voluntary_ctxt_switches&#39; in line:
                end_vol = int(line.split()[1])
            # nonvoluntary_ctxt_switches: 비자발적 문맥 교환 횟수를 찾아 end_nonvol 에 저장
            if &#39;nonvoluntary_ctxt_switches&#39; in line:
                end_nonvol = int(line.split()[1])

    print(f&quot;자발적 스위칭: {end_vol - start_vol}&quot;)
    print(f&quot;비자발적 스위칭: {end_nonvol - start_nonvol}&quot;)

    &quot;&quot;&quot;
    자발적 (Voluntary): 프로세스가 스스로 CPU 양보
    - I/O 대기, sleep() 등

    비자발적 (Involuntary): OS가 강제로 전환
    - 타임 슬라이스 만료, 우선순위가 더 높은 작업이 나타났을 때
    &quot;&quot;&quot;

# Linux에서만 동작
try:
    measure_context_switches()
except:
    print(&quot;Linux에서만 측정 가능&quot;)</code></pre>
<h3 id="성능-고려사항">성능 고려사항</h3>
<pre><code class="language-python">def performance_considerations():
    &quot;&quot;&quot;
    컨텍스트 스위칭 성능 고려
    &quot;&quot;&quot;

    # 나쁜 예: 너무 많은 프로세스
    import multiprocessing

    def bad_example():
        &quot;&quot;&quot;
        프로세스 1000개 생성
        → 과도한 컨텍스트 스위칭
        → 오버헤드 증가
        &quot;&quot;&quot;
        processes = []
        for i in range(1000):  # 너무 많음!
            p = multiprocessing.Process(target=worker)
            p.start()
            processes.append(p)

    # 좋은 예: CPU 코어 수만큼
    def good_example():
        &quot;&quot;&quot;
        CPU 코어 수에 맞춰 프로세스 생성
        → 적절한 병렬성
        → 오버헤드 최소화
        &quot;&quot;&quot;
        cpu_count = os.cpu_count()
        pool = multiprocessing.Pool(cpu_count)
        # Pool이 알아서 작업 분배

    # 최선: 스레드 사용 (I/O 바운드)
    import threading

    def best_for_io():
        &quot;&quot;&quot;
        I/O 바운드 작업은 스레드
        → 컨텍스트 스위칭 비용 적음
        → 메모리 공유로 효율적
        &quot;&quot;&quot;
        threads = []
        for i in range(100):  # 많아도 OK
            t = threading.Thread(target=io_worker)
            t.start()
            threads.append(t)</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>컨텍스트 스위칭</strong></p>
<pre><code>정의:
CPU가 현재 프로세스를 멈추고 다른 프로세스를 실행하는 과정

목적:
멀티태스킹 구현 → 여러 프로그램이 동시에 실행되는 것처럼</code></pre><p><strong>동작 과정</strong></p>
<pre><code>1. 현재 프로세스 컨텍스트 저장 → PCB에 CPU 레지스터 값 저장

2. 다음 프로세스 선택 → 스케줄러 호출

3. 다음 프로세스 컨텍스트 복원 → PCB에서 CPU 레지스터로 복사

4. 실행 재개 → 멈춘 곳부터 계속</code></pre><p><strong>발생 시점</strong></p>
<pre><code>- 타임 슬라이스 만료
- I/O 요청
- 시스템 콜 (sleep, wait)
- 인터럽트
- 프로세스 종료</code></pre><p><strong>성능 영향</strong></p>
<pre><code>오버헤드:
프로세스: 수 마이크로초 (느림)
스레드: 수백 나노초 (빠름)

최적화:
- 적절한 타임 슬라이스
- 스레드 사용
- 프로세스 수 제한
- CPU 친화도</code></pre><p><strong>프로세스 vs 스레드</strong></p>
<pre><code>프로세스 스위칭:
- 메모리 맵 전환
- 캐시 무효화
- 비용 큼

스레드 스위칭:
- 메모리 공유
- 캐시 유지
- 비용 적음 (10~100배 빠름)</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[09-04] 동시성 (Concurrency)</strong></p>
<ul>
<li>동시성의 정의: 여러 작업을 번갈아 수행</li>
<li>병렬성과의 차이: 동시 vs 동시에 보이는 것</li>
<li>동시성의 장점: 반응성, 자원 활용</li>
<li>동시성 프로그래밍: 코루틴, 비동기 프로그래밍</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[09-02] 스레드</a><br><strong>다음 글</strong>: <a href="#">[09-04] 동시성</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [09-02] 스레드 (Thread)]]></title>
            <link>https://velog.io/@road_to_ai/09-02-%EC%8A%A4%EB%A0%88%EB%93%9C-Thread</link>
            <guid>https://velog.io/@road_to_ai/09-02-%EC%8A%A4%EB%A0%88%EB%93%9C-Thread</guid>
            <pubDate>Mon, 30 Mar 2026 12:59:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>스레드는 프로세스 내부의 실행 흐름으로, 가벼운 프로세스라고도 불립니다.</p>
</blockquote>
<hr>
<h2 id="🎯-스레드란-무엇인가">🎯 스레드란 무엇인가</h2>
<h3 id="스레드-thread의-정의">스레드 (Thread)의 정의</h3>
<p><strong>스레드</strong>는 프로세스 내에서 실행되는 <strong>독립적인 실행 흐름</strong>입니다.</p>
<p><strong>쉬운 비유:</strong></p>
<pre><code>프로세스 = 식당
스레드 = 식당 직원

단일 스레드 식당:
- 주방장 1명
- 혼자서 주문 받고, 요리하고, 서빙
- 손님이 많으면 느림

멀티 스레드 식당:
- 주방장 3명
- 같은 주방(메모리) 공유
- 같은 재료(자원) 공유
- 동시에 여러 요리 가능
- 빠름!

핵심:
같은 공간과 자원을 공유하면서 독립적으로 일함</code></pre><h3 id="프로세스-내의-스레드">프로세스 내의 스레드</h3>
<pre><code>프로세스:
┌─────────────────────────────────┐
│           프로세스             │
│  메모리 (공유):                │
│  ┌───────────────────────────┐  │
│  │ 코드 영역                │  │
│  ├───────────────────────────┤  │
│  │ 데이터 영역 (전역 변수)    │  │
│  ├───────────────────────────┤  │
│  │ 힙 (동적 할당)            │  │
│  └───────────────────────────┘  │
│  스레드별 독립:                 │
│  ┌─────────┐ ┌─────────┐        │
│  │스레드 1 │ │스레드 2  │        │
│  │        │ │         │        │
│  │스택     │ │스택     │        │
│  │레지스터  │ │레지스터 │        │
│  │PC      │ │PC       │        │
│  └─────────┘ └─────────┘        │
└─────────────────────────────────┘

공유: 코드, 데이터, 힙
독립: 스택, 레지스터, PC (프로그램 카운터)

* 프로그램 카운터(Program Counter, PC) : CPU 내부 레지스터 중 하나로,
   다음에 실행할 명령어의 주기억장치 주소를 가리키는 포인터</code></pre><hr>
<h2 id="🔄-프로세스-vs-스레드">🔄 프로세스 vs 스레드</h2>
<h3 id="핵심-차이">핵심 차이</h3>
<pre><code>프로세스:
- 독립적인 실행 단위
- 자신만의 메모리 공간
- 무거움 (생성/전환 비용 큼)
- 안전함 (격리됨)

스레드:
- 프로세스 내부의 실행 흐름
- 메모리 공간 공유
- 가벼움 (생성/전환 비용 작음)
- 위험함 (공유로 인한 문제 가능)</code></pre><h3 id="상세-비교">상세 비교</h3>
<pre><code class="language-python">def process_vs_thread_comparison():
    &quot;&quot;&quot;
    프로세스 vs 스레드 비교
    &quot;&quot;&quot;

    # ===== 메모리 =====
    &quot;&quot;&quot;
    프로세스:
    - 독립적인 메모리 공간
    - 프로세스 A의 변수를 B가 접근 불가
    - 안전하지만 통신 어려움

    스레드:
    - 같은 메모리 공간 공유
    - 스레드 A의 전역 변수를 B가 접근 가능
    - 통신 쉽지만 동기화 필요
    &quot;&quot;&quot;

    # ===== 생성 비용 =====
    &quot;&quot;&quot;
    프로세스:
    - 생성: 느림 (새 메모리 공간 할당)
    - 전환: 느림 (컨텍스트 스위칭 비용 큼)
         * Context Switching: CPU가 현재 작업(프로세스/스레드)을 중단하고 
            다른 작업으로 전환하기 위해, 이전 작업의 상태(문맥)를 저장하고
            새 작업의 상태를 불러오는 핵심 기술

    스레드:
    - 생성: 빠름 (메모리 공유)
    - 전환: 빠름 (같은 주소 공간)
    &quot;&quot;&quot;

    # ===== 통신 =====
    &quot;&quot;&quot;
    프로세스 간 통신 (IPC):
    - 파이프, 소켓, 공유 메모리 등 필요
    - 복잡하고 느림

    스레드 간 통신:
    - 전역 변수, 힙 공유
    - 간단하고 빠름
    &quot;&quot;&quot;

    # ===== 안전성 =====
    &quot;&quot;&quot;
    프로세스:
    - 한 프로세스가 죽어도 다른 프로세스 무관
    - 격리되어 안전

    스레드:
    - 한 스레드가 죽으면 전체 프로세스 죽음
    - 공유로 인한 버그 가능
    &quot;&quot;&quot;</code></pre>
<h3 id="비교-표">비교 표</h3>
<pre><code>┌──────────────┬─────────────┬─────────────┐
│ 구  분      │  프로세스    │   스레드    │
├──────────────┼─────────────┼─────────────┤
│ 메모리       │ 독립        │ 공유       │
│ 생성 속도    │ 느림        │ 빠름       │
│ 전환 속도    │ 느림        │ 빠름       │
│ 통신         │ 어려움(IPC) │ 쉬움(공유) │
│ 안전성       │ 높음        │ 낮음       │
│ 자원 사용    │ 많음        │ 적음       │
│ 디버깅       │ 쉬움        │ 어려움     │
└──────────────┴─────────────┴─────────────┘

언제 사용?
프로세스: 독립성, 안정성 중요
스레드: 속도, 자원 효율 중요</code></pre><hr>
<h2 id="🚀-멀티스레딩">🚀 멀티스레딩</h2>
<h3 id="단일-스레드-vs-멀티-스레드">단일 스레드 vs 멀티 스레드</h3>
<pre><code class="language-python">import time
import threading

# ===== 단일 스레드 =====
def single_thread_example():
    &quot;&quot;&quot;
    단일 스레드로 작업 처리

    특징:
    - 순차적 실행
    - 한 번에 하나씩
    - 느림
    &quot;&quot;&quot;
    print(&quot;=== 단일 스레드 ===&quot;)
    start = time.time()

    # 작업 1
    print(&quot;작업 1 시작&quot;)
    time.sleep(1)  # 1초 걸리는 작업 시뮬레이션
    print(&quot;작업 1 완료&quot;)

    # 작업 2
    print(&quot;작업 2 시작&quot;)
    time.sleep(1)
    print(&quot;작업 2 완료&quot;)

    # 작업 3
    print(&quot;작업 3 시작&quot;)
    time.sleep(1)
    print(&quot;작업 3 완료&quot;)

    end = time.time()
    print(f&quot;총 시간: {end - start:.2f}초\n&quot;)
    # 예상: 약 3초

# ===== 멀티 스레드 =====
def worker(task_id, duration):
    &quot;&quot;&quot;
    작업을 수행하는 워커 함수

    각 스레드가 이 함수를 독립적으로 실행

    task_id: 작업 번호
    duration: 작업 시간 (초)
    &quot;&quot;&quot;
    print(f&quot;작업 {task_id} 시작&quot;)
    time.sleep(duration)  # 작업 시뮬레이션
    print(f&quot;작업 {task_id} 완료&quot;)

def multi_thread_example():
    &quot;&quot;&quot;
    멀티 스레드로 작업 처리

    특징:
    - 병렬 실행
    - 동시에 여러 작업
    - 빠름
    &quot;&quot;&quot;
    print(&quot;=== 멀티 스레드 ===&quot;)
    start = time.time()

    # 스레드 3개 생성
    threads = []

    for i in range(1, 4):  # 작업 1, 2, 3
        # threading.Thread: 새 스레드 생성
        # target: 스레드가 실행할 함수
        # args: 함수에 전달할 인자 (튜플)
        thread = threading.Thread(
            target=worker,
            args=(i, 1)  # (task_id, duration)
        )
        threads.append(thread)

        # 스레드 시작
        thread.start()
        # 주의: start()는 스레드를 시작만 하고 바로 반환
        # 스레드는 백그라운드에서 실행됨

    # 모든 스레드가 끝날 때까지 대기
    for thread in threads:
        # join(): 이 스레드가 끝날 때까지 기다림
        thread.join()

    end = time.time()
    print(f&quot;총 시간: {end - start:.2f}초\n&quot;)
    # 예상: 약 1초 (병렬 실행)

# 실행
single_thread_example()  # 약 3초
multi_thread_example()   # 약 1초</code></pre>
<p><strong>출력:</strong></p>
<pre><code>=== 단일 스레드 ===
작업 1 시작
작업 1 완료
작업 2 시작
작업 2 완료
작업 3 시작
작업 3 완료
총 시간: 3.00초

=== 멀티 스레드 ===
작업 1 시작
작업 2 시작
작업 3 시작
작업 1 완료
작업 2 완료
작업 3 완료
총 시간: 1.00초</code></pre><h3 id="실용적인-예시-웹-크롤러">실용적인 예시: 웹 크롤러</h3>
<p>*웹 크롤러(web crawler): 조직적, 자동화된 방법으로 웹(web)을 탐색하는 컴퓨터 프로그램</p>
<pre><code class="language-python">import threading
import time
import requests  # pip install requests

def download_page(url):
    &quot;&quot;&quot;
    웹페이지 다운로드

    네트워크 I/O: CPU가 놀고 있음 → 스레드 사용하기 좋은 경우!

    url: 다운로드할 URL
    &quot;&quot;&quot;
    try:
        print(f&quot;다운로드 시작: {url}&quot;)
        response = requests.get(url, timeout=5)
        print(f&quot;다운로드 완료: {url} ({len(response.text)} bytes)&quot;)
    except Exception as e:
        print(f&quot;오류: {url} - {e}&quot;)

def single_thread_crawler(urls):
    &quot;&quot;&quot;
    단일 스레드 크롤러

    순차적으로 하나씩 다운로드
    &quot;&quot;&quot;
    print(&quot;=== 단일 스레드 크롤러 ===&quot;)
    start = time.time()

    for url in urls:
        download_page(url)

    print(f&quot;총 시간: {time.time() - start:.2f}초\n&quot;)

def multi_thread_crawler(urls):
    &quot;&quot;&quot;
    멀티 스레드 크롤러

    동시에 여러 페이지 다운로드
    &quot;&quot;&quot;
    print(&quot;=== 멀티 스레드 크롤러 ===&quot;)
    start = time.time()

    threads = []

    for url in urls:
        thread = threading.Thread(target=download_page, args=(url,))
        threads.append(thread)
        thread.start()

    # 모든 다운로드 완료 대기
    for thread in threads:
        thread.join()

    print(f&quot;총 시간: {time.time() - start:.2f}초\n&quot;)

# 사용 예시
urls = [
    &#39;https://www.example.com&#39;,
    &#39;https://www.python.org&#39;,
    &#39;https://www.github.com&#39;,
]

# single_thread_crawler(urls)  # 순차: 느림
# multi_thread_crawler(urls)   # 병렬: 빠름</code></pre>
<hr>
<h2 id="🔐-스레드-안전성-thread-safety">🔐 스레드 안전성 (Thread Safety)</h2>
<h3 id="공유-자원-문제">공유 자원 문제</h3>
<p>스레드들이 같은 메모리를 공유하므로, <strong>동시에 같은 데이터를 수정하면 문제</strong>가 발생합니다.</p>
<pre><code class="language-python">import threading

# 공유 변수
counter = 0

def increment_unsafe():
    &quot;&quot;&quot;
    안전하지 않은 증가

    문제:
    여러 스레드가 동시에 counter를 수정
    → 경쟁 상태(Race Condition)
    → 예상과 다른 결과
    &quot;&quot;&quot;
    global counter

    # 이 코드는 실제로 3단계:
    # 1. counter 값 읽기
    # 2. 1 증가
    # 3. counter에 쓰기
    #
    # 스레드 A와 B가 동시에 실행하면:
    # A: 읽기(0) → 증가(1) → 쓰기(1)
    # B: 읽기(0) → 증가(1) → 쓰기(1)
    # 결과: 1 (2가 되어야 하는데!)

    for _ in range(100000):
        counter += 1

def race_condition_demo():
    &quot;&quot;&quot;
    경쟁 상태 시연
    &quot;&quot;&quot;
    global counter
    counter = 0

    print(&quot;=== 경쟁 상태 (Race Condition) ===&quot;)

    # 스레드 2개 생성
    thread1 = threading.Thread(target=increment_unsafe)
    thread2 = threading.Thread(target=increment_unsafe)

    # 동시 실행
    thread1.start()
    thread2.start()

    # 완료 대기
    thread1.join()
    thread2.join()

    print(f&quot;예상값: 200000&quot;)
    print(f&quot;실제값: {counter}&quot;)
    print(f&quot;차이: {200000 - counter}\n&quot;)
    # 실행할 때마다 결과가 다름!
    # 예: 187234, 193451, 198732 등

# 실행
race_condition_demo()</code></pre>
<p><strong>왜 문제가 발생하는가?</strong></p>
<pre><code>counter += 1은 원자적(atomic)이지 않음!

실제로는:
1. LOAD counter (메모리 → 레지스터)
2. ADD 1
3. STORE counter (레지스터 → 메모리)

타이밍 예시:
시간   | 스레드 A          | 스레드 B          | counter
------|------------------|------------------|--------
t0    | LOAD (0)         |                  | 0
t1    |                  | LOAD (0)         | 0
t2    | ADD 1 → (1)      |                  | 0
t3    |                  | ADD 1 → (1)      | 0
t4    | STORE (1)        |                  | 1
t5    |                  | STORE (1)        | 1

결과: 1 (2가 되어야 하는데!)</code></pre><h3 id="해결-lock-사용">해결: Lock 사용</h3>
<pre><code class="language-python">import threading

counter = 0
lock = threading.Lock()  # 자물쇠 생성

def increment_safe():
    &quot;&quot;&quot;
    안전한 증가

    Lock 사용:
    - 한 번에 한 스레드만 실행
    - 다른 스레드는 대기
    - 안전하지만 느림
    &quot;&quot;&quot;
    global counter

    for _ in range(100000):
        # Lock 획득 (자물쇠 잠그기)
        lock.acquire()

        try:
            # 임계 영역 (Critical Section)
            # 한 번에 한 스레드만 실행
            counter += 1
        finally:
            # Lock 해제 (자물쇠 풀기)
            # finally: 예외가 발생해도 반드시 실행
            lock.release()

def safe_increment_demo():
    &quot;&quot;&quot;
    안전한 증가 시연
    &quot;&quot;&quot;
    global counter
    counter = 0

    print(&quot;=== Lock 사용 (안전) ===&quot;)

    thread1 = threading.Thread(target=increment_safe)
    thread2 = threading.Thread(target=increment_safe)

    thread1.start()
    thread2.start()

    thread1.join()
    thread2.join()

    print(f&quot;예상값: 200000&quot;)
    print(f&quot;실제값: {counter}&quot;)
    print(f&quot;차이: {200000 - counter}\n&quot;)
    # 항상 200000!

# 더 간단한 방법: with 문
def increment_safe_with():
    &quot;&quot;&quot;
    with 문으로 Lock 사용

    장점:
    - 자동으로 acquire/release
    - 예외 처리 불필요
    - 코드 간결
    &quot;&quot;&quot;
    global counter

    for _ in range(100000):
        with lock:       # with 문: 자동으로 lock.acquire()와 lock.release()
            counter += 1

# 실행
safe_increment_demo()</code></pre>
<hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<h3 id="언제-스레드를-사용하는가">언제 스레드를 사용하는가?</h3>
<pre><code class="language-python">def when_to_use_threads():
    &quot;&quot;&quot;
    스레드 사용이 좋은 경우 vs 나쁜 경우
    &quot;&quot;&quot;
        # ===== 좋은 경우: I/O 바운드 =====
    &quot;&quot;&quot;
    I/O 바운드: CPU가 놀고 있는 작업
        * I/O Bound: 프로세스가 진행될 때, I/O Wating 시간이 많은 경우로, 
          파일 쓰기, 디스크 작업, 네트워크 통신을 할 때 주로 나타나며 
          작업에 의한 병목(다른 시스템과 통신할 때 나타남)에 의해 작업 속도가 결정

    예시:
    - 파일 읽기/쓰기
    - 네트워크 요청
    - 데이터베이스 쿼리
    - 사용자 입력 대기

    이유:
    I/O 대기 중에 다른 스레드가 일할 수 있음
    &quot;&quot;&quot;
        # ===== 나쁜 경우: CPU 바운드 =====
    &quot;&quot;&quot;
    CPU 바운드: CPU를 계속 쓰는 작업
        * CPU Bound: 프로세스가 진행될 때, CPU 사용 기간이 I/O Wating 보다 많은 경우로,
          주로 행렬 곱이나 고속 연산을 할 때 나타나며 CPU 성능에 의해 작업 속도가 결정

    예시:
    - 복잡한 계산
    - 이미지 처리
    - 비디오 인코딩
    - 암호화/복호화

    이유:
    Python은 GIL(Global Interpreter Lock)로 인해 한 번에 한 스레드만 Python 코드 실행
    → CPU 바운드에서는 멀티스레딩 효과 없음
    → 대신 multiprocessing 사용!
    &quot;&quot;&quot;</code></pre>
<h3 id="스레드-수는-얼마나">스레드 수는 얼마나?</h3>
<pre><code class="language-python">import os
import threading

def how_many_threads():
    &quot;&quot;&quot;
    적절한 스레드 수
    &quot;&quot;&quot;  
    # CPU 코어 수 확인
    cpu_count = os.cpu_count()
    print(f&quot;CPU 코어: {cpu_count}개&quot;)

    # 현재 활성 스레드 수
    active = threading.active_count()
    print(f&quot;활성 스레드: {active}개&quot;)

    &quot;&quot;&quot;
    가이드라인:

    I/O 바운드:
    - 스레드 수 = CPU 코어 수 × 2 ~ 10
    - 대부분 대기하므로 많이 만들어도 OK

    CPU 바운드:
    - 스레드 수 = CPU 코어 수
    - 더 많이 만들면 오버헤드만 증가

    실제:
    - 테스트해보고 결정
    - 너무 많으면 컨텍스트 스위칭 비용
    - 너무 적으면 자원 낭비
    &quot;&quot;&quot;

how_many_threads()</code></pre>
<h3 id="디버깅-팁">디버깅 팁</h3>
<pre><code class="language-python">import threading

def thread_debugging_tips():
    &quot;&quot;&quot;
    스레드 디버깅
    &quot;&quot;&quot;    
    # 스레드 이름 지정
    def worker():
        name = threading.current_thread().name
        print(f&quot;[{name}] 작업 중...&quot;)

    thread = threading.Thread(
        target=worker,
        name=&quot;Worker-1&quot;  # 이름 지정
    )
    thread.start()
    thread.join()

    # 데몬 스레드
    &quot;&quot;&quot;
    데몬 스레드:
    - 백그라운드 작업용
    - 메인 스레드 종료 시 자동 종료
    - 로그, 모니터링 등에 사용
    &quot;&quot;&quot;
    def daemon_worker():
        while True:
            print(&quot;백그라운드 작업...&quot;)
            threading.Event().wait(1)

    daemon = threading.Thread(
        target=daemon_worker,
        daemon=True  # 데몬으로 설정
    )
    daemon.start()
    # 메인 종료하면 daemon도 자동 종료

thread_debugging_tips()</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>스레드</strong></p>
<pre><code>정의:
프로세스 내부의 실행 흐름

특징:
- 메모리 공유
- 가볍고 빠름
- 통신 쉬움
- 동기화 필요</code></pre><p><strong>프로세스 vs 스레드</strong></p>
<pre><code>프로세스:
독립적, 무거움, 안전, 통신 어려움

스레드:
공유, 가벼움, 위험, 통신 쉬움

선택:
독립성/안정성 → 프로세스
속도/효율 → 스레드</code></pre><p><strong>멀티스레딩</strong></p>
<pre><code>장점:
- I/O 대기 중 다른 작업 가능
- 반응성 향상 (UI가 멈추지 않음)
- 자원 효율

단점:
- 경쟁 상태 (Race Condition)
- 디버깅 어려움
- 데드락 가능
   *교착 상태(Deadlock): 운영체제에서 2개 이상의 프로세스가 서로 상대방이 가진 자원을 기다리며
                        무한히 대기하여, 결과적으로 어떤 작업도 진행되지 못하고 멈추는 현상</code></pre><p><strong>스레드 안전성</strong></p>
<pre><code>문제:
여러 스레드가 같은 데이터 동시 수정
→ 예상치 못한 결과

해결:
Lock 사용
→ 한 번에 한 스레드만 실행
→ 안전하지만 느려짐</code></pre><p><strong>실무 가이드</strong></p>
<pre><code>I/O 바운드: 스레드 ✓
CPU 바운드: 프로세스 (multiprocessing)

스레드 수:
I/O: CPU 코어 × 2~10
CPU: CPU 코어 수

항상 테스트!</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[09-03] 컨텍스트 스위칭 (Context Switching)</strong></p>
<ul>
<li>컨텍스트 스위칭의 정의: CPU가 작업을 전환하는 과정</li>
<li>왜 필요한가: 멀티태스킹의 핵심 메커니즘</li>
<li>어떻게 동작하는가: PCB 저장/복원 과정</li>
<li>성능 영향: 오버헤드와 최적화 방법</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[09-01] 프로세스</a><br><strong>다음 글</strong>: <a href="#">[09-03] 컨텍스트 스위칭</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [09-01] 프로세스 (Process)]]></title>
            <link>https://velog.io/@road_to_ai/09-01-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-Process</link>
            <guid>https://velog.io/@road_to_ai/09-01-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-Process</guid>
            <pubDate>Sat, 28 Mar 2026 05:49:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프로세스는 실행 중인 프로그램으로, 운영체제가 관리하는 작업의 기본 단위입니다.</p>
</blockquote>
<hr>
<h2 id="📖-운영체제란-무엇인가">📖 운영체제란 무엇인가</h2>
<h3 id="운영체제의-정의">운영체제의 정의</h3>
<p><strong>운영체제(Operating System, OS)</strong>는 하드웨어와 응용 프로그램 사이에서 중개자 역할을 하는 시스템 소프트웨어입니다.</p>
<p><strong>쉬운 비유:</strong></p>
<pre><code>컴퓨터 = 아파트 건물
하드웨어 = 건물 시설 (전기, 수도, 엘리베이터)
운영체제 = 관리 시스템
응용 프로그램 = 입주민들

관리 시스템(OS):
- 엘리베이터 순서 정하기 (CPU 스케줄링)
- 각 집에 전기 배분하기 (자원 할당)
- 세대 간 분쟁 해결하기 (프로세스 간 통신)
- 보안 관리하기 (권한 제어)</code></pre><p><strong>운영체제가 없다면?</strong></p>
<pre><code>프로그램이 직접 해야 할 일:
- CPU 사용 시간 조절
- 메모리 주소 계산
- 하드디스크 제어
- 키보드/마우스 입력 처리
- 화면 출력 제어

→ 각 하드웨어마다 다른 코드 작성 → 프로그램 작성이 극도로 어려움

운영체제가 있으면:
print(&quot;Hello&quot;)  # 이 한 줄이면 됨! → OS가 알아서 화면에 출력</code></pre><hr>
<h2 id="🏗️-운영체제의-구성-요소">🏗️ 운영체제의 구성 요소</h2>
<p>운영체제는 크게 <strong>커널(Kernel), 셸(Shell), 시스템 프로그램</strong> 3가지로 구성됩니다.</p>
<h3 id="전체-구조">전체 구조</h3>
<pre><code>┌─────────────────────────────────────┐
│         응용 프로그램              │
│   브라우저, 게임, 에디터, 계산기     │
└─────────────────────────────────────┘
                 ↕
┌─────────────────────────────────────┐
│      셸 (Shell) - 인터페이스       │
│   사용자 명령어를 커널로 전달        │
└─────────────────────────────────────┘
                 ↕
┌─────────────────────────────────────┐
│         시스템 프로그램            │
│   ls, cp, mv, ps, 작업 관리자 등   │
└─────────────────────────────────────┘
                 ↕ 시스템 콜
┌─────────────────────────────────────┐
│      커널 (Kernel) - 핵심         │
│   프로세스, 메모리, 파일, 장치 관리  │
└─────────────────────────────────────┘
                 ↕
┌─────────────────────────────────────┐
│           하드웨어                │
│   CPU, 메모리, 디스크, 네트워크     │
└─────────────────────────────────────┘

**운영체제의 실행 권한**

- 사용자 모드 (User Mode): 사용자가 사용하는 일반 앱(브라우저, 게임 등)이 실행되는 상태
     하드웨어에 직접 접근할 권한이 없어서 시스템을 망가뜨릴 위험이 적음

- 커널 모드 (Kernel Mode): 운영체제의 핵심인 커널이 실행되는 상태
     CPU, 메모리, 디스크 등 모든 하드웨어 자원에 접근하고 제어할 수 있는 &#39;무한 권한&#39;을 가짐</code></pre><h3 id="1-커널-kernel---핵심">1. 커널 (Kernel) - 핵심</h3>
<p><strong>커널</strong>은 운영체제의 핵심으로, 항상 메모리에 상주하며 하드웨어를 직접 제어합니다.</p>
<pre><code>커널의 역할:

1. 프로세스 관리
   - 프로세스 생성, 종료
   - CPU 스케줄링
   - 프로세스 간 통신

2. 메모리 관리
   - 메모리 할당, 해제
   - 가상 메모리
   - 메모리 보호

3. 파일 시스템
   - 파일 읽기, 쓰기
   - 디렉토리 관리
   - 권한 제어

4. 장치 드라이버
   - 하드웨어 제어
   - 입출력 관리

특징:
- 항상 메모리에 상주
- 커널 모드(특권 모드)에서 실행
- 하드웨어 직접 접근 가능
- 가장 신뢰할 수 있는 코드</code></pre><h3 id="2-셸-shell---인터페이스">2. 셸 (Shell) - 인터페이스</h3>
<p><strong>셸</strong>은 사용자와 커널 사이의 인터페이스입니다. 일종의 통역사 역할을 합니다.</p>
<pre><code>셸의 역할:

사용자 명령어 해석:
$ ls -l
  ↓
셸이 해석
  ↓
시스템 콜 호출
  ↓
커널이 처리
  ↓
결과 반환
  ↓
화면에 출력

셸의 종류:
- bash (Bourne Again Shell) - Linux/macOS 기본
- zsh (Z Shell) - macOS 최신 기본
- cmd.exe - Windows 명령 프롬프트
- PowerShell - Windows 고급 셸

GUI 셸:
- Windows 탐색기
- macOS Finder
- GNOME, KDE (Linux)</code></pre><h3 id="3-시스템-프로그램">3. 시스템 프로그램</h3>
<p><strong>시스템 프로그램</strong>은 운영체제가 제공하는 유틸리티입니다.</p>
<pre><code>시스템 프로그램 예시:

파일 관리:
- ls, dir: 파일 목록
- cp, copy: 파일 복사
- mv, move: 파일 이동
- rm, del: 파일 삭제

프로세스 관리:
- ps: 프로세스 목록
- top, 작업 관리자: 프로세스 모니터
- kill: 프로세스 종료

시스템 정보:
- df: 디스크 사용량
- free: 메모리 사용량
- uname: 시스템 정보

네트워크:
- ping: 네트워크 연결 확인
- ifconfig, ipconfig: 네트워크 설정

특징:
- 커널이 아닌 일반 프로그램
- 필요할 때만 실행
- 사용자 모드에서 실행
- 시스템 콜로 커널에 요청</code></pre><hr>
<h2 id="🔄-실제-동작-예시">🔄 실제 동작 예시</h2>
<h3 id="예시-1-명령어-실행-셸-사용">예시 1: 명령어 실행 (셸 사용)</h3>
<p>사용자가 터미널에서 명령어를 입력할 때 무슨 일이 일어나는지 봅시다.</p>
<pre><code class="language-bash">$ ls -l   # &quot;현재 디렉토리에 있는 파일들의 목록을 &#39;자세히(Long format)&#39; 보여달라&quot;는 의미</code></pre>
<p><strong>내부 동작 과정:</strong></p>
<pre><code>1단계: 사용자 입력
   터미널에서 &quot;ls -l&quot; 입력, Enter 키 누름
   ↓

2단계: 셸이 명령어 받음
   명령어 파싱: &quot;ls&quot;와 &quot;-l&quot; 분리
   ↓

3단계: 셸이 프로그램 찾기
   ls 프로그램이 어디 있는지 검색 → /bin/ls 또는 /usr/bin/ls 발견
   PATH 환경변수 참조
   ↓

4단계: 새 프로세스 생성 (시스템 콜)
   셸이 시스템 콜 호출:
   fork() → 자식 프로세스 생성
   exec(&quot;/bin/ls&quot;, [&quot;-l&quot;]) → ls 프로그램 실행
   ↓

5단계: ls 프로그램 실행
   a. ls가 시스템 콜 호출 → opendir(&quot;.&quot;) - 현재 디렉토리 열기
   b. 커널이 디렉토리 정보 읽기
   c. readdir() 반복 - 파일 목록 가져오기
   d. stat() - 각 파일 정보 가져오기 (크기, 권한, 날짜)
   ↓

6단계: 커널 처리 (커널 모드)
   a. 파일 시스템에서 디렉토리 데이터 찾기
   b. 디스크에서 inode 정보 읽기
   c. 파일 메타데이터 수집
   d. 권한 확인
   ↓

7단계: 결과 출력
   ls가 포맷팅해서 화면에 출력
   -rw-r--r--  1 user group  1234 Jan 1 12:00 file.txt
   drwxr-xr-x  2 user group  4096 Jan 2 13:00 folder
   ↓

8단계: 프로그램 종료
   ls 종료 (exit 시스템 콜)
   셸로 제어 복귀
   $ (프롬프트 다시 표시)</code></pre><p><strong>계층별 역할:</strong></p>
<pre><code>사용자: &quot;현재 디렉토리 파일 목록 보여줘&quot;
  ↓
셸 (bash): &quot;알았어, ls 프로그램 실행할게&quot;
  ↓
ls 프로그램: &quot;커널한테 파일 정보 달라고 할게&quot;
  ↓
커널: &quot;파일 시스템 확인하고, 디스크에서 읽어올게&quot;
  ↓
하드웨어: &quot;디스크에서 데이터 읽어서 전달&quot;</code></pre><h3 id="예시-2-프로그램-내부에서-파일-열기-셸-미사용">예시 2: 프로그램 내부에서 파일 열기 (셸 미사용)</h3>
<p>이미 실행 중인 프로그램이 파일을 열 때는 <strong>셸을 거치지 않습니다</strong>.</p>
<pre><code class="language-python"># Python 코드
with open(&#39;data.txt&#39;, &#39;r&#39;) as f:  # &#39;r&#39;: 읽기 모드
    content = f.read()

# 위 코드는 다음과 같은 코드로 with 를 사용하면 열린 파일을 자동으로 닫아 줌
# f = open(&#39;data.txt&#39;, &#39;r&#39;)
# content = f.read()
# f.close()</code></pre>
<p><strong>내부 동작 과정:</strong></p>
<pre><code>1단계: 응용 프로그램 (사용자 모드)
   Python 프로그램 이미 실행 중
   open(&#39;data.txt&#39;, &#39;r&#39;) 호출
   ↓

2단계: 시스템 콜 발생
   셸을 거치지 않고 직접 커널에 요청!
   사용자 모드 → 커널 모드 전환
   시스템 콜: open(&quot;/path/to/data.txt&quot;, O_RDONLY)
                     # O_RDONLY (Open Read-Only): 파일을 &quot;읽기 전용&quot;으로 열겠다는 설정 값(플래그)
   ↓

3단계: 커널 처리 (커널 모드)
   a. 파일 시스템에서 &#39;data.txt&#39; 찾기
   b. 권한 확인 (읽기 권한 있는가?)
   c. 파일 디스크립터 생성 (정수, 예: 3)
   d. 파일 테이블에 등록
   ↓

4단계: 디스크 I/O
   a. 파일이 디스크 어디에 있는지 확인 (inode)
   b. 디스크 컨트롤러에 명령
   c. 디스크에서 데이터 읽기
   d. 메모리 버퍼로 복사
   ↓

5단계: 결과 반환
   커널 모드 → 사용자 모드 전환
   파일 디스크립터 번호 반환 (예: 3)
   ↓

6단계: 응용 프로그램으로 복귀
   f.read() 호출하면 2-5단계 반복
   read(3, buffer, size) 시스템 콜
   데이터를 프로그램 변수에 저장</code></pre><p><strong>계층별 역할:</strong></p>
<pre><code>응용 프로그램 (Python): &quot;data.txt 파일 열어줘!&quot;
  ↓
셸: (역할 없음!)
  ↓
커널: &quot;알았어, 파일 시스템 확인하고, 권한 체크하고, 디스크에서 읽어올게&quot;
  ↓
하드웨어: &quot;디스크 헤드 이동, 데이터 읽기, 전송&quot;</code></pre><h3 id="실제-동작-차이점-정리">실제 동작 차이점 정리</h3>
<pre><code>┌─────────────────┬──────────────┬──────────────┐
│ 구    분       │ 명령어 실행   │ 파일 열기    │
├─────────────────┼──────────────┼──────────────┤
│ 셸 사용 여부    │ ✓ 사용       │ ✗ 미사용    │
│ 예시           │ $ ls -l     │ open(&#39;file&#39;)│
│ 프로세스 생성    │ ✓ (fork)   │ ✗           │
│ 시스템 콜 호출자 │ 셸 + ls     │ Python      │
└─────────────────┴──────────────┴──────────────┘

핵심:
- 명령어 입력: 셸이 중개 역할
- 프로그램 내부: 직접 시스템 콜</code></pre><hr>
<h2 id="💻-실제-운영체제의-종류">💻 실제 운영체제의 종류</h2>
<h3 id="데스크톱서버-운영체제">데스크톱/서버 운영체제</h3>
<p><strong>1. Windows</strong></p>
<pre><code>기반: 독자적 (NT 커널)

특징:
- 사용자 친화적 GUI
- 게임, 업무용 소프트웨어 풍부
- .exe 실행 파일
- 상업용 (유료)

버전:
- Windows 11
- Windows Server (서버용)

장점: 호환성, 사용 편의성
단점: 비용, 보안 취약점</code></pre><p><strong>2. macOS</strong></p>
<pre><code>기반: Unix (BSD 기반)

특징:
- 세련된 디자인
- Unix 기반이라 개발자 친화적
- 상업용 (유료, 하드웨어 포함)

버전:
- macOS Sonoma
- macOS Ventura

장점: 안정성, 디자인, Unix 도구
단점: 비용, 하드웨어 제한</code></pre><p><strong>3. Linux</strong></p>
<pre><code>기반: Unix-like

특징:
- 오픈소스
- 커스터마이징 자유
- 배포판(distro)이 다양

주요 배포판:
- Ubuntu: 초보자 친화적
- Fedora: 최신 기술
- Debian: 안정적
- CentOS/RHEL: 서버용
- Arch: 고급 사용자용

장점: 무료, 보안, 유연성, 서버 최적화
단점: 데스크톱 앱 부족, 학습 곡선</code></pre><p><strong>4. Unix</strong></p>
<pre><code>역사적 의미:
- 1970년대 개발 (AT&amp;T Bell Labs)
- 현대 OS의 조상
- macOS, Linux의 기반

현재:
- 순수 Unix는 거의 사용 안됨
- Unix-like 시스템들이 계승
- POSIX 표준으로 호환성 유지

영향:
- C 언어 탄생
- 파이프, 셸, 파일 시스템 개념
- &quot;모든 것은 파일이다&quot; 철학</code></pre><h3 id="모바일-운영체제">모바일 운영체제</h3>
<p><strong>5. Android</strong></p>
<pre><code>기반: Linux 커널

특징:
- 오픈소스
- 다양한 제조사 지원

장점: 개방성, 선택의 폭
단점: 파편화, 보안</code></pre><p><strong>6. iOS</strong></p>
<pre><code>기반: Unix (Darwin 커널)

특징:
- iPhone, iPad 전용
- 폐쇄적 생태계

장점: 안정성, 보안, 최적화
단점: 폐쇄성, 비용</code></pre><h3 id="운영체제-비교">운영체제 비교</h3>
<pre><code>┌──────────┬─────────┬─────────┬──────────┐
│ 구 분    │Windows │ macOS  │  Linux  │
├──────────┼─────────┼─────────┼──────────┤
│비용      │ 유료    │ 유료    │  무료    │
│오픈소스   │ ✗      │ 일부    │  ✓      │
│사용 난이도│ 쉬움    │ 쉬움    │ 중~어려움 │
│게임      │ ✓✓✓    │ ✓      │  ✓      │
│개발      │ ✓      │ ✓✓     │  ✓✓✓    │
│서버      │ ✓      │ ✓      │  ✓✓✓    │
│보안      │ ✓      │ ✓✓     │  ✓✓✓    │
└──────────┴─────────┴─────────┴──────────┘</code></pre><h3 id="왜-다양한-os가-존재하는가">왜 다양한 OS가 존재하는가?</h3>
<pre><code>사용 목적의 차이:
- 데스크톱: 사용 편의성 (Windows, macOS)
- 서버: 안정성, 보안 (Linux)
- 모바일: 배터리, 터치 (Android, iOS)
- 임베디드: 최소 자원 (특화 Linux)

철학의 차이:
- 상업적: Windows, macOS (수익 모델)
- 오픈소스: Linux (공유, 자유)</code></pre><hr>
<h2 id="🎯-이-섹션에서-배울-내용">🎯 이 섹션에서 배울 내용</h2>
<p>특정 운영체제의 사용법이 아니라, <strong>모든 운영체제에 공통되는 핵심 원리</strong>를 배웁니다.</p>
<p><strong>학습 흐름:</strong></p>
<pre><code>프로세스 → 스레드 → 스케줄링 → 동시성/병렬성 → 동기화 → 교착 상태 → 메모리 관리

각 주제는 Windows, Linux, macOS 모두에 적용됩니다.</code></pre><hr>
<h2 id="🎯-프로세스란-무엇인가">🎯 프로세스란 무엇인가</h2>
<h3 id="프로그램-vs-프로세스">프로그램 vs 프로세스</h3>
<p><strong>프로그램(Program)</strong>과 <strong>프로세스(Process)</strong>는 다릅니다.</p>
<p><strong>쉬운 비유:</strong></p>
<pre><code>프로그램 = 레시피 (요리책)
- 종이에 적힌 지침
- 실행되지 않음
- 정적(static)

프로세스 = 요리하는 과정
- 레시피를 따라 실제로 요리 중
- 재료, 도구, 진행 상황 포함
- 동적(dynamic)

예: 같은 레시피(프로그램)로 여러 사람이 동시에 요리(프로세스) 가능</code></pre><p><strong>프로그래밍으로 이해:</strong></p>
<pre><code class="language-python"># 프로그램: 디스크에 저장된 파일
# 파일: calculator.py

def add(a, b):
    &quot;&quot;&quot;덧셈 함수&quot;&quot;&quot;
    return a + b

def main():
    result = add(3, 5)
    print(f&quot;결과: {result}&quot;)

if __name__ == &quot;__main__&quot;:
    main()

# 이 코드 자체는 프로그램 실행 전으로 아직 아무것도 안 함</code></pre>
<p><strong>프로세스: 실행 중</strong></p>
<pre><code class="language-bash"># 프로세스: 프로그램을 실행하면
$ python calculator.py
결과: 8

# 실행하는 순간:
# 1. 메모리에 로드됨
# 2. CPU가 코드 실행
# 3. 변수에 값 저장
# 4. 결과 출력
# → 이것이 프로세스!</code></pre>
<h3 id="프로세스의-정의">프로세스의 정의</h3>
<p><strong>프로세스</strong>는 실행 중인 프로그램으로, 다음을 포함합니다:</p>
<pre><code>프로세스 = 프로그램 코드
         + 현재 실행 상태
         + 메모리 내용
         + 시스템 자원 (파일, 네트워크 등)

즉:
프로그램: 수동적 (passive)
프로세스: 능동적 (active)</code></pre><hr>
<h2 id="🏗️-프로세스의-구조">🏗️ 프로세스의 구조</h2>
<h3 id="메모리-구조">메모리 구조</h3>
<p>프로세스는 메모리를 <strong>4개 영역</strong>으로 나눠 사용합니다.</p>
<pre><code>높은 주소
    ↑
┌─────────────┐
│   스택      │ ← 함수 호출, 지역 변수
│   (Stack)  │   아래로 성장 ↓
├─────────────┤
│   (여유)    │ ← 스택과 힙이 만나면 overflow!
├─────────────┤
│   힙       │ ← 동적 할당 메모리
│   (Heap)   │   위로 성장 ↑
├─────────────┤
│   데이터    │ ← 전역 변수, 정적 변수
│   (Data)   │
├─────────────┤
│   코드      │ ← 프로그램 명령어
│   (Code)   │
└─────────────┘
낮은 주소
    ↓</code></pre><p><strong>왜 스택과 힙은 서로 마주 보고 성장할까?</strong></p>
<pre><code>스택은 아래로(↓), 힙은 위로(↑) 자라는 것을 볼 수 있습니다. 이것은 운영체제의 효율적인 설계 의도가 담겨 있습니다. 

- 메모리 공간의 유연성: 스택과 힙 중 어느 쪽이 더 많이 쓰일지 미리 알 수 없기 때문에
   양 끝에서 시작해 가운데 여유 공간을 공유하게 함으로써 메모리를 낭비 없이 최대한 활용
- 오버플로(Overflow): 
   -- Stack Overflow는 함수를 너무 깊게 재귀 호출하여 스택이 힙 영역을 침범할 때 발생
   -- Heap Overflow는 동적 할당을 너무 많이 해서 힙이 스택 영역을 침범할 때 발생</code></pre><h3 id="1-코드-영역-code-segment">1. 코드 영역 (Code Segment)</h3>
<p><strong>역할:</strong> 실행할 명령어(기계어) 저장</p>
<ul>
<li>소스 코드가 컴파일되어 이 영역에 들어감</li>
</ul>
<pre><code class="language-python">def greet(name):
    &quot;&quot;&quot;
    이 함수의 기계어 명령어가 코드 영역에 저장됨
    &quot;&quot;&quot;
    message = f&quot;Hello, {name}!&quot;
    return message

# 함수 정의 자체는 코드 영역에 실행되지 않으면 메모리만 차지</code></pre>
<p><strong>특징:</strong></p>
<pre><code>- 읽기 전용 (Read-Only)
- 프로그램 실행 중 변하지 않음
- 여러 프로세스가 공유 가능 (같은 프로그램을 여러 번 실행해도 코드는 하나)</code></pre><h3 id="2-데이터-영역-data-segment">2. 데이터 영역 (Data Segment)</h3>
<p><strong>역할:</strong> 전역(Global) 변수, 정적(Static) 변수 저장</p>
<pre><code class="language-python"># 전역 변수 - 데이터 영역에 저장
counter = 0  # 프로그램 시작부터 메모리에 존재
PI = 3.14159

def increment():
    &quot;&quot;&quot;
    전역 변수 사용

    counter는 데이터 영역에 있으므로 프로그램 종료까지 유지됨
    &quot;&quot;&quot;
    global counter
    counter += 1
    return counter

# 호출할 때마다 counter가 유지됨
print(increment())  # 1
print(increment())  # 2
print(increment())  # 3</code></pre>
<p><strong>특징:</strong></p>
<pre><code>- 프로그램 시작부터 종료까지 유지
- 초기화된 데이터: .data
- 초기화 안된 데이터: .bss   * BSS(Block Started by Symbol)는 별도의 공간</code></pre><h3 id="3-힙-영역-heap">3. 힙 영역 (Heap)</h3>
<p><strong>역할:</strong> 동적 할당 메모리로 개발자가 필요에 따라 실시간(Run-time)으로 할당하는 메모리 공간</p>
<pre><code class="language-python">def heap_example():
    &quot;&quot;&quot;
    힙 영역 사용 예시

    동적 할당:
    - 실행 중에 크기 결정
    - 프로그래머가 관리
    &quot;&quot;&quot;

    numbers = []  # 빈 리스트 생성 (힙), 크기가 실행 중에 변함

    for i in range(1000000):
        numbers.append(i)  # 힙 영역 계속 증가

        # 힙이 스택과 만나면? → MemoryError!

    # 리스트 객체: 힙 영역
    data = [1, 2, 3, 4, 5]

    # 딕셔너리: 힙 영역
    user = {
        &#39;name&#39;: &#39;Alice&#39;,
        &#39;age&#39;: 25
    }

    # 객체: 힙 영역
    class Person:
        def __init__(self, name):
            self.name = name  # 힙에 저장

    person = Person(&#39;Bob&#39;)  # 힙 할당

    return data  # 함수 끝나도 data는 힙에 남음</code></pre>
<p><strong>특징:</strong></p>
<pre><code>- 프로그래머가 직접 관리 (명시적 할당/해제)
- 크기가 실행 중에 변함
- 위로 성장 ↑   * 낮은 주소에서 높은 주소 방향으로 쌓여 올라감
- 느림 (할당/해제 오버헤드)
- Python은 자동 가비지 컬렉션 (다 쓰고 나면 반드시 해제해야 하며, 그렇지 않으면 &#39;메모리 누수&#39; 발생)</code></pre><h3 id="4-스택-영역-stack">4. 스택 영역 (Stack)</h3>
<p><strong>역할:</strong> 함수가 호출될 때 생성되는 지역 변수, 매개 변수, 반환 주소 저장</p>
<ul>
<li>함수 실행이 끝나면 자동으로 메모리가 회수되므로 관리가 매우 편리하며, &#39;LIFO(Last In, First Out)&#39; 구조로 동작</li>
</ul>
<pre><code class="language-python">def stack_example():
    &quot;&quot;&quot;
    스택 영역 사용 예시

    함수 호출마다 스택 프레임 생성:
    - 매개 변수
    - 지역 변수
    - 반환 주소
    &quot;&quot;&quot;

    # 지역 변수: 스택에 저장
    x = 10  # stack_example의 스택 프레임에
    y = 20

    # 함수 호출: 새 스택 프레임
    result = helper(x, y)

    return result
    # 함수 종료 → 스택 프레임 제거 → x, y 자동으로 사라짐!

def helper(a, b):
    &quot;&quot;&quot;
    새로운 스택 프레임 생성

    스택 구조:
    ┌───────────────┐ ← 스택 포인터
    │ helper 프레임 │
    │ - a = 10     │
    │ - b = 20     │
    │ - temp = 30  │
    ├───────────────┤
    │ stack_example│
    │ - x = 10     │
    │ - y = 20     │
    └───────────────┘
    &quot;&quot;&quot;
    temp = a + b  # helper의 스택 프레임에
    return temp
    # helper 종료 → helper 프레임 제거 → temp 자동 사라짐

# 재귀 호출: 스택 깊이 주의!
def factorial(n):
    &quot;&quot;&quot;
    재귀: 스택 프레임 n개 생성

    factorial(5):
    ┌───────────────┐
    │ factorial(1) │
    ├───────────────┤
    │ factorial(2) │
    ├───────────────┤
    │ factorial(3) │
    ├───────────────┤
    │ factorial(4) │
    ├───────────────┤
    │ factorial(5) │
    └───────────────┘

    너무 깊으면: Stack Overflow!
    &quot;&quot;&quot;
    if n &lt;= 1:
        return 1
    return n * factorial(n - 1)

# 사용
result = stack_example()
print(f&quot;결과: {result}&quot;)</code></pre>
<p><strong>특징:</strong></p>
<pre><code>- 자동 관리 (함수 호출/종료 시)
- LIFO (Last In First Out) 구조
- 아래로 성장 ↓   * 높은 주소에서 낮은 주소 방향으로 내려감
- 빠름 (단순 포인터 이동)
- 크기 제한 (보통 수 MB)
- 초과 시 Stack Overflow</code></pre><h3 id="메모리-영역-비교">메모리 영역 비교</h3>
<pre><code class="language-python">def memory_regions_demo():
    &quot;&quot;&quot;
    메모리 영역 종합 예시
    &quot;&quot;&quot;
    # ===== 코드 영역 =====
    # 이 함수의 명령어들

    # ===== 데이터 영역 =====
    # (함수 밖의 전역 변수)

    # ===== 스택 영역 =====
    # 지역 변수
    local_var = 100  # 스택

    # 매개변수도 스택
    def func(param):  # param은 스택
        inner = 10  # inner도 스택
        return inner + param

    # ===== 힙 영역 =====
    # 동적 할당
    my_list = [1, 2, 3]  # 리스트 객체는 힙
    my_dict = {}  # 딕셔너리도 힙

    # 스택 vs 힙
    stack_data = 42  # 스택: 빠름, 자동 관리
    heap_data = [42]  # 힙: 느림, 크기 유연

    print(f&quot;스택: {stack_data}&quot;)
    print(f&quot;힙: {heap_data}&quot;)</code></pre>
<hr>
<h2 id="🔄-프로세스-상태">🔄 프로세스 상태</h2>
<h3 id="프로세스-생명-주기">프로세스 생명 주기</h3>
<p>프로세스는 실행 중에 <strong>5가지 상태</strong>를 거칩니다.</p>
<pre><code>           ┌─────────┐
           │  생성   │ ← 프로세스 시작
           │ (New)  │
           └────┬────┘
                ↓
           ┌─────────┐
      ┌───→│  준비   │←───┐
      │    │ (Ready)│    │
      │    └────┬────┘    │
      │         ↓        │
      │    ┌─────────┐    │
      │    │  실행   │    │ 인터럽트
      │    │(Running)│────┘ 또는 시간 초과
      │    └────┬────┘
      │         ↓
      │    ┌─────────┐
      └────│  대기    │ ← I/O 대기
           │(Waiting)│
           └────┬────┘
                ↓
           ┌─────────┐
           │  종료   │ ← 프로세스 끝
           │ (Exit  │
           └─────────┘</code></pre><h3 id="1-생성-new">1. 생성 (New)</h3>
<pre><code class="language-python">import os
import subprocess

def create_process_example():
    &quot;&quot;&quot;
    프로세스 생성 예시

    생성 상태:
    - 운영체제가 프로세스 생성 중
    - 메모리 할당
    - 프로세스 제어 블록 (PCB: Process Control Block) 생성
    - 아직 실행 안 됨
    &quot;&quot;&quot;
    print(&quot;부모 프로세스 시작&quot;)
    print(f&quot;부모 PID: {os.getpid()}&quot;)   
      # 프로세스 식별자(PID, Process ID): 운영체제가 각 프로세스를 구분하기 위해 부여하는 0 또는 양의 정수 번호로
      # 커널이 프로세스를 생성할 때 자동으로 할당하며, 프로세스 종료 시 재사용될 수 있는 임시 고유 번호 

    # 새 프로세스 생성 (생성 상태)
    # subprocess.Popen: 비동기 실행  
    # Popen은 자식 프로세스를 실행시킨 뒤, 그 프로세스가 끝날 때까지 기다리지 않고 바로 다음 파이썬 코드를 실행
    # (기다리게 하려면 .wait()나 .communicate()를 사용)
    process = subprocess.Popen(
        [&#39;python&#39;, &#39;-c&#39;, &#39;print(&quot;자식 프로세스!&quot;)&#39;],
        stdout=subprocess.PIPE  # .PIPE: 한 프로세스의 출력을 다른 프로세스의 입력으로 연결하는 가상의 데이터 통로
    )

    print(f&quot;자식 PID: {process.pid}&quot;)

    # 자식 프로세스 종료 대기
    output, _ = process.communicate()  # .communicate(): Popen을 사용할 때 자식 프로세스와의 &#39;대화와 기다림&#39;을 한꺼번에 
                                       # 처리해주는 도구(데이터 전달(입력), 데이터 수거(출력 및 에러), 프로세스 종료 대기(Wait))
    print(f&quot;자식 출력: {output.decode()}&quot;)

# 실행
create_process_example()</code></pre>
<h3 id="2-준비-ready">2. 준비 (Ready)</h3>
<pre><code>준비 상태:
- 실행 준비 완료
- CPU만 기다림
- 준비 큐(Ready Queue)에 대기

비유:
은행 대기 줄
- 서류 준비 완료
- 창구만 기다림</code></pre><h3 id="3-실행-running">3. 실행 (Running)</h3>
<pre><code>실행 상태:
- CPU를 할당받음
- 실제로 명령어 실행 중

전환:
실행 → 준비: 시간 할당량 소진
실행 → 대기: I/O 요청
실행 → 종료: 프로그램 완료</code></pre><h3 id="4-대기-waitingblocked">4. 대기 (Waiting/Blocked)</h3>
<pre><code class="language-python">import time

def waiting_state_example():
    &quot;&quot;&quot;
    대기 상태 예시

    대기 상태가 되는 경우:
    - 파일 읽기/쓰기
    - 네트워크 통신
    - 사용자 입력
    - sleep() 호출
    &quot;&quot;&quot;

    print(&quot;실행 상태: 계산 중...&quot;)
    result = 100 + 200

    print(&quot;대기 상태 진입: 파일 읽기&quot;)
    # I/O 작업 → 대기 상태
    # CPU를 다른 프로세스에게 양보
    with open(&#39;example.txt&#39;, &#39;r&#39;) as f:
        data = f.read()  # 대기 상태!

    print(&quot;실행 상태 복귀: 파일 읽기 완료&quot;)

    print(&quot;대기 상태 진입: sleep&quot;)
    time.sleep(2)  # 2초 동안 대기 상태

    print(&quot;실행 상태 복귀: sleep 완료&quot;)

    return result</code></pre>
<h3 id="5-종료-exitterminated">5. 종료 (Exit/Terminated)</h3>
<pre><code class="language-python">import sys

def termination_example():
    &quot;&quot;&quot;
    프로세스 종료

    종료 방법:
    1. 정상 종료: return, 프로그램 끝
    2. 비정상 종료: 에러, 강제 종료
    &quot;&quot;&quot;

    # 정상 종료
    def normal_exit():
        print(&quot;작업 완료&quot;)
        return 0  # 정상 종료 코드

    # 비정상 종료
    def abnormal_exit():
        print(&quot;오류 발생!&quot;)
        sys.exit(1)  # 에러 코드 1로 종료

    # 예외로 종료
    def exception_exit():
        raise Exception(&quot;예상치 못한 오류&quot;)
        # 프로세스 종료

    # 강제 종료 (외부에서)
    # kill 명령어, Ctrl+C 등</code></pre>
<hr>
<h2 id="📋-프로세스-제어-블록-pcb">📋 프로세스 제어 블록 (PCB)</h2>
<h3 id="pcb란">PCB란?</h3>
<p><strong>PCB (Process Control Block)</strong>는 운영체제가 프로세스를 관리하기 위해 유지하는 정보입니다.</p>
<pre><code>PCB = 프로세스의 신분증

포함 정보:
- PID (프로세스 ID)
- 프로세스 상태
- CPU 레지스터 값
- 메모리 정보
- 스케줄링 정보
- 파일/네트워크 정보</code></pre><p><strong>개념적 구조:</strong></p>
<pre><code class="language-python">class ProcessControlBlock:
    &quot;&quot;&quot;
    PCB 개념적 구조

    실제로는 C 구조체로 구현되지만 개념 이해를 위한 Python 클래스
    &quot;&quot;&quot;

    def __init__(self, pid, program_name):
        # ===== 프로세스 식별 정보 =====
        self.pid = pid  # 프로세스 ID (고유)
        self.parent_pid = None  # 부모 프로세스 ID
        self.program_name = program_name  # 실행 파일명

        # ===== 프로세스 상태 =====
        self.state = &quot;NEW&quot;  # NEW, READY, RUNNING, WAITING, EXIT

        # ===== CPU 정보 =====
        # 컨텍스트 스위칭 시 저장/복원
        self.program_counter = 0  # 다음 실행할 명령어 주소
        self.registers = {}  # CPU 레지스터 값들

        # ===== 메모리 정보 =====
        self.memory_base = None  # 메모리 시작 주소
        self.memory_limit = None  # 메모리 크기
        self.page_table = None  # 페이지 테이블 (가상 메모리)

        # ===== 스케줄링 정보 =====
        self.priority = 0  # 우선순위
        self.cpu_time = 0  # CPU 사용 시간
        self.arrival_time = None  # 도착 시간

        # ===== 파일/자원 정보 =====
        self.open_files = []  # 열린 파일 목록
        self.network_sockets = []  # 네트워크 연결

        # ===== 기타 =====
        self.user_id = None  # 소유자
        self.working_directory = None  # 작업 디렉토리

    def __repr__(self):
        &quot;&quot;&quot;PCB 정보 출력&quot;&quot;&quot;
        return f&quot;&quot;&quot;
PCB [PID: {self.pid}]
├─ 프로그램: {self.program_name}
├─ 상태: {self.state}
├─ 우선순위: {self.priority}
├─ CPU 시간: {self.cpu_time}ms
└─ 열린 파일: {len(self.open_files)}개
        &quot;&quot;&quot;.strip()

# 예시: PCB 생성
pcb = ProcessControlBlock(pid=1234, program_name=&quot;calculator.py&quot;)
pcb.state = &quot;RUNNING&quot;
pcb.priority = 5
pcb.cpu_time = 150
pcb.open_files = [&quot;input.txt&quot;, &quot;output.txt&quot;]

print(pcb)</code></pre>
<h3 id="실제-프로세스-정보-확인">실제 프로세스 정보 확인</h3>
<pre><code class="language-python">import os
import psutil  # pip install psutil

def show_process_info():
    &quot;&quot;&quot;
    실제 프로세스 정보 확인

    psutil: 시스템/프로세스 정보 라이브러리
    &quot;&quot;&quot;

    # 현재 프로세스
    current_process = psutil.Process()

    print(&quot;=== 현재 프로세스 정보 ===&quot;)
    print(f&quot;PID: {current_process.pid}&quot;)
    print(f&quot;이름: {current_process.name()}&quot;)
    print(f&quot;상태: {current_process.status()}&quot;)

    # CPU 정보
    cpu_times = current_process.cpu_times()
    print(f&quot;\nCPU 시간:&quot;)
    print(f&quot;  사용자: {cpu_times.user}초&quot;)
    print(f&quot;  시스템: {cpu_times.system}초&quot;)

    # 메모리 정보
    memory_info = current_process.memory_info()
    print(f&quot;\n메모리:&quot;)
    print(f&quot;  RSS: {memory_info.rss / 1024 / 1024:.2f} MB&quot;)
    print(f&quot;  VMS: {memory_info.vms / 1024 / 1024:.2f} MB&quot;)

    # 열린 파일
    try:
        open_files = current_process.open_files()
        print(f&quot;\n열린 파일: {len(open_files)}개&quot;)
        for f in open_files[:3]:  # 처음 3개만
            print(f&quot;  - {f.path}&quot;)
    except:
        print(&quot;\n열린 파일: 접근 권한 없음&quot;)

    # 부모 프로세스
    try:
        parent = current_process.parent()
        if parent:
            print(f&quot;\n부모 프로세스:&quot;)
            print(f&quot;  PID: {parent.pid}&quot;)
            print(f&quot;  이름: {parent.name()}&quot;)
    except:
        print(&quot;\n부모 프로세스: 없음&quot;)

# 실행
show_process_info()</code></pre>
<hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<h3 id="프로세스-vs-스레드-미리보기">프로세스 vs 스레드 (미리보기)</h3>
<pre><code>프로세스:
- 독립적인 실행 단위
- 자신만의 메모리 공간
- 프로세스 간 통신 어려움
- 생성/전환 비용 큼

스레드 (다음 글):
- 프로세스 내부의 실행 흐름
- 메모리 공간 공유
- 스레드 간 통신 쉬움
- 생성/전환 비용 작음

선택:
- 독립성 필요: 프로세스
- 속도 중요: 스레드</code></pre><h3 id="메모리-누수-방지">메모리 누수 방지</h3>
<pre><code class="language-python">def memory_leak_prevention():
    &quot;&quot;&quot;
    메모리 누수 방지

    메모리 누수:
    - 힙에 할당한 메모리를 해제 안함
    - 계속 쌓여서 메모리 부족

    Python:
    - 자동 가비지 컬렉션
    - 하지만 주의 필요
    &quot;&quot;&quot;

    # 나쁜 예: 순환 참조
    class Node:
        def __init__(self):
            self.next = None

    def bad_example():
        node1 = Node()
        node2 = Node()
        node1.next = node2
        node2.next = node1  # 순환!
        # 함수 끝나도 메모리 해제 안될 수 있음

    # 좋은 예: 명시적 정리
    def good_example():
        node1 = Node()
        node2 = Node()
        node1.next = node2
        node2.next = node1

        # 사용 후 정리
        node1.next = None
        node2.next = None
        # 이제 가비지 컬렉션 가능</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>프로세스</strong></p>
<pre><code>정의:
실행 중인 프로그램

프로그램 vs 프로세스:
프로그램 = 레시피 (정적)
프로세스 = 요리 중 (동적)</code></pre><p><strong>메모리 구조</strong></p>
<pre><code>코드: 명령어 (읽기 전용)
데이터: 전역 변수
힙: 동적 할당 (위로 성장)
스택: 함수 호출 (아래로 성장)</code></pre><p><strong>프로세스 상태</strong></p>
<pre><code>생성 → 준비 → 실행 ⇄ 대기 → 종료

준비: CPU 대기
실행: CPU 사용 중
대기: I/O 대기</code></pre><p><strong>PCB</strong></p>
<pre><code>프로세스 제어 블록:
- PID
- 상태
- CPU 레지스터
- 메모리 정보
- 스케줄링 정보</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[09-02] 스레드 (Thread)</strong></p>
<ul>
<li>스레드의 정의: 프로세스 내부의 실행 흐름</li>
<li>프로세스 vs 스레드: 차이점과 언제 무엇을 사용할까</li>
<li>멀티스레딩: 하나의 프로세스에 여러 스레드</li>
<li>스레드 안전성: 공유 메모리 문제와 해결</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[08-09] 기타 복잡도 클래스</a><br><strong>다음 글</strong>: <a href="#">[09-02] 스레드</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [08-09] 기타 복잡도 클래스]]></title>
            <link>https://velog.io/@road_to_ai/08-09-%EA%B8%B0%ED%83%80-%EB%B3%B5%EC%9E%A1%EB%8F%84-%ED%81%B4%EB%9E%98%EC%8A%A4</link>
            <guid>https://velog.io/@road_to_ai/08-09-%EA%B8%B0%ED%83%80-%EB%B3%B5%EC%9E%A1%EB%8F%84-%ED%81%B4%EB%9E%98%EC%8A%A4</guid>
            <pubDate>Fri, 20 Mar 2026 11:31:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>계산 복잡도 이론에는 P, NP, NP-완전 외에도 다양한 복잡도 클래스가 있으며, 이들의 관계는 컴퓨터 과학의 중요한 연구 주제입니다.</p>
</blockquote>
<hr>
<h2 id="🎯-복잡도-클래스-개요">🎯 복잡도 클래스 개요</h2>
<h3 id="복잡도-클래스란">복잡도 클래스란?</h3>
<p><strong>복잡도 클래스(Complexity Class)</strong>는 비슷한 자원(시간, 공간)을 필요로 하는 문제들의 집합입니다.</p>
<p><strong>쉬운 비유:</strong></p>
<pre><code>학교 과목을 난이도로 분류:

쉬운 과목 (P):
- 기초 수학
- 기초 영어
- 노력하면 누구나 풀 수 있음

어려운 과목 (NP):
- 고급 수학
- 답 확인은 쉽지만 찾기 어려움

매우 어려운 과목 (NP-완전):
- 수학 올림피아드
- 가장 어려운 문제들

더 어려운 과목 (PSPACE, EXPTIME):
- 대학원 수준
- 더 많은 자원 필요</code></pre><h3 id="복잡도-클래스-계층">복잡도 클래스 계층</h3>
<pre><code>결정 불가능 (Undecidable)
    ↑
EXPTIME (지수 시간)
    ↑
PSPACE (다항 공간)
    ↑
NP (비결정적 다항 시간)
    ↑
P (결정적 다항 시간)

포함 관계: P ⊆ NP ⊆ PSPACE ⊆ EXPTIME ⊂ 결정 불가능

주의: P = NP인지는 아직 모름!</code></pre><hr>
<h2 id="📊-주요-복잡도-클래스">📊 주요 복잡도 클래스</h2>
<h3 id="1-co-np-np의-여집합">1. co-NP (NP의 여집합)</h3>
<p><strong>정의:</strong></p>
<pre><code>문제 L이 co-NP에 속한다 ⟺ L의 여집합이 NP에 속한다

즉:
L: &quot;Yes&quot; 답을 검증하기 쉬움
L의 여집합: &quot;No&quot; 답을 검증하기 쉬움</code></pre><p><strong>쉬운 비유:</strong></p>
<pre><code>NP 문제:
&quot;이 그래프에 클리크가 있는가?&quot; → 있다면 클리크를 제시하면 쉽게 확인

co-NP 문제:
&quot;이 그래프에 클리크가 없는가?&quot;
→ 없다면... 어떻게 증명?
→ 모든 조합 확인해야 함 (어려움)

차이:
NP: &quot;있다&quot;는 증명 쉬움
co-NP: &quot;없다&quot;는 증명 쉬움</code></pre><p><strong>예시:</strong></p>
<pre><code class="language-python"># NP 문제: 소인수분해 가능성
def is_composite(n):
    &quot;&quot;&quot;
    합성수인가? (소수가 아닌가?)

    문제: n이 합성수인가?

    검증 (NP):
    - &quot;합성수다&quot;의 증명: 인수 제시
    - 예: 15 = 3 × 5
    - 곱해보면 확인 가능 (쉬움)

    n: 검사할 수
    factors: 제시된 인수 (증명서)

    Returns: True if 합성수
    &quot;&quot;&quot;
    pass


# co-NP 문제: 소수 판정
def is_prime(n):
    &quot;&quot;&quot;
    소수인가?

    문제: n이 소수인가?

    검증 (co-NP):
    - &quot;소수다&quot;의 증명: 어려움
    - 모든 가능한 인수가 없음을 보여야 함
    - 전통적으로는 모든 수 확인 필요

    특이 사항:
    - 소수 판정은 사실 P에 속함 (AKS 알고리즘, 2002)
    - 하지만 개념적으로는 co-NP 문제

    n: 검사할 수

    Returns: True if 소수
    &quot;&quot;&quot;
    pass</code></pre>
<p><strong>P, NP, co-NP 관계:</strong></p>
<pre><code>알려진 사실:
- P ⊆ NP
- P ⊆ co-NP
- P = NP ∩ co-NP (추측, 증명 안 됨)

미해결 질문:
- NP = co-NP인가?
- 대부분 학자: NP ≠ co-NP 추측</code></pre><h3 id="2-pspace-다항-공간">2. PSPACE (다항 공간)</h3>
<p><strong>정의:</strong></p>
<pre><code>PSPACE: 다항 공간으로 해결 가능한 문제들

특징:
- 시간은 제한 없음
- 공간(메모리)만 다항식으로 제한
- 예: O(n²) 메모리 사용</code></pre><p><strong>왜 중요한가?</strong></p>
<pre><code class="language-python">def pspace_vs_np():
    &quot;&quot;&quot;
    PSPACE와 NP의 차이

    NP:
    - 다항 시간에 검증 가능
    - 시간에 집중

    PSPACE:
    - 다항 공간으로 해결 가능
    - 공간에 집중

    관계:
    NP ⊆ PSPACE (확실)

    이유:
    - 다항 시간 알고리즘은 최대 다항 공간만 사용 가능 (한 단계당 최대 O(1) 공간)

    역은?
    PSPACE ⊆ NP? (모름!)
    &quot;&quot;&quot;
    pass</code></pre>
<p><strong>대표 문제: 양적 불 공식 (QBF)</strong></p>
<pre><code class="language-python">def quantified_boolean_formula():
    &quot;&quot;&quot;
    양적 불 공식 (Quantified Boolean Formula)

    일반 불 공식 (SAT): (x₁ ∨ ¬x₂) ∧ (x₂ ∨ x₃)  → x₁, x₂, x₃에 값 할당

    양적 불 공식 (QBF): ∀x₁ ∃x₂ ∀x₃ ((x₁ ∨ ¬x₂) ∧ (x₂ ∨ x₃))

    의미:
    - ∀x₁: 모든 x₁에 대해
    - ∃x₂: 어떤 x₂가 존재
    - ∀x₃: 모든 x₃에 대해
    - 식이 참이 되는가?

    복잡도:
    - PSPACE-완전
    - SAT(NP-완전)보다 어려움

    왜 어려운가:
    - 변수마다 &quot;모든&quot;과 &quot;존재&quot; 반복
    - 게임 트리와 비슷
    &quot;&quot;&quot;

    # 간단한 예시
    # ∀x ∃y (x ∨ y)
    #
    # 의미:
    # 모든 x(True/False)에 대해
    # 어떤 y가 존재해서
    # x ∨ y가 참이 되는가?
    #
    # 확인:
    # x=True: y=True 또는 False (둘 다 OK)
    # x=False: y=True 선택
    # → 답: Yes
    pass</code></pre>
<p><strong>PSPACE-완전 문제들:</strong></p>
<pre><code>게임 문제들:
- 체스 (일반화된 n×n)
- 바둑 (일반화된 n×n)
- 리버시
- 체커

왜 PSPACE?
- 게임 트리 탐색
- 모든 수를 다 볼 필요 없음
- 현재 상태만 메모리에 유지
- 재귀로 다음 수 탐색
- 공간 복잡도: O(깊이)</code></pre><h3 id="3-exptime-지수-시간">3. EXPTIME (지수 시간)</h3>
<p><strong>정의:</strong></p>
<pre><code>EXPTIME: 지수 시간으로 해결 가능한 문제들

시간: 2^(다항식)
예: 2^n, 2^(n²), 2^(n³)

특징:
- 매우 느림
- 작은 입력만 가능
- 하지만 계산 가능</code></pre><p><strong>PSPACE vs EXPTIME:</strong></p>
<pre><code>알려진 사실:
PSPACE ⊆ EXPTIME (확실)

이유:
- 다항 공간 사용하는 알고리즘은
- 최대 2^(다항 공간) 가지 상태
- 모든 상태 확인하면 해결 가능
- 시간: 지수

엄격한 포함:
PSPACE ⊊ EXPTIME (확실!)
- EXPTIME에만 속하는 문제 존재</code></pre><p><strong>대표 문제:</strong></p>
<pre><code class="language-python">def exptime_problems():
    &quot;&quot;&quot;
    EXPTIME-완전 문제들

    1. 체커 (표준 8×8)
       - 일반화 버전(n×n)은 EXPTIME
       - 표준 버전도 매우 어려움

    2. 정규 표현식 매칭
       - 일부 복잡한 정규식
       - 백트래킹이 지수 시간

    3. 프레스버거 산술
       - 덧셈만 있는 산술 논리
       - 곱셈 없음
       - 그래도 지수 시간!
    &quot;&quot;&quot;
    pass</code></pre>
<hr>
<h2 id="🔗-복잡도-클래스-관계도">🔗 복잡도 클래스 관계도</h2>
<h3 id="전체-계층-구조">전체 계층 구조</h3>
<pre><code>┌─────────────────────────────────┐
│     결정 불가능 (Undecidable)  │
│  - 정지 문제                   │
│  - 프로그램 동치               │
└─────────────────────────────────┘
              ↑
┌─────────────────────────────────┐
│     EXPTIME (지수 시간)        │
│  - 체커, 복잡한 정규식          │
└─────────────────────────────────┘
              ↑
┌─────────────────────────────────┐
│     PSPACE (다항 공간)         │
│  - QBF, 체스, 바둑             │
└─────────────────────────────────┘
              ↑
┌─────────────────────────────────┐
│     NP                       │
│  - 다항 시간 검증              │
│  ┌──────────────┐             │
│  │  NP-완전     │             │
│  │- SAT, TSP   │             │
│  └──────────────┘             │
└─────────────────────────────────┘
              ↑
┌─────────────────────────────────┐
│     P                        │
│  - 다항 시간 해결              │
│  - 정렬, 최단 경로             │
└─────────────────────────────────┘

확실한 관계:
P ⊆ NP ⊆ PSPACE ⊆ EXPTIME ⊂ Undecidable

미해결 질문:
- P = NP?
- NP = PSPACE?</code></pre><h3 id="다이어그램으로-이해하기">다이어그램으로 이해하기</h3>
<pre><code>      ┌─────────────────────────┐
      │    결정 불가능          │
      └─────────────────────────┘
                 ↑
      ┌─────────────────────────┐
      │     EXPTIME           │
      │  ┌──────────────────┐   │
      │  │   PSPACE        │   │
      │  │ ┌─────────────┐  │   │
      │  │ │    NP      │  │   │
      │  │ │ ┌─────────┐ │  │   │
      │  │ │ │    P   │ │  │   │
      │  │ │ └─────────┘ │  │   │
      │  │ └─────────────┘  │   │
      │  └──────────────────┘   │
      └─────────────────────────┘

보장된 포함:
P ⊆ NP ⊆ PSPACE ⊆ EXPTIME

엄격한 차이:
P ⊊ EXPTIME (확실)
PSPACE ⊊ EXPTIME (확실)</code></pre><hr>
<h2 id="💡-실무-의미">💡 실무 의미</h2>
<h3 id="문제-분류-가이드">문제 분류 가이드</h3>
<pre><code class="language-python">def classify_problem(problem_description):
    &quot;&quot;&quot;
    문제를 복잡도 클래스로 분류

    체크리스트:
    &quot;&quot;&quot;

    # P 클래스
    if has_polynomial_algorithm(problem_description):
        return &quot;P - 효율적으로 풀 수 있음&quot;

    # NP 클래스
    if can_verify_in_polynomial(problem_description):
        if is_known_npc(problem_description):
            return &quot;NP-완전 - 매우 어려움, 근사 필요&quot;
        else:
            return &quot;NP - 검증 쉬움, 풀기 어려움&quot;

    # PSPACE 클래스
    if is_game_problem(problem_description):
        return &quot;PSPACE - 게임 문제, 매우 어려움&quot;

    # EXPTIME 클래스
    if requires_exponential_time(problem_description):
        return &quot;EXPTIME - 지수 시간 필요&quot;

    # 결정 불가능
    if is_undecidable(problem_description):
        return &quot;결정 불가능 - 컴퓨터로 풀 수 없음&quot;

    return &quot;추가 분석 필요&quot;</code></pre>
<h3 id="실무-전략">실무 전략</h3>
<pre><code class="language-python">def choose_solving_strategy(complexity_class, input_size):
    &quot;&quot;&quot;
    복잡도 클래스에 따른 해결 전략

    complexity_class: P, NP, PSPACE, EXPTIME 등
    input_size: 입력 크기

    Returns: 권장 전략
    &quot;&quot;&quot;

    if complexity_class == &quot;P&quot;:
        # 다항 시간 알고리즘 사용
        return &quot;정확한 알고리즘으로 해결&quot;

    elif complexity_class == &quot;NP-완전&quot;:
        if input_size &lt;= 20:
            return &quot;정확한 알고리즘 (브루트포스)&quot;
        elif input_size &lt;= 1000:
            return &quot;동적 계획법 또는 근사 알고리즘&quot;
        else:
            return &quot;휴리스틱 (유전 알고리즘, 시뮬레이티드 어닐링)&quot;

    elif complexity_class == &quot;PSPACE&quot;:
        if input_size &lt;= 10:
            return &quot;게임 트리 탐색 (알파-베타 가지치기)&quot;
        else:
            return &quot;몬테카를로 트리 탐색, 강화학습&quot;

    elif complexity_class == &quot;EXPTIME&quot;:
        if input_size &lt;= 5:
            return &quot;작은 입력만 처리 가능&quot;
        else:
            return &quot;문제 단순화 또는 근사&quot;

    else:  # 결정 불가능
        return &quot;완벽한 해결 불가능, 휴리스틱만 가능&quot;</code></pre>
<hr>
<h2 id="🎯-계산-복잡도-이론-총정리">🎯 계산 복잡도 이론 총정리</h2>
<h3 id="지금까지-배운-내용">지금까지 배운 내용</h3>
<p><strong>1. 기본 개념</strong></p>
<pre><code>[08-01] 결정 문제
- 모든 문제를 Yes/No로 변환
- 복잡도 분석의 기초

[08-02] 클래스 P
- 다항 시간에 해결 가능
- 효율적인 알고리즘 존재
- 예: 정렬, 최단 경로</code></pre><p><strong>2. NP와 NP-완전</strong></p>
<pre><code>[08-03] 클래스 NP
- 다항 시간에 검증 가능
- 답 찾기는 어려울 수 있음
- 예: 스도쿠, 소인수분해

[08-04] NP-완전
- NP 중 가장 어려운 문제들
- 하나만 풀면 모든 NP 풀림
- 예: SAT, TSP, 클리크

[08-05] NP-난해
- NP-완전만큼 또는 더 어려움
- NP에 속하지 않을 수 있음
- 예: TSP 최적화, 정지 문제</code></pre><p><strong>3. 환원과 증명</strong></p>
<pre><code>[08-06] 다항 시간 환원
- 문제를 다른 문제로 변환
- 난이도 비교 도구
- NP-완전 증명 방법</code></pre><p><strong>4. 계산 불가능성</strong></p>
<pre><code>[08-07] 계산 불가능성
- 컴퓨터로 절대 풀 수 없는 문제
- 자기 참조의 역설
- 예: 프로그램 동치, Rice의 정리

[08-08] 정지 문제
- 가장 유명한 불가능 문제
- 튜링의 증명 (귀류법)
- 실무 의미: 완벽한 도구 불가능</code></pre><p><strong>5. 기타 클래스</strong></p>
<pre><code>[08-09] 기타 복잡도 클래스
- co-NP: &quot;No&quot; 증명이 쉬움
- PSPACE: 다항 공간
- EXPTIME: 지수 시간</code></pre><h3 id="핵심-질문-p-vs-np">핵심 질문: P vs NP</h3>
<p><strong>P = NP 문제:</strong></p>
<pre><code>질문:
&quot;검증이 쉬운 문제는 풀기도 쉬운가?&quot;

만약 P = NP이면:
- 모든 NP-완전 문제를 다항 시간에 풀 수 있음
- 암호화 대부분 무용지물
- 최적화 문제들 쉽게 해결
- 컴퓨터 과학 혁명

대부분 학자의 추측:
P ≠ NP
- NP-완전은 본질적으로 어려움
- 효율적 알고리즘 없음

현재 상황:
- 증명 안 됨 (50년째)
- 100만 달러 상금
- 밀레니엄 문제 중 하나</code></pre><h3 id="복잡도-이론의-의의">복잡도 이론의 의의</h3>
<p><strong>1. 문제의 본질 이해</strong></p>
<pre><code>알고리즘을 못 찾는 이유:
- 내가 부족해서? ✗
- 문제가 본질적으로 어려워서? ✓

복잡도 이론:
→ 문제의 난이도를 수학적으로 증명
→ &quot;어려운 문제&quot;를 정확히 정의</code></pre><p><strong>2. 실무적 가치</strong></p>
<pre><code>문제를 받으면:
1. 복잡도 클래스 판단
2. 적절한 전략 선택

P 문제: 정확한 알고리즘
NP-완전: 근사/휴리스틱
결정 불가능: 휴리스틱만</code></pre><p><strong>3. 철학적 의미</strong></p>
<pre><code>컴퓨터의 한계:
- 모든 문제를 풀 수 없음
- 증명된 한계 존재
- 하지만 대부분은 해결 가능

인간과 컴퓨터:
- 둘 다 한계가 있음
- 하지만 보완적
- 함께 더 나은 해결</code></pre><hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>복잡도 클래스</strong></p>
<pre><code>P: 다항 시간 해결
NP: 다항 시간 검증
NP-완전: NP 중 가장 어려움
co-NP: &quot;No&quot; 증명 쉬움
PSPACE: 다항 공간
EXPTIME: 지수 시간
결정 불가능: 풀 수 없음</code></pre><p><strong>계층 구조</strong></p>
<pre><code>P ⊆ NP ⊆ PSPACE ⊆ EXPTIME ⊂ Undecidable

확실한 차이:
P ⊊ EXPTIME
PSPACE ⊊ EXPTIME

미해결:
P = NP?
NP = PSPACE?</code></pre><p><strong>실무 전략</strong></p>
<pre><code>P: 정확한 알고리즘
NP-완전: 근사/휴리스틱
PSPACE: 게임 AI
EXPTIME: 작은 입력만
결정 불가능: 휴리스틱</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p>운영체제의 기초 개념으로</p>
<p><strong>[09-01] 프로세스 (Process)</strong></p>
<ul>
<li>프로세스의 정의: 실행 중인 프로그램, 프로그램 vs 프로세스의 차이</li>
<li>프로세스의 구조: 코드, 데이터, 스택, 힙 영역의 역할</li>
<li>프로세스 상태: 생성, 준비, 실행, 대기, 종료 상태 전이</li>
<li>프로세스 제어 블록 (PCB): 운영체제가 프로세스를 관리하는 방법</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[08-08] 정지 문제</a><br><strong>다음 글</strong>: <a href="#">[09-01] 프로세스</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [08-08] 정지 문제 (Halting Problem)]]></title>
            <link>https://velog.io/@road_to_ai/08-08-%EC%A0%95%EC%A7%80-%EB%AC%B8%EC%A0%9C-Halting-Problem</link>
            <guid>https://velog.io/@road_to_ai/08-08-%EC%A0%95%EC%A7%80-%EB%AC%B8%EC%A0%9C-Halting-Problem</guid>
            <pubDate>Fri, 20 Mar 2026 10:55:48 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>정지 문제는 컴퓨터로 풀 수 없는 가장 유명한 문제로, 컴퓨터 과학의 근본적 한계를 보여 줍니다.</p>
</blockquote>
<hr>
<h2 id="🎯-정지-문제란-무엇인가">🎯 정지 문제란 무엇인가</h2>
<h3 id="정지-문제-halting-problem의-정의">정지 문제 (Halting Problem)의 정의</h3>
<p><strong>정지 문제</strong>는 다음 질문에 답하는 것입니다:</p>
<pre><code>주어진 프로그램과 입력에 대해, &quot;이 프로그램이 정지할까? 무한 루프일까?&quot;</code></pre><p><strong>쉬운 비유:</strong></p>
<pre><code>영화 보기:

질문: &quot;이 영화가 언제 끝날까?&quot;

쉬운 경우:
- 90분짜리 영화 → 90분 후 끝남 (명확)

어려운 경우:
- 반복 재생 설정된 영화 → 영원히 안 끝남 → 하지만 처음엔 구분 어려움

정지 문제:
프로그램이 &quot;끝나는 영화&quot;인지 &quot;무한 반복 영화&quot;인지 판단하기</code></pre><h3 id="명백한-경우들">명백한 경우들</h3>
<p>일부 프로그램은 명백히 정지하거나 명백히 정지하지 않습니다.</p>
<pre><code class="language-python"># ===== 명백히 정지하는 프로그램 =====

def clearly_halts_1(n):
    &quot;&quot;&quot;
    간단한 계산만 하고 끝 → 명백히 정지함
    &quot;&quot;&quot;
    return n + 1

def clearly_halts_2(n):
    &quot;&quot;&quot;
    제한된 반복 후 종료 → 명백히 정지함
    &quot;&quot;&quot;
    total = 0
    for i in range(n):  # n번만 반복
        total += i
    return total


# ===== 명백히 정지하지 않는 프로그램 =====

def clearly_infinite_1(n):
    &quot;&quot;&quot;
    무한 루프 → 명백히 정지 안함
    &quot;&quot;&quot;
    while True:
        pass  # 영원히 반복

def clearly_infinite_2(n):
    &quot;&quot;&quot;
    끝나지 않는 카운트 → 명백히 정지 안함
    &quot;&quot;&quot;
    i = 0
    while i &gt;= 0:  # i는 항상 양수
        i += 1  # 계속 증가</code></pre>
<p>이런 간단한 경우는 사람이 쉽게 판단할 수 있습니다.</p>
<h3 id="어려운-경우">어려운 경우</h3>
<p>문제는 <strong>복잡한 프로그램</strong>입니다.</p>
<pre><code class="language-python">def collatz(n):
    &quot;&quot;&quot;
    콜라츠 추측 (Collatz Conjecture)

    규칙:
    - 짝수면: n을 2로 나누기
    - 홀수면: 3n + 1
    - 1이 되면 종료

    질문: 모든 양수 n에 대해 정지하는가?
    &quot;&quot;&quot;
    while n != 1:
        if n % 2 == 0:  # 짝수
            n = n // 2
        else:  # 홀수
            n = 3 * n + 1
    return True

# 예시들
def test_collatz():
    &quot;&quot;&quot;
    콜라츠 추측 테스트
    &quot;&quot;&quot;
    # n = 5
    # 5 → 16 → 8 → 4 → 2 → 1 (정지!)

    # n = 6
    # 6 → 3 → 10 → 5 → 16 → 8 → 4 → 2 → 1 (정지!)

    # n = 27
    # 27 → 82 → 41 → 124 → 62 → 31 → 94 → 47 → ...
    # ... → 1 (111단계 후 정지!)

    # 질문: 모든 n에 대해 정지하는가?
    # 답: 아무도 모름!
    # → 1,000,000,000,000,000,000까지 확인됨
    # → 하지만 모든 n에 대해서는 증명 안됨
    # → 90년 넘게 미해결!</code></pre>
<p><strong>왜 어려운가?</strong></p>
<pre><code>간단해 보이는 규칙이지만:
- 어떤 숫자는 금방 1에 도달
- 어떤 숫자는 오래 걸림
- 패턴을 예측할 수 없음

인간도 판단 못 하는데, 컴퓨터가 자동으로 판단할 수 있을까?
→ 튜링이 증명: 불가능!</code></pre><hr>
<h2 id="🚫-튜링의-증명-정지-문제는-불가능하다">🚫 튜링의 증명: 정지 문제는 불가능하다</h2>
<h3 id="귀류법-증명">귀류법 증명</h3>
<p>튜링은 <strong>귀류법(proof by contradiction)</strong>으로 정지 문제가 불가능함을 증명했습니다.</p>
<p><strong>귀류법이란?</strong></p>
<pre><code>1. 가정: &quot;A가 참이다&quot;
2. 논리 전개: A가 참이면...
3. 결과: 모순 발생!
4. 결론: A는 거짓이다

예:
가정: &quot;가장 큰 소수가 존재한다&quot;
→ 그보다 큰 소수를 만들 수 있음
→ 모순!
→ 결론: 가장 큰 소수는 없다</code></pre><h3 id="튜링의-증명-단계별">튜링의 증명 (단계별)</h3>
<p><strong>1단계: 정지 판단기가 있다고 가정</strong></p>
<pre><code class="language-python"># 가정: 이런 함수가 존재한다고 가정
def halts(program, input_data):
    &quot;&quot;&quot;
    정지 판단기 (가상)

    program: 판단할 프로그램 (함수)
    input_data: 프로그램의 입력

    Returns: True if program(input_data)가 정지
             False if program(input_data)가 무한 루프

    특징:
    - 항상 정확함
    - 모든 프로그램에 대해 작동
    - 스스로는 항상 정지함 (답을 냄)

    이런 함수가 존재한다고 가정!
    &quot;&quot;&quot;
    # 마법처럼 판단한다고 가정
    pass</code></pre>
<p><strong>2단계: 역설 프로그램 만들기</strong></p>
<pre><code class="language-python">def paradox(p):
    &quot;&quot;&quot;
    역설을 만드는 프로그램

    p: 프로그램 (자기 자신을 받을 수도 있음!)

    논리:
    1. halts가 &quot;정지한다&quot;고 판단하면 → 무한 루프 실행 (정지 안 함!)

    2. halts가 &quot;정지 안 한다&quot;고 판단하면 → 즉시 종료 (정지함!)

    핵심:
    halts의 판단과 반대로 행동! → halts가 항상 틀리게 만듦

    &quot;&quot;&quot;
    # p(p)가 정지하는지 판단 (프로그램에 자기 자신을 입력으로 줌)
    if halts(p, p):
        # halts가 &quot;정지한다&quot;고 했으면 → 반대로 무한 루프!
        while True:
            pass  # 영원히 실행
    else:
        # halts가 &quot;정지 안한다&quot;고 했으면 → 반대로 즉시 종료!
        return  # 바로 끝</code></pre>
<p><strong>3단계: 자기 자신에게 적용</strong></p>
<pre><code class="language-python"># 질문: paradox(paradox)는 어떻게 되는가?

# 경우 1: halts(paradox, paradox) = True라고 가정
# → halts: &quot;paradox(paradox)는 정지한다&quot;
# → 하지만 paradox의 코드를 보면:
#    if halts(p, p):  # True
#        while True: pass  # 무한 루프!
# → 실제로는 정지 안함!
# → halts가 틀림!
# → 모순!

# 경우 2: halts(paradox, paradox) = False라고 가정
# → halts: &quot;paradox(paradox)는 정지 안한다&quot;
# → 하지만 paradox의 코드를 보면:
#    if halts(p, p):  # False
#        pass
#    else:
#        return  # 즉시 종료!
# → 실제로는 정지함!
# → halts가 틀림!
# → 모순!</code></pre>
<p><strong>4단계: 결론</strong></p>
<pre><code>두 경우 모두 모순!
→ halts는 paradox(paradox)를 판단할 수 없음
→ halts는 존재할 수 없음!

∴ 정지 문제를 푸는 알고리즘은 존재하지 않는다!</code></pre><h3 id="증명-정리">증명 정리</h3>
<pre><code>핵심 아이디어:

1. &quot;정지 판단기&quot;가 있다고 가정
2. 그 판단과 &quot;반대로 행동&quot;하는 프로그램 만들기
3. 그 프로그램에 &quot;자기 자신&quot;을 입력
4. 모순 발생!
5. 따라서 정지 판단기는 불가능

왜 이런 일이?
→ 자기 참조(self-reference) 때문
→ 프로그램이 자기 자신을 입력으로 받을 수 있음
→ 자기 자신에 대한 예측을 뒤집을 수 있음</code></pre><hr>
<h2 id="📊-정지-문제의-실제-예시">📊 정지 문제의 실제 예시</h2>
<h3 id="예시-1-콜라츠-추측">예시 1: 콜라츠 추측</h3>
<pre><code class="language-python">def collatz_detailed(n):
    &quot;&quot;&quot;
    콜라츠 추측 상세

    규칙:
    - 짝수: n ÷ 2
    - 홀수: 3n + 1
    - 1이면 종료

    주장: 모든 양수는 결국 1에 도달한다

    상태: 미증명 (1937년~)
    &quot;&quot;&quot;
    steps = 0
    original = n
    sequence = [n]

    while n != 1:
        if n % 2 == 0:
            n = n // 2
        else:
            n = 3 * n + 1

        sequence.append(n)
        steps += 1

        # 너무 길면 중단 (무한 루프 방지)
        if steps &gt; 10000:
            print(f&quot;{original}: 10000단계 초과!&quot;)
            return None

    return steps, sequence

# 테스트
print(&quot;콜라츠 추측 테스트:&quot;)
print()

test_numbers = [5, 27, 97, 871]

for n in test_numbers:
    result = collatz_detailed(n)
    if result:
        steps, seq = result
        print(f&quot;n = {n}:&quot;)
        print(f&quot;  단계: {steps}&quot;)
        print(f&quot;  경로: {seq[:5]}...{seq[-3:]}&quot;)
        print()

# 출력 예시:
# n = 5:
#   단계: 5
#   경로: [5, 16, 8, 4, 2]...[4, 2, 1]
#
# n = 27:
#   단계: 111
#   경로: [27, 82, 41, 124, 62]...[4, 2, 1]
#
# n = 97:
#   단계: 118
#   경로: [97, 292, 146, 73, 220]...[4, 2, 1]</code></pre>
<p><strong>왜 증명 못 하는가?</strong></p>
<pre><code>시도한 방법들:
1. 모든 수 확인: 불가능 (무한함)
2. 패턴 찾기: 아직 못 찾음
3. 수학적 증명: 너무 어려움

현재 상황:
- 2⁶⁸ (약 3×10²⁰)까지 확인됨
- 모두 1에 도달함
- 하지만 모든 수에 대한 증명은 없음

정지 문제와 관계:
&quot;collatz(n)이 모든 n에 대해 정지하는가?&quot;
→ 정지 문제의 특수한 경우
→ 일반적인 정지 문제가 불가능하므로 이런 특수한 경우도 어려울 수 있음</code></pre><h3 id="예시-2-비버-게임">예시 2: 비버 게임</h3>
<pre><code class="language-python">def busy_beaver(n):
    &quot;&quot;&quot;
    비버 게임 (Busy Beaver)

    문제:
    n개 상태를 가진 튜링 기계 중 가장 많은 1을 쓰고 정지하는 기계는?

    왜 어려운가?
    - 정지하는 기계만 세야 함
    - 정지 여부를 판단해야 함
    - 정지 문제가 불가능하므로 일반적인 해법 없음

    알려진 값:
    BB(1) = 1
    BB(2) = 6
    BB(3) = 21
    BB(4) = 107
    BB(5) ≥ 47,176,870
    BB(6) &gt; 10¹⁵ (확실하지 않음)

    특징:
    계산 가능한 함수 중 가장 빠르게 증가!
    &quot;&quot;&quot;
    pass</code></pre>
<hr>
<h2 id="💡-실무-의미">💡 실무 의미</h2>
<h3 id="완벽한-도구는-불가능">완벽한 도구는 불가능</h3>
<p><strong>1. 완벽한 디버거</strong></p>
<pre><code class="language-python">def why_perfect_debugger_impossible():
    &quot;&quot;&quot;
    완벽한 디버거가 불가능한 이유

    원하는 기능:
    - 모든 무한 루프 탐지
    - 코드 실행 전에 경고

    왜 불가능:
    → 무한 루프 탐지 = 정지 문제
    → 정지 문제는 불가능
    → 완벽한 디버거도 불가능

    실제 디버거:
    - 간단한 무한 루프는 탐지
    - 복잡한 경우는 못 찾음
    - 타임아웃 같은 휴리스틱 사용
    &quot;&quot;&quot;

    # 간단한 무한 루프: 탐지 가능
    def simple_loop():
        while True:
            pass
    # → 명백한 무한 루프, 정적 분석으로 탐지 가능

    # 복잡한 경우: 탐지 어려움
    def complex_case(n):
        # 콜라츠 추측
        while n != 1:
            if n % 2 == 0:
                n = n // 2
            else:
                n = 3 * n + 1
    # → 정지 여부 불확실, 자동 탐지 불가능</code></pre>
<p><strong>2. 완벽한 최적화 컴파일러</strong></p>
<pre><code class="language-python">def why_perfect_optimizer_impossible():
    &quot;&quot;&quot;
    완벽한 최적화 컴파일러가 불가능한 이유

    원하는 기능:
    - 불필요한 코드 모두 제거
    - 실행 결과는 그대로

    예:
    def compute(n):
        x = expensive_calculation(n)  # 오래 걸림
        return 42  # x를 안 씀!

    최적화:
    def compute_optimized(n):
        return 42  # expensive_calculation 제거!

    왜 불가능:
    - expensive_calculation이 정지하는지 확인 필요
    - 정지 안 하면 원본과 최적화 버전이 다름 (원본: 무한 루프, 최적화: 즉시 42 반환)
    - 정지 여부 판단 = 정지 문제
    - 완벽한 최적화 불가능

    실제 컴파일러:
    - 안전한 최적화만 수행
    - 보수적 접근 (확실한 것만)
    &quot;&quot;&quot;
    pass</code></pre>
<p><strong>3. 완벽한 테스트 커버리지</strong></p>
<pre><code class="language-python">def why_perfect_testing_impossible():
    &quot;&quot;&quot;
    완벽한 테스트가 불가능한 이유

    목표:
    &quot;모든 실행 경로를 테스트&quot;

    문제:
    def mystery_function(n):
        if some_complex_condition(n):  # 복잡한 조건
            return True
        else:
            while complex_loop(n):  # 복잡한 루프
                n = transform(n)
            return False

    질문:
    - if 분기를 테스트하려면?
      → some_complex_condition이 True인 입력 필요
      → 그런 입력이 존재하는가? (판단 어려움)

    - else 분기를 테스트하려면?
      → 루프가 정지해야 함
      → 정지하는가? (정지 문제!)

    결론:
    - 모든 경로 테스트 불가능
    - 대표적 경로만 테스트
    - 100% 커버리지 != 100% 정확성
    &quot;&quot;&quot;
    pass</code></pre>
<h3 id="실용적-접근">실용적 접근</h3>
<p>정지 문제가 불가능하지만, 실무에서는 여전히 유용한 도구들이 있습니다.</p>
<pre><code class="language-python">def practical_approaches():
    &quot;&quot;&quot;
    실무적 접근 방법들
    &quot;&quot;&quot;

    # 1. 타임아웃
    import signal

    def with_timeout(func, timeout_sec):
        &quot;&quot;&quot;
        제한 시간 내에 실행

        완벽하지 않지만 실용적:
        - timeout_sec 안에 안 끝나면 중단
        - 진짜 무한 루프와 느린 프로그램 구분 못함
        - 하지만 대부분의 경우 충분
        &quot;&quot;&quot;
        def timeout_handler(signum, frame):
            raise TimeoutError(&quot;시간 초과!&quot;)

        # 타임아웃 설정
        signal.signal(signal.SIGALRM, timeout_handler)
        signal.alarm(timeout_sec)

        try:
            result = func()
            signal.alarm(0)  # 타임아웃 해제
            return result
        except TimeoutError:
            return None  # 시간 내 안 끝남

    # 2. 정적 분석
    def static_analysis(code):
        &quot;&quot;&quot;
        코드 실행없이 분석

        탐지 가능:
        - 명백한 무한 루프 (while True)
        - 도달 불가능 코드
        - 간단한 패턴

        탐지 불가능:
        - 복잡한 조건의 무한 루프
        - 데이터 의존적 루프
        &quot;&quot;&quot;
        pass

    # 3. 휴리스틱
    def heuristic_check(program):
        &quot;&quot;&quot;
        경험적 규칙

        예:
        - 루프 변수가 증가만 하는가?
        - 종료 조건이 있는가?
        - 재귀 깊이가 제한되는가?

        완벽하지 않지만 도움됨
        &quot;&quot;&quot;
        pass</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>정지 문제</strong></p>
<pre><code>정의:
&quot;프로그램이 정지할까? 무한 루프일까?&quot;

특징:
- 가장 유명한 계산 불가능 문제
- 튜링이 1936년 증명
- 컴퓨터 과학의 근본적 한계</code></pre><p><strong>튜링의 증명</strong></p>
<pre><code>방법: 귀류법

1. 가정: 정지 판단기 halts 존재
2. 역설 프로그램: halts와 반대로 행동
3. 자기 참조: paradox(paradox) 실행
4. 모순: 두 경우 모두 모순
5. 결론: halts는 존재 불가능</code></pre><p><strong>실제 예시</strong></p>
<pre><code>- 콜라츠 추측: 90년째 미해결
- 비버 게임: 가장 빠른 성장 함수
- 복잡한 프로그램: 정지 여부 불확실</code></pre><p><strong>실무 의미</strong></p>
<pre><code>불가능한 도구:
- 완벽한 디버거
- 완벽한 최적화 컴파일러
- 완벽한 테스트

가능한 접근:
- 타임아웃
- 정적 분석 (간단한 경우)
- 휴리스틱 (경험적 규칙)</code></pre><p><strong>철학적 의미</strong></p>
<pre><code>컴퓨터의 한계:
- 모든 문제를 풀 수 없음
- 자기 자신에 대한 완벽한 분석 불가능
- 수학적으로 증명된 한계

하지만:
- 대부분의 실용적 문제는 해결 가능
- 근사와 휴리스틱으로 충분히 유용</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[08-09] 기타 복잡도 클래스</strong></p>
<ul>
<li>co-NP: NP의 여집합, 반대 증명이 쉬운 문제들</li>
<li>PSPACE: 다항 공간으로 해결 가능한 문제들</li>
<li>EXPTIME: 지수 시간이 필요한 문제들</li>
<li>복잡도 클래스 계층: P ⊆ NP ⊆ PSPACE ⊆ EXPTIME</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[08-07] 계산 불가능성</a><br><strong>다음 글</strong>: <a href="#">[08-09] 기타 복잡도 클래스</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [08-07] 계산 불가능성 (Undecidability)]]></title>
            <link>https://velog.io/@road_to_ai/08-07-%EA%B3%84%EC%82%B0-%EB%B6%88%EA%B0%80%EB%8A%A5%EC%84%B1-Undecidability</link>
            <guid>https://velog.io/@road_to_ai/08-07-%EA%B3%84%EC%82%B0-%EB%B6%88%EA%B0%80%EB%8A%A5%EC%84%B1-Undecidability</guid>
            <pubDate>Fri, 20 Mar 2026 10:24:19 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>계산 불가능성은 컴퓨터로 절대 풀 수 없는 문제들이 존재한다는 놀라운 사실을 다룹니다.</p>
</blockquote>
<hr>
<h2 id="🎯-계산-불가능성이란-무엇인가">🎯 계산 불가능성이란 무엇인가</h2>
<h3 id="계산-불가능성의-정의">계산 불가능성의 정의</h3>
<p><strong>계산 불가능(Undecidable)</strong>한 문제는 <strong>어떤 알고리즘으로도 모든 입력에 대해 올바르게 판단할 수 없는</strong> 문제입니다.</p>
<p><strong>쉬운 비유:</strong></p>
<pre><code>마법의 수정구슬로 비유:

가능한 질문: &quot;내일 비가 올까?&quot; → 일기예보로 예측 가능 (완벽하지 않지만)

불가능한 질문: &quot;이 프로그램은 언제까지 실행될까?&quot; → 절대 정확히 알 수 없음! → 수정구슬도 못 봄 → 컴퓨터도 못 봄</code></pre><p><strong>중요한 구분:</strong></p>
<pre><code>어려운 문제 (NP-완전):
- 시간이 오래 걸림
- 하지만 원칙적으로는 풀 수 있음
- 예: 외판원 문제 (느리지만 정확한 답 가능)

불가능한 문제 (계산 불가능):
- 아무리 시간을 줘도 풀지 못함
- 원칙적으로 풀 수 없음
- 예: 정지 문제 (어떤 방법으로도 불가능)

차이:
NP-완전 = 느림
계산 불가능 = 절대 불가능</code></pre><hr>
<h2 id="🚫-왜-불가능한-문제가-존재하는가">🚫 왜 불가능한 문제가 존재하는가?</h2>
<h3 id="컴퓨터의-근본적-한계">컴퓨터의 근본적 한계</h3>
<p><strong>핵심 이유:</strong></p>
<p>프로그램은 자기 자신에 대해 완벽히 분석할 수 없습니다. 이것이 튜링이 발견한 컴퓨터의 근본적 한계입니다.</p>
<p><strong>거울 비유:</strong></p>
<pre><code>상황: 거울로 자기 눈을 보려고 함

문제:
- 왼쪽 눈으로 왼쪽 눈을 볼 수 없음
- 거울에 비친 눈을 보지만, 그건 &quot;직접&quot; 보는 게 아님

프로그램도 마찬가지:
- 프로그램 A가 프로그램 B를 분석: 가능
- 프로그램 A가 자기 자신을 완벽히 분석: 불가능</code></pre><h3 id="간단한-예시-출력-예측-문제">간단한 예시: 출력 예측 문제</h3>
<pre><code class="language-python">def predict_output_problem():
    &quot;&quot;&quot;
    출력 예측 문제

    문제: 프로그램과 입력이 주어졌을 때, &quot;이 프로그램이 &#39;Hello&#39;를 출력할까?&quot;

    왜 불가능한가?
    프로그램이 자기 자신을 분석하려 하면 무한 재귀에 빠짐
    &quot;&quot;&quot;

    # 가상의 예측기가 있다고 가정
    def will_print_hello(program, input_data):
        &quot;&quot;&quot;
        program이 input_data에 대해 &quot;Hello&quot;를 출력하는지 예측

        이런 함수가 존재한다면?
        &quot;&quot;&quot;
        # 마법처럼 예측한다고 가정
        pass

    # 역설 만들기
    def paradox_program(x):
        &quot;&quot;&quot;
        역설을 만드는 프로그램

        논리:
        1. will_print_hello가 &quot;출력한다&quot;고 예측하면 → 출력하지 않음
        2. will_print_hello가 &quot;출력 안 한다&quot;고 예측하면 → 출력함

        결과: will_print_hello는 항상 틀림!
        &quot;&quot;&quot;
        if will_print_hello(paradox_program, x):
            # 예측이 &quot;출력한다&quot;면 출력하지 않기
            return  # 아무것도 출력 안함
        else:
            # 예측이 &quot;출력 안 한다&quot;면 출력하기
            print(&quot;Hello&quot;)

    # will_print_hello(paradox_program, None)을 실행하면?
    # - Yes 답하면: 실제로는 출력 안 함 → 틀림
    # - No 답하면: 실제로는 출력함 → 틀림
    # 
    # 결론: will_print_hello는 존재할 수 없음!

# 이것이 계산 불가능성의 핵심!
# 프로그램이 자기 자신에 대해 완벽히 예측할 수 없음</code></pre>
<p><strong>왜 이런 일이 발생하는가?</strong></p>
<p>프로그램이 자기 자신을 입력으로 받을 수 있기 때문입니다. 이것이 &quot;자기 참조(self-reference)&quot;를 만들고, 자기 참조는 역설을 만듭니다.</p>
<pre><code>유명한 역설: &quot;이 문장은 거짓이다&quot; → 참이면 거짓, 거짓이면 참 → 모순!

프로그램도 마찬가지:
프로그램이 자기 자신에 대해 예측하면
→ 예측에 따라 행동을 바꿀 수 있음
→ 예측이 항상 틀리게 만들 수 있음
→ 완벽한 예측 불가능!</code></pre><hr>
<h2 id="📋-계산-불가능한-문제들">📋 계산 불가능한 문제들</h2>
<h3 id="예시-1-프로그램-동치-문제-program-equivalence-problem">예시 1: 프로그램 동치 문제 (Program Equivalence Problem)</h3>
<ul>
<li>프로그램 동치 문제 : 두 개의 서로 다른 프로그램(또는 알고리즘)이 모든 가능한 입력에 대해 동일한 출력(결과)을 내놓는지 판별하는 문제</li>
</ul>
<p><strong>문제:</strong></p>
<pre><code>두 프로그램이 주어졌을 때, &quot;이 두 프로그램은 같은 일을 하는가?&quot;

예:
프로그램 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는 동치인가?
→ 사람은 &quot;같다&quot;고 알 수 있음
→ 하지만 일반적으로 컴퓨터는 판단 못함!</code></pre><p><strong>왜 불가능한가?</strong></p>
<pre><code class="language-python">def program_equivalence_impossible():
    &quot;&quot;&quot;
    프로그램 동치 문제가 왜 불가능한가?

    이유:
    두 프로그램이 &quot;같다&quot;는 것을 증명하려면 모든 가능한 입력에 대해 결과가 같아야 함

    문제:
    1. 입력이 무한할 수 있음
    2. 프로그램이 무한 루프일 수 있음
    3. 프로그램의 행동이 복잡할 수 있음

    결과:
    모든 경우를 확인할 수 없음!
    &quot;&quot;&quot;

    # 예: 이 두 프로그램이 같은가?
    def program1(n):
        &quot;&quot;&quot;
        콜라츠 추측 검증
        모든 n에 대해 1에 도달하면 True
        &quot;&quot;&quot;
        while n != 1:
            if n % 2 == 0:
                n = n // 2
            else:
                n = 3 * n + 1
        return True

    def program2(n):
        &quot;&quot;&quot;
        항상 True 반환
        &quot;&quot;&quot;
        return True

    # 질문: program1과 program2는 동치인가?
    # 
    # 만약 모든 n에 대해 콜라츠 추측이 참이면: 동치
    # 하지만 콜라츠 추측은 아직 증명 안됨!
    # 
    # 따라서 동치 여부를 판단할 수 없음</code></pre>
<h3 id="예시-2-프로그램-특성-문제">예시 2: 프로그램 특성 문제</h3>
<p><strong>Rice의 정리:</strong></p>
<pre><code>프로그램의 의미론적 특성은 계산 불가능하다

의미론적 특성이란?
- &quot;이 프로그램이 짝수만 출력하는가?&quot;
- &quot;이 프로그램이 항상 종료하는가?&quot;
- &quot;이 프로그램이 &#39;Hello&#39;를 출력하는가?&quot;

Rice의 정리:
이런 질문들에 대해 모든 프로그램을 정확히 분류하는 알고리즘은 존재하지 않는다!</code></pre><p><strong>실용적 예시:</strong></p>
<pre><code class="language-python">def rice_theorem_example():
    &quot;&quot;&quot;
    Rice의 정리 예시

    불가능한 질문들:
    &quot;&quot;&quot;

    # 질문 1: 이 프로그램이 항상 양수를 반환하는가?
    def always_positive(program):
        &quot;&quot;&quot;
        계산 불가능!

        이유:
        모든 입력에 대해 확인해야 하는데 입력이 무한할 수 있음
        &quot;&quot;&quot;
        pass

    # 질문 2: 이 프로그램이 배열을 정렬하는가?
    def is_sorting_program(program):
        &quot;&quot;&quot;
        계산 불가능!

        이유:
        프로그램이 &quot;의도적으로&quot; 정렬하는지, 아니면 우연히 정렬된 결과를 내는지 판단할 수 없음
        &quot;&quot;&quot;
        pass

    # 질문 3: 이 프로그램이 변수 x를 사용하는가?
    def uses_variable_x(program):
        &quot;&quot;&quot;
        이건 가능!

        이유:
        프로그램 코드를 분석하면 됨 (구문론적)
        실행 결과를 볼 필요 없음

        Rice의 정리는 의미론적 특성에만 적용됨
        &quot;&quot;&quot;
        # 코드 텍스트에서 &#39;x&#39; 찾기
        return &#39;x&#39; in program</code></pre>
<p><strong>핵심:</strong></p>
<pre><code>가능 (구문론적 특성):
- 코드에 &#39;x&#39;가 있는가?
- 줄 수가 100줄 이상인가?
- for 루프를 사용하는가?

불가능 (의미론적 특성):
- 실행하면 &#39;x&#39;를 사용하는가?
- 100번 이상 반복하는가?
- 결과가 정렬되는가?

차이:
구문론적 = 코드만 봐도 알 수 있음
의미론적 = 실행해봐야 알 수 있음</code></pre><hr>
<h2 id="🔧-환원으로-불가능성-증명하기">🔧 환원으로 불가능성 증명하기</h2>
<h3 id="환원의-활용">환원의 활용</h3>
<p>불가능한 문제가 하나 있으면, 환원으로 다른 문제도 불가능함을 증명할 수 있습니다.</p>
<p><strong>방법:</strong></p>
<pre><code>1. 알려진 불가능 문제: A (예: 정지 문제)
2. 증명하고 싶은 문제: B

3. A ≤ B 환원 만들기 (A를 B로 변환)

4. 결론:
   만약 B를 풀 수 있다면
   → A도 풀 수 있다 (환원으로)
   → 하지만 A는 불가능
   → 모순!
   → 따라서 B도 불가능!</code></pre><p><strong>예시: 정지 문제 → 출력 예측 문제</strong></p>
<pre><code class="language-python">def halting_to_output_reduction():
    &quot;&quot;&quot;
    정지 문제를 출력 예측 문제로 환원

    목표:
    출력 예측 문제도 불가능함을 증명

    방법:
    정지 문제를 풀 수 있다면 출력 예측 문제로 변환해서 풀 수 있음을 보임
    &quot;&quot;&quot;

    # 가정: 출력 예측기가 있다고 가정
    def will_output_yes(program, input_data):
        &quot;&quot;&quot;
        program(input_data)가 &quot;YES&quot;를 출력하는가?

        이게 가능하다고 가정
        &quot;&quot;&quot;
        pass

    # 정지 문제 풀기
    def solve_halting(program, input_data):
        &quot;&quot;&quot;
        정지 문제를 출력 예측으로 풀기

        아이디어:
        1. 원래 program을 변형
        2. 정지하면 &quot;YES&quot; 출력하는 새 프로그램 만들기
        3. 새 프로그램이 &quot;YES&quot; 출력하는지 확인
        &quot;&quot;&quot;

        # 1단계: 변환 - 새 프로그램 만들기
        def modified_program(x):
            &quot;&quot;&quot;
            원래 프로그램을 감싼 프로그램

            행동:
            - program(input_data) 실행
            - 정지하면 &quot;YES&quot; 출력
            - 무한 루프면 출력 안함
            &quot;&quot;&quot;
            program(input_data)  # 원래 프로그램 실행
            print(&quot;YES&quot;)  # 정지했으면 여기 도달

        # 2단계: 출력 예측기로 확인
        # modified_program이 &quot;YES&quot; 출력하는가?
        # = program이 정지하는가?
        return will_output_yes(modified_program, input_data)

    # 결론:
    # will_output_yes가 존재하면
    # → solve_halting으로 정지 문제 풀 수 있음
    # → 하지만 정지 문제는 불가능
    # → 모순!
    # → will_output_yes도 존재할 수 없음!</code></pre>
<p><strong>환원 그림:</strong></p>
<pre><code>정지 문제 (불가능)
    ↓ 환원
출력 예측 문제
    ↓
만약 풀 수 있다면
    ↓
정지 문제도 풀 수 있음
    ↓
모순! (정지 문제는 불가능)
    ↓
따라서 출력 예측도 불가능</code></pre><hr>
<h2 id="🎨-실생활-비유">🎨 실생활 비유</h2>
<h3 id="비유-1-거짓말쟁이-역설">비유 1: 거짓말쟁이 역설</h3>
<pre><code>크레타 사람:
&quot;모든 크레타 사람은 거짓말쟁이다&quot;

분석:
- 이 말이 참이면: 크레타 사람이  → 이 말도 거짓  → 모순!

- 이 말이 거짓이면: 크레타 사람이 정직  → 이 말이 참  → 모순!

결론: 자기 참조는 역설을 만듦

프로그램:
프로그램이 자기 자신에 대해 판단하면 → 비슷한 역설 발생 → 완벽한 판단 불가능</code></pre><h3 id="비유-2-이발사-역설">비유 2: 이발사 역설</h3>
<pre><code>마을 규칙:
&quot;이발사는 자기 자신을 면도하지 않는 모든 사람을 면도한다&quot;

질문: 이발사는 자기를 면도하는가?

- 면도한다면:
  → &quot;자기를 면도하지 않는 사람&quot;이 아님  → 면도하면 안됨  → 모순!

- 면도하지 않는다면:
  → &quot;자기를 면도하지 않는 사람&quot;임  → 면도해야 함  → 모순!

결론: 이런 이발사는 존재할 수 없음

프로그램:
&quot;자기를 분석하는 프로그램&quot;도 비슷한 이유로 존재할 수 없음</code></pre><hr>
<h2 id="💡-실무-의미">💡 실무 의미</h2>
<h3 id="계산-불가능성의-영향">계산 불가능성의 영향</h3>
<p><strong>1. 완벽한 도구는 불가능</strong></p>
<pre><code>불가능한 도구들:
- 완벽한 바이러스 검사기  → 모든 바이러스를 찾는 건 불가능

- 완벽한 버그 찾기  → 모든 버그를 자동으로 찾는 건 불가능

- 완벽한 최적화 컴파일러  → 항상 최적 코드 생성은 불가능

이유:
이런 도구들은 프로그램의 의미론적 특성을 완벽히 분석해야 하는데, Rice의 정리에 의해 불가능</code></pre><p><strong>2. 근사 (Approximation)와 휴리스틱 (Heuristic) 필요</strong></p>
<ul>
<li>근사와 휴리스틱: 복잡한 문제나 정보가 부족한 상황에서 완벽한 정답 대신 &#39;충분히 좋은&#39; 해를 빠르게 찾기 위한 실용적인 접근 방식<ul>
<li>근사: 최적해(Optimal solution)에 가까운 해(근사해)를 찾기 위해 논리적/수학적 기법을 사용</li>
<li>휴리스틱: &#39;발견법&#39; 또는 &#39;경험 법칙&#39;이라고도 하며, 경험에 기반하여 문제를 대략적으로 해결하는 간편한 방법</li>
</ul>
</li>
</ul>
<pre><code class="language-python">def practical_approach():
    &quot;&quot;&quot;
    실무에서의 접근

    완벽은 불가능하지만, &quot;대부분의 경우&quot;에 작동하는 도구는 가능!
    &quot;&quot;&quot;

    # 바이러스 검사
    def virus_scanner_practical(program):
        &quot;&quot;&quot;
        완벽하진 않지만 실용적

        접근:
        1. 알려진 바이러스 패턴 확인
        2. 의심스러운 행동 탐지
        3. 샌드박스에서 실행

        한계:
        - 새로운 바이러스는 못 찾을 수 있음
        - 교묘한 바이러스는 놓칠 수 있음

        하지만:
        - 대부분의 바이러스는 찾음
        - 실무에서 충분히 유용
        &quot;&quot;&quot;
        pass

    # 버그 찾기
    def bug_finder_practical(program):
        &quot;&quot;&quot;
        완벽하진 않지만 도움됨

        접근:
        1. 정적 분석 (코드만 봄)
        2. 동적 분석 (테스트 실행)
        3. 알려진 패턴 확인

        한계:
        - 모든 버그를 못 찾음
        - 거짓 양성 가능

        하지만:
        - 많은 버그를 찾아줌
        - 코드 품질 향상
        &quot;&quot;&quot;
        pass</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>계산 불가능성</strong></p>
<pre><code>정의:
어떤 알고리즘으로도 모든 입력에 대해 올바르게 판단할 수 없는 문제

특징:
- 시간 문제가 아님 (아무리 기다려도 안 됨)
- 컴퓨터의 근본적 한계
- 자기 참조로 인한 역설</code></pre><p><strong>왜 불가능한가?</strong></p>
<pre><code>핵심 이유:
프로그램이 자기 자신에 대해 완벽히 분석할 수 없음

역설:
- 예측기가 예측하면
- 프로그램이 예측 반대로 행동
- 예측이 항상 틀림
- 완벽한 예측 불가능</code></pre><p><strong>대표 예시</strong></p>
<pre><code>- 정지 문제
- 프로그램 동치 문제
- 출력 예측 문제
- Rice의 정리 (의미론적 특성)</code></pre><p><strong>실무 의미</strong></p>
<pre><code>불가능:
- 완벽한 바이러스 검사
- 완벽한 버그 찾기
- 완벽한 최적화

가능:
- 대부분의 경우 작동
- 휴리스틱과 근사
- 실용적 도구</code></pre><p><strong>환원 활용</strong></p>
<pre><code>불가능성 증명:
1. 알려진 불가능 문제 A
2. A ≤ B 환원
3. B도 불가능 증명

예: 정지 문제 → 출력 예측 → 불가능</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[08-08] 정지 문제 (Halting Problem)</strong></p>
<ul>
<li>정지 문제의 정의: 프로그램이 멈추는지 판단하기</li>
<li>튜링의 증명: 귀류법으로 불가능성 증명</li>
<li>콜라츠 추측: 여전히 풀리지 않은 정지 문제</li>
<li>실무 영향: 완벽한 디버거가 불가능한 이유</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[08-06] 다항 시간 환원</a><br><strong>다음 글</strong>: <a href="#">[08-08] 정지 문제</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [08-06] 다항 시간 환원 (Polynomial-time Reduction)]]></title>
            <link>https://velog.io/@road_to_ai/08-06-%EB%8B%A4%ED%95%AD-%EC%8B%9C%EA%B0%84-%ED%99%98%EC%9B%90-Polynomial-time-Reduction</link>
            <guid>https://velog.io/@road_to_ai/08-06-%EB%8B%A4%ED%95%AD-%EC%8B%9C%EA%B0%84-%ED%99%98%EC%9B%90-Polynomial-time-Reduction</guid>
            <pubDate>Fri, 13 Mar 2026 04:14:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>다항 시간 환원은 한 문제를 다른 문제로 변환하는 방법으로, 문제들의 난이도를 비교하고 NP-완전을 증명하는 핵심 도구입니다.</p>
</blockquote>
<hr>
<h2 id="🎯-환원이란-무엇인가">🎯 환원이란 무엇인가</h2>
<h3 id="환원의-직관적-이해">환원의 직관적 이해</h3>
<p><strong>환원(Reduction)</strong>은 어려운 문제를 이미 아는 문제로 바꿔서 푸는 것입니다.</p>
<p><strong>실생활 비유:</strong></p>
<pre><code>상황: 외국어로 된 편지를 읽어야 함

어려운 방법: 외국어를 처음부터 배우기 → 몇 년 걸림

환원 방법:
1. 편지를 번역기에 넣기 (변환)
2. 한국어로 읽기 (이미 아는 문제 풀기)
3. 이해 완료!

핵심: 모르는 문제 → 아는 문제로 변환 → 해결</code></pre><p><strong>프로그래밍 예시:</strong></p>
<pre><code class="language-python"># 문제 A: 배열의 최댓값과 최솟값 차이는?
def range_of_array_hard(arr):
    &quot;&quot;&quot;
    직접 풀기: 어떻게 하지?
    &quot;&quot;&quot;
    pass

# 문제 B: 배열의 최댓값은? (이미 안다고 가정)
def max_of_array(arr):
    &quot;&quot;&quot;max() 함수로 쉽게 풀 수 있음&quot;&quot;&quot;
    return max(arr)

# 문제 C: 배열의 최솟값은? (이미 안다고 가정)
def min_of_array(arr):
    &quot;&quot;&quot;min() 함수로 쉽게 풀 수 있음&quot;&quot;&quot;
    return min(arr)

# 환원: A를 B와 C로 변환!
def range_of_array_easy(arr):
    &quot;&quot;&quot;
    환원을 사용한 해결

    핵심 아이디어:
    - &quot;차이&quot;는 &quot;최댓값 - 최솟값&quot;
    - 최댓값, 최솟값은 이미 알고 있음
    - 그러므로 문제 A를 B, C로 변환 가능!

    단계:
    1. 문제 A를 문제 B, C로 변환
    2. 문제 B, C 각각 풀기 (쉬움!)
    3. 결과 조합
    &quot;&quot;&quot;
    # 1단계: 입력 그대로 사용 (변환 없음)

    # 2단계: B와 C 풀기
    maximum = max_of_array(arr)  # 문제 B
    minimum = min_of_array(arr)  # 문제 C

    # 3단계: 결과 조합
    result = maximum - minimum

    return result

# 사용
arr = [3, 1, 4, 1, 5, 9, 2, 6]
print(f&quot;범위: {range_of_array_easy(arr)}&quot;)  # 9 - 1 = 8</code></pre>
<p>이것이 환원입니다! 모르는 문제를 아는 문제들로 바꿔서 푸는 것!</p>
<hr>
<h2 id="📐-다항-시간-환원의-정의">📐 다항 시간 환원의 정의</h2>
<h3 id="형식적-정의">형식적 정의</h3>
<p><strong>기호: A ≤ₚ B</strong></p>
<pre><code>읽기: &quot;A는 B로 다항 시간 환원된다&quot;

의미: &quot;B를 다항 시간에 풀 수 있으면 A도 다항 시간에 풀 수 있다&quot;

즉:
- B가 쉬우면 A도 쉽다
- A가 어려우면 B도 어렵다</code></pre><p><strong>환원의 3단계:</strong></p>
<pre><code>1단계: 입력 변환 (다항 시간)
   A의 입력 → B의 입력

2단계: B 문제 풀기
   B의 알고리즘 실행

3단계: 결과 변환 (다항 시간)
   B의 답 → A의 답</code></pre><h3 id="환원-예시-1-독립-집합-→-클리크">환원 예시 1: 독립 집합 → 클리크</h3>
<p><strong>두 문제 정의:</strong></p>
<pre><code>독립 집합 (Independent Set): k개 정점을 선택하되, 선택된 정점들끼리 간선이 없어야 함

예:
그래프: A-B, B-C (A와 B 연결, B와 C 연결)
k=2 독립 집합: {A, C} (A와 C는 연결 안 됨)

클리크 (Clique): k개 정점을 선택하되, 선택된 정점들끼리 모두 간선으로 연결되어야 함

예:
그래프: A-B, B-C, A-C (모두 연결)
k=3 클리크: {A, B, C} (모두 연결됨)</code></pre><p><strong>환원 아이디어:</strong></p>
<p>독립 집합의 &quot;간선 없음&quot;은 여집합 그래프의 &quot;간선 있음&quot;과 같습니다!</p>
<pre><code class="language-python">def independent_set_to_clique(graph, k):
    &quot;&quot;&quot;
    독립 집합 문제를 클리크 문제로 환원

    핵심 아이디어:
    - 원래 그래프에서 &quot;연결 안 됨&quot; = 여집합 그래프에서 &quot;연결됨&quot;

    - 그러므로 원래 그래프의 독립 집합 = 여집합 그래프의 클리크

    단계:
    1. 그래프의 여집합 만들기 (다항 시간)
    2. 여집합에서 k-클리크 찾기
    3. 그 클리크 = 원래 그래프의 독립 집합

    graph: 그래프 {정점: [이웃들]}
    k: 찾을 정점 개수

    Returns: 독립 집합 (클리크 문제로 풀어서)
    &quot;&quot;&quot;
    # 1단계: 입력 변환 (여집합 그래프 만들기)
    complement = create_complement_graph(graph)

    # 2단계: 클리크 문제 풀기 (여기서는 클리크 문제를 풀 수 있다고 가정)
    clique = find_clique(complement, k)

    # 3단계: 결과 변환 (여집합의 클리크 = 원래의 독립 집합)
    independent_set = clique

    return independent_set

def create_complement_graph(graph):
    &quot;&quot;&quot;
    그래프의 여집합 생성

    여집합(Complement)이란?
    - 원래 간선 있음 → 여집합 간선 없음
    - 원래 간선 없음 → 여집합 간선 있음
    - 즉, 간선 유무를 완전히 뒤집기!

    왜 이렇게 하는가?
    - 독립 집합: 간선 없는 정점들
    - 클리크: 간선 있는 정점들
    - 여집합에서 간선 관계가 반대가 되므로 독립 집합 ↔ 클리크 변환 가능!

    시간복잡도: O(V²)
    - 모든 정점 쌍을 확인해야 함
    - V개 정점이면 V×V 쌍
    - 다항 시간!
    &quot;&quot;&quot;
    # 1. 모든 정점 리스트 만들기
    vertices = list(graph.keys())

    # 2. 빈 여집합 그래프 초기화
    complement = {}
    for vertex in vertices:
        complement[vertex] = []

    # 3. 모든 정점 쌍 (v1, v2) 확인
    for i in range(len(vertices)):
        v1 = vertices[i]

        # i+1부터 시작하는 이유:
        # - 자기 자신과는 간선 만들지 않음 (v1-v1 같은 건 없음)
        # - 이미 확인한 쌍은 건너뛰기 (v1-v2 확인했으면 v2-v1은 같음)
        for j in range(i + 1, len(vertices)):
            v2 = vertices[j]

            # 원래 그래프에 v1-v2 간선이 없는가?
            if v2 not in graph[v1]:
                # 없으면 여집합에는 있어야 함!
                complement[v1].append(v2)
                complement[v2].append(v1)  # 양방향 간선

    return complement

# ===== 사용 예시 =====

# 원래 그래프
graph = {
    &#39;A&#39;: [&#39;B&#39;],      # A-B만 연결
    &#39;B&#39;: [&#39;A&#39;, &#39;C&#39;], # B-C도 연결
    &#39;C&#39;: [&#39;B&#39;],
    &#39;D&#39;: []          # D는 독립
}

print(&quot;원래 그래프:&quot;)
print(&quot;  간선: A-B, B-C&quot;)
print(&quot;  D는 독립적&quot;)

# 여집합 그래프 만들기
complement = create_complement_graph(graph)

print(&quot;여집합 그래프:&quot;)
print(f&quot;  {complement}&quot;)
print(&quot;  원래 없던 간선들만 있음!&quot;)

# 독립 집합 {A, C, D}는 여집합에서 클리크가 됨!
print(&quot;분석:&quot;)
print(&quot;  원래: A와 C는 연결 안 됨 (독립)&quot;)
print(&quot;  여집합: A와 C는 연결됨 (클리크)&quot;)</code></pre>
<p><strong>환원 시간 복잡도:</strong></p>
<pre><code>1단계: 여집합 만들기 = O(V²)
2단계: 클리크 찾기 = 클리크 알고리즘 시간
3단계: 결과 변환 = O(1)

총: O(V²) + 클리크 시간 → 다항 시간 환원!</code></pre><hr>
<h2 id="🔗-환원의-성질">🔗 환원의 성질</h2>
<h3 id="성질-1-추이성-transitivity">성질 1: 추이성 (Transitivity)</h3>
<p><strong>추이성</strong>은 &quot;A → B이고 B → C이면 A → C&quot;라는 성질입니다.</p>
<pre><code>만약:
- A ≤ₚ B (A를 B로 환원 가능)
- B ≤ₚ C (B를 C로 환원 가능)

그러면: A ≤ₚ C (A를 C로 환원 가능)

비유: 번역과 같음
- 한국어 → 영어 번역 가능
- 영어 → 프랑스어 번역 가능
→ 한국어 → 프랑스어 번역 가능 (영어 거쳐서)</code></pre><p><strong>코드 예시:</strong></p>
<pre><code class="language-python">def transitivity_example():
    &quot;&quot;&quot;
    환원의 추이성 예시

    문제 A: 배열 정렬되어 있는가?
    문제 B: 배열 정렬하기
    문제 C: 배열 병합하기 (정렬된 두 배열)

    환원:
    A ≤ₚ B: 정렬해서 비교
    B ≤ₚ C: 병합 정렬 사용

    추이성: A ≤ₚ C: A를 C로 직접 환원 가능
    &quot;&quot;&quot;

    # A ≤ₚ B: A를 B로 환원
    def is_sorted_via_sort(arr):
        &quot;&quot;&quot;
        정렬되어 있는가? (정렬 이용)
        &quot;&quot;&quot;
        sorted_arr = sort_array(arr)  # B 문제 사용
        return arr == sorted_arr

    # B ≤ₚ C: B를 C로 환원
    def sort_array(arr):
        &quot;&quot;&quot;
        정렬하기 (병합 이용)
        &quot;&quot;&quot;
        if len(arr) &lt;= 1:
            return arr

        mid = len(arr) // 2
        left = sort_array(arr[:mid])
        right = sort_array(arr[mid:])

        return merge_arrays(left, right)  # C 문제 사용

    # C 문제: 병합
    def merge_arrays(left, right):
        &quot;&quot;&quot;정렬된 두 배열 병합&quot;&quot;&quot;
        result = []
        i = j = 0

        while i &lt; len(left) and j &lt; len(right):
            if left[i] &lt;= right[j]:
                result.append(left[i])
                i += 1
            else:
                result.append(right[j])
                j += 1

        result.extend(left[i:])
        result.extend(right[j:])
        return result

    # 추이성: A ≤ₚ C
    # A를 푸는데 B를 거쳐 C를 사용 → A를 C로 직접 환원한 것과 같음

    arr = [1, 2, 3, 4, 5]
    print(f&quot;정렬됨? {is_sorted_via_sort(arr)}&quot;)

transitivity_example()</code></pre>
<h3 id="성질-2-방향성">성질 2: 방향성</h3>
<p>환원은 <strong>방향</strong>이 있습니다.</p>
<pre><code>A ≤ₚ B이지만 B ≤ₚ A는 아닐 수 있음

예: 정렬 여부 확인 ≤ₚ 정렬 (정렬할 수 있으면 확인도 가능)

하지만: 정렬 ≤ₚ 정렬 여부 확인 (✗)  (확인만 할 수 있다고 정렬할 수 있는 건 아님)

비유: 읽기 ≤ₚ 쓰기 (쓸 수 있으면 읽을 수 있음)
     하지만 쓰기 ≤ₚ 읽기 (✗) (읽을 수 있다고 쓸 수 있는 건 아님)</code></pre><h3 id="성질-3-np-완전-증명에-사용">성질 3: NP-완전 증명에 사용</h3>
<p><strong>NP-완전 증명 방법:</strong></p>
<pre><code>문제 X가 NP-완전임을 증명하려면:

1단계: X ∈ NP 증명 → 다항 시간 검증자 만들기

2단계: 알려진 NP-완전 Y 선택 → 예: SAT, 클리크, 해밀턴 경로

3단계: Y ≤ₚ X 증명 → Y를 X로 다항 시간 환원

결론: X는 NP-완전!</code></pre><hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<h3 id="환원-설계-전략">환원 설계 전략</h3>
<p><strong>전략 1: 유사한 구조 찾기</strong></p>
<pre><code class="language-python">def find_reduction_strategy(problem_A, problem_B):
    &quot;&quot;&quot;
    두 문제 사이의 환원 찾기

    체크리스트:
    1. 입력이 비슷한가?
       - 둘 다 그래프?
       - 둘 다 수열?

    2. 제약 조건이 비슷한가?
       - 둘 다 &quot;선택&quot; 문제?
       - 둘 다 &quot;연결성&quot; 문제?

    3. 변환이 간단한가?
       - 간선 추가/제거?
       - 정점 추가/제거?

    4. 여집합/반대 개념인가?
       - 독립 집합 ↔ 클리크
       - 최대 ↔ 최소
    &quot;&quot;&quot;
    pass</code></pre>
<p><strong>전략 2: 가젯(Gadget) 사용</strong></p>
<pre><code>가젯: 작은 구조를 조합해서 큰 구조 만들기

예: SAT → 해밀턴 경로
1. 각 변수 → 작은 그래프 (가젯)
2. 각 절 → 연결 구조 (가젯)
3. 조합 → 전체 그래프

장점: 복잡한 변환을 단순한 부품으로</code></pre><h3 id="환원-검증-방법">환원 검증 방법</h3>
<pre><code class="language-python">def verify_reduction(A_input, B_input, A_solver, B_solver):
    &quot;&quot;&quot;
    환원이 올바른지 검증

    확인 사항:
    1. A의 Yes 입력 → B의 Yes 입력
    2. A의 No 입력 → B의 No 입력
    3. 변환 시간이 다항인가?

    A_input: 문제 A의 입력
    B_input: A_input을 변환한 B의 입력
    A_solver: A를 푸는 (가상의) 함수
    B_solver: B를 푸는 함수
    &quot;&quot;&quot;
    # A의 답
    A_answer = A_solver(A_input)

    # B의 답
    B_answer = B_solver(B_input)

    # 같아야 함!
    if A_answer == B_answer:
        print(&quot;✓ 환원 올바름&quot;)
        return True
    else:
        print(&quot;✗ 환원 잘못됨&quot;)
        return False</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>다항 시간 환원</strong></p>
<pre><code>정의: A ≤ₚ B 즉 &quot;A를 B로 다항 시간에 변환 가능&quot;

의미:
- B를 풀 수 있으면 A도 풀 수 있음
- B가 쉬우면 A도 쉬움
- A가 어려우면 B도 어려움</code></pre><p><strong>환원의 3단계</strong></p>
<pre><code>1. 입력 변환 (다항 시간)
   A의 입력 → B의 입력

2. B 문제 풀기
   B의 알고리즘 실행

3. 결과 변환 (다항 시간)
   B의 답 → A의 답</code></pre><p><strong>환원의 성질</strong></p>
<pre><code>추이성: A ≤ₚ B이고 B ≤ₚ C이면 A ≤ₚ C

방향성: A ≤ₚ B이지만 B ≤ₚ A는 아닐 수 있음

NP-완전 증명: 알려진 NP-완전 Y에서 새 문제 X로 환원 → X도 NP-완전</code></pre><p><strong>실무 활용</strong></p>
<pre><code>1. 문제 난이도 비교
2. NP-완전 증명
3. 알고리즘 재사용
4. 복잡한 문제를 알려진 문제로 변환</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[08-07] 계산 불가능성 (Undecidability)</strong></p>
<ul>
<li>계산 불가능성의 정의: 컴퓨터로 절대 풀 수 없는 문제들</li>
<li>결정 가능 vs 불가능: 튜링 기계의 한계</li>
<li>대표 예시: 정지 문제, 타일링 문제</li>
<li>환원으로 불가능성 증명: 정지 문제를 다른 문제로 환원</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[08-05] NP-난해</a><br><strong>다음 글</strong>: <a href="#">[08-07] 계산 불가능성</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [08-05] NP-난해 (NP-Hard)]]></title>
            <link>https://velog.io/@road_to_ai/08-05-NP-%EB%82%9C%ED%95%B4-NP-Hard</link>
            <guid>https://velog.io/@road_to_ai/08-05-NP-%EB%82%9C%ED%95%B4-NP-Hard</guid>
            <pubDate>Thu, 12 Mar 2026 04:31:11 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>NP-난해는 적어도 NP-완전만큼 어려운 문제들의 집합으로, NP에 속할 필요가 없는 더 일반적인 개념입니다.</p>
</blockquote>
<hr>
<h2 id="🎯-np-난해란-무엇인가">🎯 NP-난해란 무엇인가</h2>
<h3 id="np-난해의-정의">NP-난해의 정의</h3>
<p><strong>NP-난해(NP-Hard)</strong>는 <strong>적어도 NP-완전만큼 어려운</strong> 문제들입니다.</p>
<p><strong>쉬운 비유:</strong></p>
<pre><code>게임 난이도로 비유하면:

쉬움 (P 문제):
- 누구나 클리어 가능
- 시간 들이면 풀림
- 예: 퍼즐 맞추기

어려움 (NP 문제):
- 답 확인은 쉬움
- 찾기는 어려움
- 예: 스도쿠 (확인은 쉬움)

최고 난이도 (NP-완전):
- NP 중 가장 어려움
- 하나만 풀면 다 풀림
- 예: 스도쿠 풀기

초월 난이도 (NP-난해):
- NP-완전만큼 또는 더 어려움
- 답 확인도 어려울 수 있음
- 예: &quot;최적의 스도쿠 전략은?&quot; (답 확인도 어려움)</code></pre><p><strong>핵심 차이:</strong></p>
<pre><code>NP-완전:
1. NP에 속함 ✓
   → 답을 빠르게 확인할 수 있음
2. 매우 어려움 ✓

NP-난해:
1. NP에 속함 ? (필수 아님!)
   → 답 확인도 어려울 수 있음
2. 매우 어려움 ✓

즉:
모든 NP-완전은 NP-난해
하지만 모든 NP-난해가 NP-완전은 아님!</code></pre><hr>
<h2 id="📊-복잡도-클래스-관계">📊 복잡도 클래스 관계</h2>
<h3 id="전체-그림">전체 그림</h3>
<pre><code>        전체 문제들
    ┌─────────────────────┐
    │    NP-난해           │  ← 매우 어려운 문제들
    │  ┌──────────────┐   │
    │  │     NP       │   │  ← 확인은 쉬운 문제들
    │  │ ┌──────────┐ │   │
    │  │ │ NP-완전   │ │   │  ← NP 중 가장 어려움
    │  │ │  ┌────┐  │ │   │
    │  │ │  │ P  │  │ │   │  ← 풀기도 쉬운 문제들
    │  │ │  └────┘  │ │   │
    │  │ └──────────┘ │   │
    │  └──────────────┘   │
    │   NP-난해이지만       │
    │   NP는 아닌 문제들:   │
    │   - 정지 문제        │
    │   - 최적화 문제들     │
    └─────────────────────┘

포함 관계:
P ⊆ NP
NP-완전 ⊆ NP
NP-완전 ⊆ NP-난해  ← 주목!</code></pre><hr>
<h2 id="🔍-결정-문제-vs-최적화-문제">🔍 결정 문제 vs 최적화 문제</h2>
<h3 id="왜-최적화-문제는-np-난해인가">왜 최적화 문제는 NP-난해인가?</h3>
<p><strong>핵심 아이디어:</strong></p>
<p>결정 문제는 Yes/No로 답하지만, 최적화 문제는 &quot;최선의 답&quot;을 찾아야 합니다. 그리고 그것이 정말 최선인지 증명하기가 어렵습니다.</p>
<pre><code>결정 문제:
&quot;1000km 이하 경로가 있나?&quot;
→ Yes/No 답
→ 경로 하나만 주면 확인 가능 (거리 계산해서 비교)
→ 다항 시간 검증 ✓
→ NP에 속함

최적화 문제:
&quot;최단 경로는?&quot;
→ 숫자 답 (예: 850km)
→ 이것이 최단인지 어떻게 증명?
→ 다른 모든 경로를 확인해야만 &quot;최단&quot;임을 알 수 있음
→ 다항 시간 검증 ✗
→ NP에 속하지 않음!</code></pre><h3 id="예시-외판원-문제">예시: 외판원 문제</h3>
<pre><code class="language-python">def tsp_optimization(cities, distances):
    &quot;&quot;&quot;
    외판원 최적화 문제

    문제: 최단 순회 경로와 그 거리는?

    이것이 NP-난해인 이유:
    - 누군가 &quot;850km가 최단&quot;이라고 주장해도
    - 정말 최단인지 증명하려면 모든 경로를 확인해야 함
    - 다항 시간에 검증 불가능
    - 따라서 NP에 속하지 않음

    cities: 도시 리스트
    distances: 거리 행렬 distances[i][j] = i에서 j까지 거리

    Returns: (최단_경로, 최단_거리)
    &quot;&quot;&quot;
    import itertools  # 데이터의 순열, 조합, 반복 같은 복잡한 계산을 직접 for문으로 짜지 않고도 매우 빠르고 메모리 효율적으로 처리

    n = len(cities)

    # 최단 거리를 추적 (처음엔 무한대로 초기화)
    min_distance = float(&#39;inf&#39;)  # float(&#39;inf&#39;)는 &quot;무한대&quot;를 의미
    best_tour = None

    # 모든 순열(permutation) 확인
    # itertools.permutations: 모든 순서 조합을 생성 # 예: [0,1,2] → (0,1,2), (0,2,1), (1,0,2), (1,2,0), (2,0,1), (2,1,0)
    for perm in itertools.permutations(range(n)):
        # perm: 도시 방문 순서  # 예: (0, 2, 1, 3) = 도시0 → 도시2 → 도시1 → 도시3 → 도시0

        # 이 경로의 총 거리 계산
        distance = 0

        for i in range(n):
            from_city = perm[i]  # 현재 도시
            # 다음 도시 (마지막 도시 다음은 첫 도시로 돌아감)
            to_city = perm[(i + 1) % n]  # % 연산자는 나머지

            # 거리 누적
            distance += distances[from_city][to_city]

        # 더 짧은 경로를 발견했는가?
        if distance &lt; min_distance:
            min_distance = distance
            best_tour = perm

    return best_tour, min_distance

# ===== 사용 예시 =====
cities = [&#39;서울&#39;, &#39;부산&#39;, &#39;대구&#39;, &#39;광주&#39;]
distances = [
    #  서울  부산  대구  광주
    [   0,  400,  300,  250],  # 서울에서
    [ 400,    0,  150,  300],  # 부산에서
    [ 300,  150,    0,  200],  # 대구에서
    [ 250,  300,  200,    0]   # 광주에서
]

tour, dist = tsp_optimization(cities, distances)

print(f&quot;최단 경로: {[cities[i] for i in tour]}&quot;)
print(f&quot;총 거리: {dist}km&quot;)</code></pre>
<p><strong>검증의 어려움:</strong></p>
<p>누군가 &quot;900km가 최단 거리&quot;라고 주장하면, 정말 최단인지 확인하려면 어떻게 해야 할까요?</p>
<p>위 코드처럼 모든 경로를 다 확인해야만 합니다:</p>
<ul>
<li>4개 도시: 3! = 6가지 경로 (작아서 가능)</li>
<li>10개 도시: 9! = 362,880가지</li>
<li>20개 도시: 19! = 121,645,100,408,832,000가지 (불가능!)</li>
</ul>
<p>다항 시간에 검증할 수 없으므로, NP에 속하지 않습니다. 따라서 NP-난해입니다.</p>
<hr>
<h2 id="📋-np-난해-문제-예시">📋 NP-난해 문제 예시</h2>
<h3 id="예시-1-배낭-최적화">예시 1: 배낭 최적화</h3>
<pre><code class="language-python">def knapsack_optimization(items, capacity):
    &quot;&quot;&quot;
    배낭 최적화 문제 (초보자용 상세 설명)

    문제:
    - 배낭 용량: capacity (예: 50kg)
    - 물건들: [(무게, 가치), ...]
    - 질문: 최대 가치는?

    핵심 아이디어: 각 물건에 대해 &quot;넣는다&quot; 또는 &quot;안 넣는다&quot; 두 가지 선택 → n개 물건이면 2^n 가지 조합

    예: 3개 물건
    - 조합 1: 아무것도 안 넣음
    - 조합 2: 물건 0만
    - 조합 3: 물건 1만
    - 조합 4: 물건 0, 1
    - ...
    - 조합 8: 물건 0, 1, 2 모두  총 2³ = 8가지

    비트로 표현하는 이유:
    각 물건의 선택 여부를 0/1로 표현하면 편리함    예: 101(이진수) = 물건 0과 2를 선택

    items: 물건 리스트, 각 물건은 (무게, 가치)
    capacity: 배낭의 최대 용량

    Returns: (최대_가치, 선택한_물건_인덱스들)
    &quot;&quot;&quot;
    n = len(items)  # 물건 개수

    # 최대 가치 추적
    max_value = 0
    best_subset = []

    # ===== 핵심: 모든 부분집합을 비트로 표현 =====
    # 
    # 왜 range(2^n)인가?  각 물건마다 &quot;넣음/안넣음&quot; 2가지 선택
    # → n개 물건이면 2 × 2 × ... × 2 = 2^n 가지
    #
    # 예: n=3이면
    # 0 = 000(이진) = 아무것도 안 넣음
    # 1 = 001(이진) = 물건 0만
    # 2 = 010(이진) = 물건 1만
    # 3 = 011(이진) = 물건 0, 1
    # 4 = 100(이진) = 물건 2만
    # 5 = 101(이진) = 물건 0, 2
    # 6 = 110(이진) = 물건 1, 2
    # 7 = 111(이진) = 물건 0, 1, 2 모두
    total_combinations = 2 ** n

    # 모든 조합 시도
    for i in range(total_combinations):  # i를 이진수로 해석해서 부분집합 만들기
        # 예: i=5, n=3이면,  5 = 101(이진수) → 물건 0(첫 번째 비트=1), 물건 2(세 번째 비트=1) 선택

        subset = []       # 선택한 물건들
        total_weight = 0  # 총 무게
        total_value = 0   # 총 가치

        # 각 물건(j)에 대해 i의 j번째 비트가 1인지 확인
        for j in range(n):
            # ===== 비트 연산 상세 설명 =====
            #
            # (1 &lt;&lt; j): 1을 왼쪽으로 j칸 이동 → j번째 비트만 1인 숫자 만들기
            # 
            # 예:
            # j=0: 1 &lt;&lt; 0 = 1   = 001(이진)
            # j=1: 1 &lt;&lt; 1 = 2   = 010(이진)
            # j=2: 1 &lt;&lt; 2 = 4   = 100(이진)
            #
            # i &amp; (1 &lt;&lt; j): i의 j번째 비트가 1인가?
            # → &amp;는 AND 연산 (둘 다 1이어야 1)
            #
            # 예: i=5=101(이진)일 때
            # j=0: 101 &amp; 001 = 001 (0이 아님) → True
            # j=1: 101 &amp; 010 = 000 (0)        → False
            # j=2: 101 &amp; 100 = 100 (0이 아님) → True
            #
            # 즉, j번째 비트가 1이면 물건 j를 선택!

            if i &amp; (1 &lt;&lt; j):  # j번째 비트가 1인가?
                # 이 물건을 선택!
                subset.append(j)
                total_weight += items[j][0]  # 무게 추가
                total_value += items[j][1]   # 가치 추가

        # 이 조합이 용량 이내이고, 가치가 더 큰가?
        if total_weight &lt;= capacity and total_value &gt; max_value:
            max_value = total_value
            best_subset = subset

    return max_value, best_subset

# ===== 사용 예시와 비트 연산 이해하기 =====

items = [
    (10, 60),   # 물건 0: 무게 10kg, 가치 60
    (20, 100),  # 물건 1: 무게 20kg, 가치 100
    (30, 120),  # 물건 2: 무게 30kg, 가치 120
]
capacity = 50

# 비트 연산이 어떻게 작동하는지 확인
print(&quot;=== 비트 연산 작동 방식 ===\n&quot;)

# 예: i=5일 때 (101 이진수)
i = 5
print(f&quot;i = {i} = {bin(i)} (이진수)&quot;)
print(f&quot;물건 개수: {len(items)}개\n&quot;)

for j in range(len(items)):
    bit_mask = 1 &lt;&lt; j  # j번째 비트만 1
    result = i &amp; bit_mask  # AND 연산

    print(f&quot;물건 {j} 확인:&quot;)
    print(f&quot;  (1 &lt;&lt; {j}) = {bit_mask:3d} = {bin(bit_mask):&gt;5s}&quot;)
    print(f&quot;  {i} &amp; {bit_mask} = {result:3d} = {bin(result):&gt;5s}&quot;)

    if result:
        print(f&quot;  → 물건 {j} 선택! (비트가 1)&quot;)
    else:
        print(f&quot;  → 물건 {j} 제외  (비트가 0)&quot;)
    print()

# 실제 함수 실행
max_val, selected = knapsack_optimization(items, capacity)

print(&quot;=== 최종 결과 ===&quot;)
print(f&quot;최대 가치: {max_val}&quot;)
print(f&quot;선택한 물건: {selected}&quot;)
print(f&quot;세부 정보: {[items[i] for i in selected]}&quot;)</code></pre>
<p><strong>검증의 어려움:</strong></p>
<p>&quot;최대 가치 220&quot;이라는 답을 검증하려면:</p>
<ul>
<li>3개 물건: 2³ = 8가지 조합 (가능)</li>
<li>30개 물건: 2³⁰ = 1,073,741,824가지 (불가능!)</li>
</ul>
<p>모든 조합을 확인해야 최대인지 알 수 있으므로, 다항 시간 검증이 불가능합니다.</p>
<h3 id="예시-2-정지-문제-더-어려움">예시 2: 정지 문제 (더 어려움!)</h3>
<p><strong>정지 문제</strong>는 NP-난해보다 훨씬 더 어렵습니다. 사실 컴퓨터로 절대 풀 수 없는 문제입니다!</p>
<p><strong>문제:</strong></p>
<pre><code>주어진 프로그램이 특정 입력에 대해
- 정지하는가? (끝나는가?)
- 무한 루프인가? (영원히 안 끝나는가?)</code></pre><p><strong>난이도:</strong> 결정 불가능(Undecidable) - 어떤 알고리즘으로도 풀 수 없음!</p>
<pre><code class="language-python"># 예시 1: 명백히 정지하는 프로그램
def program1(n):
    &quot;&quot;&quot;입력 받고 바로 종료&quot;&quot;&quot;
    return n + 1
# → 명백히 정지함!

# 예시 2: 명백히 안 끝나는 프로그램
def program2(n):
    &quot;&quot;&quot;무한 루프&quot;&quot;&quot;
    while True:
        pass
# → 명백히 정지 안 함!

# 예시 3: 모르는 경우 (콜라츠 추측)
def collatz(n):
    &quot;&quot;&quot;
    콜라츠 추측:
    - 짝수면 2로 나누기
    - 홀수면 3배 더하기 1
    - 1이 될 때까지 반복
    &quot;&quot;&quot;
    while n != 1:
        if n % 2 == 0:  # 짝수
            n = n // 2
        else:  # 홀수
            n = 3 * n + 1
    return True

# collatz(5): 5 → 16 → 8 → 4 → 2 → 1 (정지!)
# collatz(6): 6 → 3 → 10 → 5 → ... → 1 (정지!)
#
# 질문: 모든 양수 n에 대해 정지하는가?
# → 아무도 모름! (90년 이상 미해결)
# → n=1조까지는 정지함이 확인됨
# → 하지만 모든 n에 대해서는?</code></pre>
<p><strong>정지 문제의 불가능성:</strong></p>
<p>튜링이 1936년에 증명한 사실: 어떤 알고리즘으로도 모든 프로그램에 대해 정지 여부를 판단할 수 없습니다.</p>
<p>이것이 의미하는 것:</p>
<ul>
<li>완벽한 바이러스 검사: 불가능</li>
<li>완벽한 디버거: 불가능</li>
<li>프로그램 무한 루프 자동 탐지: 불가능</li>
</ul>
<p>난이도 순서:</p>
<pre><code>P &lt; NP &lt; NP-완전 &lt; NP-난해 &lt; 정지 문제 (결정 불가능)</code></pre><hr>
<h2 id="🔧-np-완전과-np-난해-비교">🔧 NP-완전과 NP-난해 비교</h2>
<h3 id="핵심-차이점">핵심 차이점</h3>
<pre><code>           NP에 속함?    검증 가능?    문제 형태      예시
----------------------------------------------------------------
NP-완전      ✓           ✓ (다항)     결정(Yes/No)   SAT, 클리크
NP-난해      ?           ? (어려움)    모든 형태      TSP최적화, 정지 문제

관계:
모든 NP-완전 → NP-난해
일부 NP-난해 → NP-완전 아님</code></pre><p><strong>결정 vs 최적화 (같은 문제):</strong></p>
<pre><code class="language-python"># NP-완전: TSP 결정 문제
def tsp_decision(cities, distances, max_distance):
    &quot;&quot;&quot;
    질문: max_distance 이하 경로가 있나?
    답: Yes/No
    검증: 경로 주면 O(n)에 확인 가능
    → NP에 속함 → NP-완전
    &quot;&quot;&quot;
    pass

# NP-난해: TSP 최적화 문제
def tsp_optimization(cities, distances):
    &quot;&quot;&quot;
    질문: 최단 경로는?
    답: 경로 + 거리
    검증: 최단임을 증명하려면 모든 경로 확인 → NP에 속하지 않음 → NP-난해
    &quot;&quot;&quot;
    pass</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>NP-난해란?</strong></p>
<pre><code>정의: 적어도 NP-완전만큼 어려운 문제들

특징:
1. NP에 속할 필요 없음 → 검증이 어려울 수 있음

2. 결정 문제가 아닐 수도 있음 → 최적화, 계산 문제 포함

3. 매우 어려움 → 효율적 알고리즘 없을 가능성</code></pre><p><strong>NP-완전 vs NP-난해</strong></p>
<pre><code>NP-완전:
- NP에 속함 ✓
- 검증 다항 시간 ✓
- 결정 문제 (Yes/No)
- 예: SAT, 클리크, 해밀턴 경로

NP-난해:
- NP에 속함 ? (선택사항)
- 검증 어려울 수 있음
- 모든 형태 가능
- 예: TSP 최적화, 배낭 최적화, 정지 문제

관계: NP-완전 ⊂ NP-난해</code></pre><p><strong>왜 최적화 문제는 NP-난해인가?</strong></p>
<pre><code>결정 문제:
&quot;1000km 이하 경로 있나?&quot;
→ 경로 하나만 확인 → O(n)
→ 다항 시간 검증 ✓
→ NP

최적화 문제:
&quot;최단 경로는?&quot;
→ 모든 경로 확인해야 최단 증명
→ O(n!) 시간 필요
→ 다항 시간 검증 ✗
→ NP 아님 → NP-난해</code></pre><p><strong>실무 대응</strong></p>
<pre><code>NP-난해 문제를 만나면:
1. 근사 알고리즘 사용 → 최적의 90% 정도 찾기

2. 휴리스틱 사용 → 경험적 방법 (보장 없지만 실전 OK)

3. 문제 완화 → 제약 조건 줄이기

4. 작은 입력만 처리 → 정확한 해 구하기</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[08-06] 다항 시간 환원 (Polynomial-time Reduction)</strong></p>
<ul>
<li>환원의 형식적 정의: Karp 환원과 Cook 환원의 차이</li>
<li>환원의 성질: 추이성, 방향성, 닫힘 성질</li>
<li>환원 예시: 다양한 NP-완전 문제들 간의 환원 과정</li>
<li>환원 활용: 새로운 NP-완전 문제를 증명하는 방법</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[08-04] NP-완전</a><br><strong>다음 글</strong>: <a href="#">[08-06] 다항 시간 환원</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [08-04] NP-완전 (NP-Complete)]]></title>
            <link>https://velog.io/@road_to_ai/08-04-NP-%EC%99%84%EC%A0%84-NP-Complete</link>
            <guid>https://velog.io/@road_to_ai/08-04-NP-%EC%99%84%EC%A0%84-NP-Complete</guid>
            <pubDate>Thu, 12 Mar 2026 03:44:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>NP-완전은 NP 클래스 중에서 가장 어려운 문제들의 집합으로, 하나만 효율적으로 풀면 모든 NP 문제를 풀 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="🎯-np-완전이란-무엇인가">🎯 NP-완전이란 무엇인가</h2>
<h3 id="np-완전의-정의">NP-완전의 정의</h3>
<p><strong>NP-완전(NP-Complete)</strong>은 NP의 &quot;가장 어려운&quot; 문제들입니다.</p>
<p><strong>쉬운 비유:</strong></p>
<pre><code>학교 시험으로 비유:

일반 문제 (NP):
- 수학 문제, 영어 문제, 과학 문제
- 각자 어려움이 다름
- 일부는 쉬울 수도

가장 어려운 문제 (NP-완전):
- 모든 과목 중 가장 어려운 문제들
- 이것만 풀 수 있으면 다른 문제도 풀 수 있음
- 예: 수학 올림피아드 문제

핵심:
NP-완전 문제 하나만 빠르게 풀면
→ 모든 NP 문제를 빠르게 풀 수 있음!</code></pre><p><strong>왜 중요한가?</strong></p>
<p>NP-완전 문제를 다항 시간에 풀 수 있으면 P = NP를 증명하는 것입니다. 이는 100만 달러 상금이 걸린 문제이며, 컴퓨터 과학의 가장 큰 미스터리입니다.</p>
<p>하지만 대부분의 학자들은 P ≠ NP라고 추측합니다. 즉, NP-완전 문제는 효율적으로 풀 수 없다고 생각합니다.</p>
<h3 id="형식적-정의">형식적 정의</h3>
<p><strong>문제 L이 NP-완전이려면:</strong></p>
<pre><code>조건 1: L ∈ NP
        L은 NP에 속해야 함
        → 답을 다항 시간에 검증 가능

조건 2: NP의 모든 문제를 L로 다항 시간에 환원 가능
        → L은 NP에서 가장 어려운 문제

즉:
NP-완전 = (NP에 속함) AND (가장 어려움)</code></pre><hr>
<h2 id="🔄-환원reduction이란">🔄 환원(Reduction)이란?</h2>
<h3 id="환원의-개념">환원의 개념</h3>
<p><strong>환원</strong>은 한 문제를 다른 문제로 &quot;변환&quot;하는 것입니다.</p>
<p><strong>실생활 비유:</strong></p>
<p>학교에서 집까지 가는 문제를 생각해봅시다. 이 어려운 문제를 &quot;버스 정류장까지 가는 문제&quot;로 바꿀 수 있습니다. 왜냐하면:</p>
<ol>
<li>버스 정류장까지만 가면</li>
<li>거기서 버스를 타고 집에 갈 수 있으니까</li>
</ol>
<p>즉, 어려운 문제를 이미 아는 문제로 변환하여 해결하는 것입니다.</p>
<p><strong>프로그래밍 예시:</strong></p>
<pre><code class="language-python"># 어려운 문제: 정렬되어 있는지 확인
def is_sorted_hard(arr):
    &quot;&quot;&quot;어떻게 확인하지? 모르겠음...&quot;&quot;&quot;
    pass

# 쉬운 문제: 정렬하기 (이미 안다고 가정)
def sort_array(arr):
    &quot;&quot;&quot;정렬하는 방법은 알고 있음!&quot;&quot;&quot;
    return sorted(arr)

# 환원: 어려운 문제를 쉬운 문제로 변환!
def is_sorted_easy(arr):
    &quot;&quot;&quot;
    환원을 사용한 해결

    1. 어려운 문제: &quot;정렬되어 있나?&quot;
    2. 쉬운 문제로 변환: &quot;정렬해보자&quot;
    3. 쉬운 문제 풀기: sorted() 사용
    4. 비교: 원본 == 정렬된 것?
    &quot;&quot;&quot;
    # 배열을 정렬 (쉬운 문제 풀기)
    sorted_arr = sort_array(arr)

    # 원본과 비교
    return arr == sorted_arr

# 사용
arr1 = [1, 2, 3, 4, 5]
arr2 = [1, 3, 2, 4, 5]

print(f&quot;{arr1} 정렬됨? {is_sorted_easy(arr1)}&quot;)  # True
print(f&quot;{arr2} 정렬됨? {is_sorted_easy(arr2)}&quot;)  # False</code></pre>
<p>이것이 환원입니다. 모르는 문제를 아는 문제로 바꿔서 푸는 것!</p>
<h3 id="다항-시간-환원">다항 시간 환원</h3>
<p><strong>기호: A ≤ₚ B</strong></p>
<pre><code>읽기: &quot;A는 B로 다항 시간 환원된다&quot;

의미: &quot;B를 풀 수 있으면 A도 풀 수 있다&quot;

방법:
1. A의 입력을 B의 입력으로 변환 (다항 시간)
2. B를 풀어서 답 얻기
3. B의 답을 A의 답으로 변환 (다항 시간)</code></pre><hr>
<h2 id="👑-첫-번째-np-완전-sat-satisfiability-충족-가능성">👑 첫 번째 NP-완전: SAT (Satisfiability, 충족 가능성)</h2>
<h3 id="sat-문제란">SAT 문제란?</h3>
<p><strong>SAT (Boolean Satisfiability)</strong>는 역사상 첫 번째로 증명된 NP-완전 문제입니다.</p>
<p><strong>문제:</strong></p>
<p>논리식을 참(True)으로 만드는 변수 할당이 존재하는가?</p>
<p>예를 들어:</p>
<pre><code>(x₁ OR NOT x₂) AND (x₂ OR x₃) AND (NOT x₁ OR NOT x₃)</code></pre><p>질문: x₁, x₂, x₃에 True/False를 할당해서 전체 식을 True로 만들 수 있는가?</p>
<p><strong>용어 설명:</strong></p>
<pre><code>변수 (Variable): x₁, x₂, x₃ 같은 것 (True 또는 False)

리터럴 (Literal):  변수: x₁ (긍정),  변수의 부정: NOT x₁ (부정)

절 (Clause): 리터럴들을 OR로 연결    예: (x₁ OR NOT x₂)

CNF (Conjunctive Normal Form): 절들을 AND로 연결   예: (절1) AND (절2) AND (절3)</code></pre><h3 id="sat-검증하기">SAT 검증하기</h3>
<pre><code class="language-python">def sat_verifier(formula, assignment):
    &quot;&quot;&quot;
    SAT 문제 검증자

    문제: 논리식을 만족하는 할당이 있는가?
    검증: 주어진 할당이 식을 만족하는가?

    시간복잡도: O(n) - n은 절의 개수 → 다항 시간! NP에 속함!

    formula: 논리식 (CNF 형태)
            [[1, -2], [2, 3], [-1, -3]]
            양수 = 변수, 음수 = NOT 변수
            예: [1, -2] = (x₁ OR NOT x₂)

    assignment: 변수 할당
                {1: True, 2: False, 3: False}
                예: x₁=True, x₂=False, x₃=False

    Returns: True if 할당이 식을 만족
    &quot;&quot;&quot;
    # 각 절(clause)을 하나씩 확인
    for clause in formula:        # clause: 예를 들어 [1, -2] = (x₁ OR NOT x₂)
        clause_satisfied = False  # 이 절이 만족되었는가?

        # 절 안의 각 리터럴 확인
        for literal in clause:    # literal: 양수면 변수, 음수면 NOT 변수
            var = abs(literal)    # 변수 번호 추출 (절댓값)
            is_positive = literal &gt; 0     # 긍정인가 부정인가?
            value = assignment.get(var, False)   # 이 변수의 값 가져오기

            # 리터럴의 값 계산
            if is_positive:
                literal_value = value  # 긍정 리터럴
            else:
                literal_value = not value  # 부정 리터럴

            # OR 연산: 하나라도 True면 절 만족!
            if literal_value:
                clause_satisfied = True
                break

        # 이 절이 만족 안 되면 전체 식이 False!
        if not clause_satisfied:
            return False

    # 모든 절이 만족되면 전체 식이 True!
    return True

# ===== 사용 예시 =====

# 논리식: (x₁ OR NOT x₂) AND (x₂ OR x₃) AND (NOT x₁ OR NOT x₃)
formula = [
    [1, -2],   # (x₁ OR NOT x₂)
    [2, 3],    # (x₂ OR x₃)
    [-1, -3]   # (NOT x₁ OR NOT x₃)
]

# 누군가 &quot;이게 답이야!&quot;라고 제시
assignment = {1: True, 2: True, 3: False}

# 검증
is_valid = sat_verifier(formula, assignment)
print(f&quot;검증 결과: {is_valid}&quot;)  # True</code></pre>
<p><strong>검증 과정:</strong></p>
<p>할당 {x₁=True, x₂=True, x₃=False}에 대해:</p>
<p>절 1: (x₁ OR NOT x₂)</p>
<ul>
<li>x₁ = True → True</li>
<li>절 만족! (OR이므로 하나만 True면 됨)</li>
</ul>
<p>절 2: (x₂ OR x₃)</p>
<ul>
<li>x₂ = True → True</li>
<li>절 만족!</li>
</ul>
<p>절 3: (NOT x₁ OR NOT x₃)</p>
<ul>
<li>NOT x₁ = False</li>
<li>NOT x₃ = True → True</li>
<li>절 만족!</li>
</ul>
<p>모든 절이 만족되므로 전체 식이 True입니다!</p>
<hr>
<h2 id="📋-대표적인-np-완전-문제">📋 대표적인 NP-완전 문제</h2>
<h3 id="1-외판원-문제-tsp---결정-버전">1. 외판원 문제 (TSP - 결정 버전)</h3>
<pre><code class="language-python">def tsp_decision_verifier(cities, distances, path, max_distance):
    &quot;&quot;&quot;
    외판원 결정 문제 검증자

    문제: max_distance 이하로 모든 도시를 방문할 수 있는가?

    검증: 주어진 경로가 조건을 만족하는가?
    시간: O(n) → NP에 속함 → NP-완전!

    cities: 도시 리스트
    distances: 거리 행렬 distances[i][j] = i에서 j까지 거리
    path: 제시된 경로 (도시 인덱스 리스트)
    max_distance: 최대 허용 거리

    Returns: True if 경로가 조건 만족
    &quot;&quot;&quot;
    # 1. 모든 도시를 포함하는가?
    if set(path) != set(range(len(cities))):
        return False

    # 2. 중복 없는가?
    if len(path) != len(set(path)):
        return False

    # 3. 총 거리 계산
    total_distance = 0
    for i in range(len(path)):
        from_city = path[i]
        to_city = path[(i + 1) % len(path)]  # % len(path): path의 마지막 인덱스에 도달했을 때, 다음 인덱스를 0으로 되돌려주는 역할
                                             # 즉, &quot;마지막 도시에서 다시 첫 번째 도시로 연결&quot;하여 순환 경로(Cycle)를 완성
        total_distance += distances[from_city][to_city]  # distances라는 2차원 배열에서 두 도시 사이의 거리를 찾아 누적 합산

    # 4. 제한 이하인가?
    return total_distance &lt;= max_distance

# ===== 사용 예시 =====
cities = [&#39;서울&#39;, &#39;부산&#39;, &#39;대구&#39;, &#39;광주&#39;]
distances = [
    [   0,  400,  300,  250],
    [ 400,    0,  150,  300],
    [ 300,  150,    0,  200],
    [ 250,  300,  200,    0]
]

# 제시된 경로
proposed_path = [0, 2, 1, 3]  # 서울 → 대구 → 부산 → 광주 → 서울

# 검증
result = tsp_decision_verifier(cities, distances, proposed_path, 1000)
print(f&quot;1000km 이하 경로? {result}&quot;)</code></pre>
<h3 id="2-부분집합-합-subset-sum">2. 부분집합 합 (Subset Sum)</h3>
<pre><code class="language-python">def subset_sum_verifier(numbers, target, subset_indices):
    &quot;&quot;&quot;
    부분집합 합 검증자

    문제: 합이 정확히 target인 부분집합이 있는가?
    검증: 주어진 부분집합의 합이 target인가?
    시간: O(n) → NP에 속함 → NP-완전!
    &quot;&quot;&quot;
    # 인덱스 유효성 확인
    for idx in subset_indices:
        if idx &lt; 0 or idx &gt;= len(numbers):
            return False

    # 합 계산
    total = sum(numbers[i] for i in subset_indices)

    # 목표와 비교
    return total == target

# 사용 예시
numbers = [3, 34, 4, 12, 5, 2]
target = 9

proposed = [0, 2, 5]  # numbers[0] = 3, [2] = 4, [5] = 2

result = subset_sum_verifier(numbers, target, proposed)
print(f&quot;합이 {target}? {result}&quot;)  # 3 + 4 + 2 = 9 → True</code></pre>
<h3 id="3-그래프-색칠">3. 그래프 색칠</h3>
<pre><code class="language-python">def graph_coloring_verifier(graph, coloring, k):
    &quot;&quot;&quot;
    그래프 k-색칠 검증자

    문제: k개 색으로 그래프를 칠할 수 있는가?
         (인접한 정점은 다른 색)

    검증: 주어진 색칠이 유효한가?
    시간: O(V + E) → NP에 속함 → NP-완전!
    &quot;&quot;&quot;
    # 1. 모든 정점이 색칠되었는가?
    vertices = set(graph.keys())
    if set(coloring.keys()) != vertices:
        return False

    # 2. k개 이하 색만 사용했는가?
    colors_used = set(coloring.values())
    if len(colors_used) &gt; k:
        return False

    # 3. 인접한 정점이 다른 색인가?
    for vertex in graph:
        vertex_color = coloring[vertex]

        for neighbor in graph[vertex]:
            neighbor_color = coloring[neighbor]

            # 같은 색이면 유효하지 않음!
            if vertex_color == neighbor_color:
                return False

    return True

# 사용 예시
graph = {
    &#39;A&#39;: [&#39;B&#39;, &#39;C&#39;],
    &#39;B&#39;: [&#39;A&#39;, &#39;C&#39;, &#39;D&#39;],
    &#39;C&#39;: [&#39;A&#39;, &#39;B&#39;, &#39;D&#39;],
    &#39;D&#39;: [&#39;B&#39;, &#39;C&#39;]
}

proposed_coloring = {
    &#39;A&#39;: &#39;빨강&#39;,
    &#39;B&#39;: &#39;파랑&#39;,
    &#39;C&#39;: &#39;초록&#39;,
    &#39;D&#39;: &#39;빨강&#39;
}

result = graph_coloring_verifier(graph, proposed_coloring, 3)
print(f&quot;3색 색칠 가능? {result}&quot;)  # True</code></pre>
<hr>
<hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<h3 id="np-완전-문제-인식하기">NP-완전 문제 인식하기</h3>
<p><strong>패턴 1: 조합 문제</strong></p>
<pre><code>&quot;모든 ~를 선택/방문/배치하는 방법&quot;

예:
- 모든 도시를 방문하는 경로
- 모든 정점을 연결하는 트리
- 모든 과목을 배정하는 시간표

→ 조합 폭발 가능성
→ NP-완전일 가능성 높음</code></pre><p><strong>패턴 2: &quot;존재하는가?&quot; 형태</strong></p>
<pre><code>&quot;~를 만족하는 방법이 존재하는가?&quot;

예:
- k개 색으로 칠할 수 있는가?
- 목표 합을 만들 수 있는가?
- 모든 조건을 만족하는 할당이 있는가?

→ NP-완전 문제의 전형적 형태</code></pre><p><strong>패턴 3: 알려진 NP-완전과 유사</strong></p>
<pre><code class="language-python">def check_if_npc_like(problem_description):
    &quot;&quot;&quot;
    문제가 NP-완전과 유사한지 확인

    체크리스트:
    1. 외판원 문제와 비슷한가?
    2. 배낭 문제와 비슷한가?
    3. 그래프 색칠과 비슷한가?
    4. 부분집합 선택 문제인가?
    &quot;&quot;&quot;

    keywords = {
        &#39;TSP형&#39;: [&#39;경로&#39;, &#39;순회&#39;, &#39;방문&#39;, &#39;최단&#39;],
        &#39;배낭형&#39;: [&#39;선택&#39;, &#39;용량&#39;, &#39;제한&#39;, &#39;최대화&#39;],
        &#39;색칠형&#39;: [&#39;할당&#39;, &#39;충돌&#39;, &#39;인접&#39;, &#39;다른&#39;],
        &#39;부분집합형&#39;: [&#39;합&#39;, &#39;목표&#39;, &#39;조합&#39;, &#39;선택&#39;]
    }

    # 각 패턴 확인
    for pattern_type, words in keywords.items():
        if any(word in problem_description for word in words):
            print(f&quot;주의: {pattern_type} 문제 패턴 감지!&quot;)
            print(&quot;→ NP-완전일 가능성 있음&quot;)
            print(&quot;→ 작은 입력으로 테스트 권장&quot;)
            return True

    return False</code></pre>
<h3 id="실무-대응-전략">실무 대응 전략</h3>
<p><strong>전략 1: 입력 크기 확인</strong></p>
<p>문제를 받으면 가장 먼저 입력 크기를 확인하세요.</p>
<pre><code class="language-python">def choose_strategy(problem_type, n):
    &quot;&quot;&quot;
    입력 크기에 따른 전략 선택

    problem_type: 문제 유형 (예: &#39;TSP&#39;, &#39;knapsack&#39;)
    n: 입력 크기

    Returns: 권장 전략
    &quot;&quot;&quot;
    if n &lt;= 15:
        return &quot;정확한 알고리즘 (브루트포스 가능)&quot;

    elif n &lt;= 30:
        return &quot;동적 계획법 시도 (문제에 따라)&quot;

    elif n &lt;= 1000:
        return &quot;휴리스틱 또는 근사 알고리즘&quot;

    else:
        return &quot;그리디 휴리스틱 (빠른 근사해)&quot;

# 예시
print(choose_strategy(&#39;TSP&#39;, 10))    # 정확한 알고리즘
print(choose_strategy(&#39;TSP&#39;, 100))   # 휴리스틱
print(choose_strategy(&#39;TSP&#39;, 10000)) # 그리디 휴리스틱</code></pre>
<p><strong>전략 2: 근사 알고리즘 사용</strong></p>
<p>정확한 답 대신 &quot;충분히 좋은&quot; 답을 빠르게 구하기:</p>
<pre><code>근사 비율:
- 2-근사: 최적해의 2배 이내 보장
- 1.5-근사: 최적해의 1.5배 이내 보장

예: TSP 2-근사 알고리즘
1. 최소 신장 트리(MST) 구하기 - O(E log V)
2. MST를 순회로 변환
3. 결과: 최적해의 2배 이내 보장

장점: 다항 시간 + 품질 보장
단점: 최적해는 아님</code></pre><p><strong>전략 3: 제약 조건 완화</strong></p>
<pre><code>원래 문제: 너무 어려움
→ 일부 제약 완화
→ 쉬운 문제로 변환

예: 외판원 문제
- 완화: 도시를 여러 번 방문 허용
- 결과: 최소 신장 트리로 해결 가능 (다항 시간)
- 활용: 하한(lower bound) 계산에 사용</code></pre><p><strong>전략 4: 작은 부분 문제로 분할</strong></p>
<pre><code class="language-python">def divide_and_conquer_npc(large_problem):
    &quot;&quot;&quot;
    큰 NP-완전 문제를 작은 부분으로 나누기

    전략:
    1. 지리적/논리적으로 분할
    2. 각 부분 독립적으로 해결
    3. 결과 병합

    예: 1000개 도시 TSP
    → 10개 지역으로 나누기 (각 100개 도시)
    → 각 지역 내 TSP 해결
    → 지역 간 연결

    주의: 최적해 보장 안 됨
    장점: 현실적 시간 내 해결
    &quot;&quot;&quot;
    pass</code></pre>
<h3 id="실무-체크리스트">실무 체크리스트</h3>
<p><strong>새로운 문제를 만났을 때:</strong></p>
<pre><code>□ 1단계: 문제 분류
   - 결정 문제인가? 최적화 문제인가?
   - 조합 문제인가?
   - 검증은 쉬운가?

□ 2단계: NP-완전 의심
   - 알려진 NP-완전 문제와 유사한가?
   - 작은 입력도 오래 걸리는가?
   - 지수적 복잡도가 의심되는가?

□ 3단계: 입력 크기 확인
   - n ≤ 20: 정확한 알고리즘 시도
   - 20 &lt; n ≤ 1000: 근사/휴리스틱
   - n &gt; 1000: 빠른 휴리스틱

□ 4단계: 전략 선택
   - 정확도 중요: 작은 입력만 처리
   - 속도 중요: 근사 알고리즘
   - 둘 다 중요: 제약 완화 또는 분할

□ 5단계: 검증
   - 결과의 품질 측정
   - 최적해와 비교 (가능하면)
   - 실무 요구사항 만족 확인</code></pre><h3 id="실제-사례">실제 사례</h3>
<p><strong>사례 1: 물류 최적화</strong></p>
<pre><code>문제: 100개 거점을 방문하는 최단 경로 → TSP (NP-완전)

해결:
1. 입력 크기 (n=100): 정확한 해는 불가능
2. 전략: 2-opt 휴리스틱
   - 초기 경로 생성 (그리디)
   - 반복적으로 개선
   - 시간: 몇 초 이내
3. 결과: 최적해의 5% 이내 (실무 충분)

교훈: 완벽한 해보다 빠른 &quot;좋은 해&quot;가 실무적</code></pre><p><strong>사례 2: 시간표 작성</strong></p>
<pre><code>문제: 과목-교수-시간 할당 (제약 많음) → 그래프 색칠 변형 (NP-완전)

해결:
1. 제약 조건 우선순위 분류
   - 필수 제약: 반드시 만족
   - 선호 제약: 가능하면 만족
2. 단계적 접근
   - 필수 제약만 고려 (백트래킹)
   - 선호 제약 추가 (그리디)
3. 결과: 완벽하지 않지만 실용적

교훈: 제약 완화로 다항 시간 해결 가능</code></pre><hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>NP-완전이란?</strong></p>
<pre><code>정의:
1. NP에 속함  → 답을 다항 시간에 검증 가능

2. NP 중 가장 어려움  → 모든 NP 문제를 이것으로 환원 가능

의미: &quot;NP에서 가장 어려운 문제들&quot;</code></pre><p><strong>환원(Reduction)</strong></p>
<pre><code>A ≤ₚ B: &quot;A를 B로 다항 시간에 변환 가능&quot;

의미:
- B를 풀 수 있으면 A도 풀 수 있음
- B가 쉬우면 A도 쉬움
- A가 어려우면 B도 어려움

용도:
- 문제 난이도 비교
- NP-완전 증명</code></pre><p><strong>대표 문제들</strong></p>
<pre><code>NP-완전 문제:
- SAT: 첫 번째 NP-완전 (Cook-Levin 정리)
- 외판원 (결정): max 거리 이하 경로?
- 부분집합 합: 목표 합 만들기?
- 그래프 색칠: k개 색으로 칠하기?
- 클리크: k개 완전 그래프?
- 해밀턴 경로: 모든 정점 한 번씩?</code></pre><p><strong>왜 중요한가?</strong></p>
<pre><code>하나만 풀면:
NP-완전 문제 하나를 다항 시간에 풀면
→ 모든 NP 문제를 다항 시간에 풀 수 있음
→ P = NP 증명!
→ 100만 달러 상금

하지만:
대부분 학자들은 불가능하다고 생각
→ P ≠ NP
→ 효율적 알고리즘 없음
→ 근사/휴리스틱 사용</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[08-05] NP-난해 (NP-Hard)</strong></p>
<ul>
<li>NP-난해의 정의: NP-완전보다 더 일반적인 개념</li>
<li>NP-완전과의 차이: NP에 속할 필요 없음</li>
<li>결정 vs 최적화: 왜 최적화 문제는 NP-난해인가</li>
<li>대표 예시: 외판원 최적화, 배낭 최적화, 정지 문제</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[08-03] 클래스 NP</a><br><strong>다음 글</strong>: <a href="#">[08-05] NP-난해</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [08-03] 클래스 NP (Nondeterministic Polynomial Time)]]></title>
            <link>https://velog.io/@road_to_ai/08-03-%ED%81%B4%EB%9E%98%EC%8A%A4-NP-Nondeterministic-Polynomial-Time</link>
            <guid>https://velog.io/@road_to_ai/08-03-%ED%81%B4%EB%9E%98%EC%8A%A4-NP-Nondeterministic-Polynomial-Time</guid>
            <pubDate>Mon, 09 Mar 2026 13:12:25 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>NP 클래스는 다항 시간에 검증할 수 있는 결정 문제들의 집합으로, &quot;답을 확인하기는 쉬운 문제&quot;를 의미합니다.</p>
</blockquote>
<hr>
<h2 id="🎯-np-클래스란-무엇인가">🎯 NP 클래스란 무엇인가</h2>
<h3 id="np의-정의">NP의 정의</h3>
<p><strong>NP (Nondeterministic Polynomial Time)</strong>는 <strong>다항 시간에 검증할 수 있는</strong> 결정 문제들의 집합입니다.</p>
<p><strong>핵심 아이디어:</strong></p>
<pre><code>P:  답을 빠르게 &quot;찾을&quot; 수 있는 문제
NP: 답을 빠르게 &quot;확인할&quot; 수 있는 문제

차이:
- 찾기 (Finding): 답을 계산
- 확인 (Verifying): 주어진 답이 맞는지 검증

예:
스도쿠 퍼즐
- 풀기: 어려움 (시간 오래 걸림)
- 확인: 쉬움 (빠르게 검증)
→ NP!</code></pre><p><strong>실생활 비유:</strong></p>
<pre><code>미로 찾기:

문제: 출구까지의 경로 찾기
- 찾기: 어려움
  → 여러 길 시도
  → 막다른 길, 되돌아가기
  → 시간 오래 걸림

검증: 경로가 주어지면
- 확인: 쉬움
  → 그 길만 따라가기
  → 출구 도달하는지 확인
  → 빠르게 검증!

이것이 NP의 본질!</code></pre><h3 id="형식적-정의">형식적 정의</h3>
<p><strong>검증자 기반 정의:</strong></p>
<pre><code>L ∈ NP ⟺ 
존재하는 다항 시간 검증자 V와 다항 길이의 증명서 c에 대해:

x ∈ L ⟺ ∃c: V(x, c) = Yes
x ∉ L ⟺ ∀c: V(x, c) = No

여기서:
- x: 입력 (문제 인스턴스)
- c: 증명서 (certificate, witness)
- V: 검증자 (verifier)
- V는 다항 시간에 실행
- c의 길이는 |x|의 다항</code></pre><p><strong>쉽게 풀어서:</strong></p>
<pre><code>NP 문제란:

1. 답이 &quot;Yes&quot;일 때
   - 이를 증명하는 증거가 있고
   - 그 증거를 빠르게 확인할 수 있음

2. 답이 &quot;No&quot;일 때
   - 어떤 증거를 제시해도
   - 확인하면 거짓임을 알 수 있음

핵심:
증거 확인이 다항 시간!</code></pre><h3 id="증명자와-검증자">증명자와 검증자</h3>
<p><strong>역할 분리:</strong></p>
<pre><code>증명자 (Prover):
- 답을 찾는 사람
- 시간 제한 없음
- 운이 좋거나 천재이거나
- &quot;이게 답이야!&quot;

검증자 (Verifier):
- 답을 확인하는 사람
- 다항 시간만 사용
- 효율적으로 확인
- &quot;정말 맞네!&quot;

비유:
증명자 = 수학자 (증명 찾기)
검증자 = 심사자 (증명 확인)</code></pre><p><strong>코드로 표현:</strong></p>
<pre><code class="language-python">def np_problem_template(x, certificate):
    &quot;&quot;&quot;
    NP 문제의 템플릿

    x: 입력 (문제)
    certificate: 증명서 (답의 증거)

    Returns: True if 증명서가 유효함

    시간복잡도: O(poly(|x|))
    - 다항 시간에 검증!
    &quot;&quot;&quot;
    # 증명서 확인 (빠르게!)
    return verify(x, certificate)


# 예시: 스도쿠
def sudoku_verifier(puzzle, solution):
    &quot;&quot;&quot;
    검증자: 스도쿠 답 확인

    puzzle: 스도쿠 퍼즐 (일부 채워짐)
    solution: 제시된 답 (완전히 채워짐)

    Returns: True if solution이 유효한 답

    시간복잡도: O(n²) - n×n 퍼즐
    → 다항 시간! NP!
    &quot;&quot;&quot;
    n = len(puzzle)

    # 1. 각 행 확인 - O(n²)
    for i in range(n):
        if len(set(solution[i])) != n:
            return False  # 중복 있음

    # 2. 각 열 확인 - O(n²)
    for j in range(n):
        col = [solution[i][j] for i in range(n)]
        if len(set(col)) != n:
            return False

    # 3. 각 3×3 박스 확인 - O(n²)
    box_size = int(n ** 0.5)
    for box_i in range(box_size):
        for box_j in range(box_size):
            box = []
            for i in range(box_i * box_size, (box_i + 1) * box_size):
                for j in range(box_j * box_size, (box_j + 1) * box_size):
                    box.append(solution[i][j])
            if len(set(box)) != n:
                return False

    # 4. 퍼즐의 초기 값 확인 - O(n²)
    for i in range(n):
        for j in range(n):
            if puzzle[i][j] != 0:  # 0 = 빈 칸
                if puzzle[i][j] != solution[i][j]:
                    return False  # 초기값 불일치

    return True  # 모든 조건 만족!

# 사용 예시
puzzle = [
    [5, 3, 0, 0, 7, 0, 0, 0, 0],
    [6, 0, 0, 1, 9, 5, 0, 0, 0],
    # ... (일부 생략)
]

solution = [
    [5, 3, 4, 6, 7, 8, 9, 1, 2],
    [6, 7, 2, 1, 9, 5, 3, 4, 8],
    # ... (완전한 답)
]

# 검증: 빠름!
is_valid = sudoku_verifier(puzzle, solution)
print(f&quot;유효한 답? {is_valid}&quot;)

# 찾기: 어려움! (백트래킹 등 필요)
# def sudoku_solver(puzzle):
#     # 시간이 오래 걸림...</code></pre>
<hr>
<h2 id="🔍-p와-np의-관계">🔍 P와 NP의 관계</h2>
<h3 id="p-⊆-np">P ⊆ NP</h3>
<p><strong>확실한 사실: P는 NP의 부분집합</strong></p>
<pre><code>정리: P ⊆ NP

증명:
1. L ∈ P라고 하자
2. L을 다항 시간에 해결하는 알고리즘 A가 존재
3. 검증자 V를 다음과 같이 만들자:
   V(x, c) = A(x)  (증명서 c 무시)
4. V는 다항 시간 (A가 다항 시간)
5. 따라서 L ∈ NP

결론:
빠르게 풀 수 있으면 → 빠르게 확인도 가능 (답을 계산해서 비교)</code></pre><p><strong>코드로 이해:</strong></p>
<pre><code class="language-python"># P 문제 예시: 정렬 여부 확인
def is_sorted(arr):
    &quot;&quot;&quot;
    P 문제: 배열이 정렬되어 있는가?

    시간: O(n) - 다항 → P에 속함
    &quot;&quot;&quot;
    for i in range(len(arr) - 1):
        if arr[i] &gt; arr[i + 1]:
            return False
    return True

# NP 검증자로 변환
def sorted_verifier(arr, certificate):
    &quot;&quot;&quot;
    NP 검증자: 배열이 정렬되어 있는가?

    certificate는 사용하지 않음 → 증명서 없이도 확인 가능

    시간: O(n) - 다항 → NP에도 속함
    &quot;&quot;&quot;
    # 증명서 무시하고 직접 확인
    return is_sorted(arr)

# P 문제는 자동으로 NP!
# 따라서 P ⊆ NP</code></pre>
<h3 id="p--np-문제">P = NP 문제</h3>
<p><strong>최대의 미스터리:</strong></p>
<pre><code>질문: P = NP인가?

의미:
&quot;빠르게 확인할 수 있으면 빠르게 찾을 수도 있는가?&quot;

현재 상황:
- 알려진 사실: P ⊆ NP
- 미지수: P = NP? 또는 P ≠ NP?
- 대부분 학자: P ≠ NP 추측
- 증명: 아무도 못 함
- 상금: 100만 달러</code></pre><p><strong>만약 P = NP이면:</strong></p>
<pre><code>1. 암호학 붕괴:
   - RSA 암호: 깨짐
   - 소인수분해: 쉬워짐
   - 새로운 암호 체계 필요

2. 최적화 혁명:
   - 외판원 문제: 해결
   - 자원 배치: 최적화
   - 물류: 완벽한 경로

3. 수학 증명:
   - 자동 증명 가능
   - 정리 찾기: 쉬워짐
   - 수학 연구 변화

4. AI 발전:
   - 학습 최적화
   - 패턴 인식 개선</code></pre><p><strong>만약 P ≠ NP이면:</strong></p>
<pre><code>1. 현 상태 유지:
   - 암호 체계 안전
   - 어려운 문제는 계속 어려움
   - 근사 알고리즘 필요

2. 본질적 한계:
   - 컴퓨터로 못 푸는 문제 존재
   - 휴리스틱 사용 불가피
   - 완벽한 해는 포기

3. 철학적 의미:
   - 창조 vs 확인의 차이
   - 발견 vs 검증의 본질적 차이</code></pre><hr>
<h2 id="📊-np-클래스-예시">📊 NP 클래스 예시</h2>
<h3 id="예시-1-해밀턴-경로">예시 1: 해밀턴 경로</h3>
<pre><code class="language-python">def hamiltonian_path_verifier(graph, path):
    &quot;&quot;&quot;
    NP 문제: 해밀턴 경로 검증

    문제: 모든 정점을 정확히 한 번씩 방문하는 경로가 있는가?

    찾기: 어려움!
    - 모든 순열 확인: O(n!)
    - 지수 시간

    검증: 쉬움!
    - 주어진 경로 확인: O(n)
    - 다항 시간

    graph: 그래프 {정점: [이웃들]}
    path: 제시된 경로 (정점 리스트)

    Returns: True if 유효한 해밀턴 경로

    시간복잡도: O(V) - V는 정점 수
    &quot;&quot;&quot;
    vertices = set(graph.keys())

    # 1. 모든 정점을 포함하는가? - O(V)
    if set(path) != vertices:
        return False

    # 2. 각 정점을 정확히 한 번만? - O(V)
    if len(path) != len(set(path)):
        return False

    # 3. 경로가 연결되어 있는가? - O(V)
    for i in range(len(path) - 1):
        current = path[i]
        next_vertex = path[i + 1]

        # 간선이 존재하는지 확인
        if next_vertex not in graph[current]:
            return False

    return True  # 유효한 해밀턴 경로!


# 사용 예시
graph = {
    &#39;A&#39;: [&#39;B&#39;, &#39;C&#39;],
    &#39;B&#39;: [&#39;A&#39;, &#39;C&#39;, &#39;D&#39;],
    &#39;C&#39;: [&#39;A&#39;, &#39;B&#39;, &#39;D&#39;],
    &#39;D&#39;: [&#39;B&#39;, &#39;C&#39;]
}

# 누군가 &quot;이게 답이야!&quot;라고 제시
proposed_path = [&#39;A&#39;, &#39;B&#39;, &#39;D&#39;, &#39;C&#39;]

# 검증: 빠름! - O(V)
is_valid = hamiltonian_path_verifier(graph, proposed_path)
print(f&quot;유효한 경로? {is_valid}&quot;)  # True

# 검증 과정 시각화
def hamiltonian_path_verifier_verbose(graph, path):
    &quot;&quot;&quot;검증 과정을 출력하는 버전&quot;&quot;&quot;
    print(f&quot;경로 검증: {path}&quot;)

    vertices = set(graph.keys())

    # 1. 정점 포함 확인
    print(f&quot;\n1. 모든 정점 포함? {set(path)} = {vertices}&quot;)
    if set(path) != vertices:
        print(&quot;   ✗ 일부 정점 누락!&quot;)
        return False
    print(&quot;   ✓ 모든 정점 포함&quot;)

    # 2. 중복 확인
    print(f&quot;\n2. 중복 없음? len={len(path)}, unique={len(set(path))}&quot;)
    if len(path) != len(set(path)):
        print(&quot;   ✗ 중복 정점 있음!&quot;)
        return False
    print(&quot;   ✓ 중복 없음&quot;)

    # 3. 연결 확인
    print(f&quot;\n3. 경로 연결 확인:&quot;)
    for i in range(len(path) - 1):
        current = path[i]
        next_vertex = path[i + 1]

        print(f&quot;   {current} → {next_vertex}: &quot;, end=&quot;&quot;)
        if next_vertex not in graph[current]:
            print(&quot;✗ 간선 없음!&quot;)
            return False
        print(&quot;✓&quot;)

    print(f&quot;\n✓ 유효한 해밀턴 경로!&quot;)
    return True

hamiltonian_path_verifier_verbose(graph, proposed_path)

# 찾기는 어려움!
def find_hamiltonian_path_bruteforce(graph):
    &quot;&quot;&quot;
    해밀턴 경로 찾기 (브루트포스)

    시간복잡도: O(V!) - 팩토리얼!
    - 모든 순열 확인
    - 매우 느림

    V=10: 10! = 3,628,800
    V=20: 20! = 2,432,902,008,176,640,000 → 불가능!
    &quot;&quot;&quot;
    import itertools

    vertices = list(graph.keys())

    # 모든 순열 확인 - O(V!)
    for path in itertools.permutations(vertices):
        if hamiltonian_path_verifier(graph, list(path)):
            return list(path)

    return None  # 해밀턴 경로 없음

# 작은 그래프는 가능
path = find_hamiltonian_path_bruteforce(graph)
print(f&quot;\n찾은 경로: {path}&quot;)
# 하지만 큰 그래프는 불가능!</code></pre>
<h3 id="예시-2-부분집합-합-subset-sum">예시 2: 부분집합 합 (Subset Sum)</h3>
<pre><code class="language-python">def subset_sum_verifier(numbers, target, subset_indices):
    &quot;&quot;&quot;
    NP 문제: 부분집합 합 검증

    문제: 주어진 숫자들 중 일부를 선택해서 합이 정확히 target이 되게 할 수 있는가?

    찾기: 어려움!
    - 모든 부분집합: O(2^n)
    - 지수 시간

    검증: 쉬움!
    - 주어진 부분집합 합: O(n)
    - 다항 시간

    numbers: 숫자 리스트
    target: 목표 합
    subset_indices: 선택한 숫자들의 인덱스

    Returns: True if 부분집합의 합이 target

    시간복잡도: O(n)
    &quot;&quot;&quot;
    # 1. 인덱스 유효성 확인 - O(k), k = len(subset_indices)
    for idx in subset_indices:
        if idx &lt; 0 or idx &gt;= len(numbers):
            return False  # 유효하지 않은 인덱스

    # 2. 합 계산 - O(k)
    total = sum(numbers[i] for i in subset_indices)

    # 3. 목표 합과 비교 - O(1)
    return total == target

# 사용 예시
numbers = [3, 34, 4, 12, 5, 2]
target = 9

# 누군가 &quot;이 부분집합이 답이야!&quot;
proposed_subset = [0, 2, 5]  # numbers[0]=3, [2]=4, [5]=2

# 검증: 빠름! - O(n)
is_valid = subset_sum_verifier(numbers, target, proposed_subset)
print(f&quot;유효한 답? {is_valid}&quot;)  # True

# 검증 과정 시각화
print(f&quot;\n선택한 숫자: {[numbers[i] for i in proposed_subset]}&quot;)
print(f&quot;합: {sum(numbers[i] for i in proposed_subset)}&quot;)
print(f&quot;목표: {target}&quot;)

# 검증 과정 상세
def subset_sum_verifier_verbose(numbers, target, subset_indices):
    &quot;&quot;&quot;검증 과정을 출력하는 버전&quot;&quot;&quot;
    print(f&quot;부분집합 검증:&quot;)
    print(f&quot;  전체 숫자: {numbers}&quot;)
    print(f&quot;  목표 합: {target}&quot;)
    print(f&quot;  제시된 인덱스: {subset_indices}&quot;)

    # 1. 유효성 확인
    print(f&quot;\n1. 인덱스 유효성:&quot;)
    for idx in subset_indices:
        if idx &lt; 0 or idx &gt;= len(numbers):
            print(f&quot;   ✗ 인덱스 {idx} 유효하지 않음!&quot;)
            return False
        print(f&quot;   ✓ {idx} → {numbers[idx]}&quot;)

    # 2. 합 계산
    selected = [numbers[i] for i in subset_indices]
    total = sum(selected)

    print(f&quot;\n2. 합 계산:&quot;)
    print(f&quot;   선택: {selected}&quot;)
    print(f&quot;   합: {&#39; + &#39;.join(map(str, selected))} = {total}&quot;)

    # 3. 비교
    print(f&quot;\n3. 목표 비교:&quot;)
    print(f&quot;   계산된 합: {total}&quot;)
    print(f&quot;   목표 합: {target}&quot;)

    if total == target:
        print(f&quot;   ✓ 일치!&quot;)
        return True
    else:
        print(f&quot;   ✗ 불일치!&quot;)
        return False

subset_sum_verifier_verbose(numbers, target, proposed_subset)


# 찾기는 어려움!
def find_subset_sum_bruteforce(numbers, target):
    &quot;&quot;&quot;
    부분집합 합 찾기 (브루트포스)

    시간복잡도: O(2^n)
    - 모든 부분집합 확인
    - 지수 시간!

    n=20: 2^20 = 1,048,576
    n=30: 2^30 = 1,073,741,824
    n=50: 2^50 = 1,125,899,906,842,624 → 불가능!
    &quot;&quot;&quot;
    n = len(numbers)

    # 모든 부분집합 확인 - O(2^n)
    for mask in range(1 &lt;&lt; n):  # 2^n 가지
        subset = []
        total = 0

        # 비트마스크로 부분집합 생성
        for i in range(n):
            if mask &amp; (1 &lt;&lt; i):
                subset.append(i)
                total += numbers[i]

        # 합이 target과 같은가?
        if total == target:
            return subset  # 찾음!

    return None  # 없음

# 작은 입력은 가능
result = find_subset_sum_bruteforce(numbers, target)
print(f&quot;\n찾은 부분집합: {result}&quot;)
print(f&quot;선택한 숫자: {[numbers[i] for i in result]}&quot;)
# 하지만 큰 입력은 불가능!</code></pre>
<h3 id="예시-3-그래프-색칠">예시 3: 그래프 색칠</h3>
<pre><code class="language-python">def graph_coloring_verifier(graph, coloring, k):
    &quot;&quot;&quot;
    NP 문제: 그래프 k-색칠 검증

    문제: 그래프를 k개 색으로 칠할 수 있는가?
         (인접한 정점은 다른 색)

    찾기: 어려움! - 지수 시간
    검증: 쉬움! - O(V + E)

    graph: 그래프 {정점: [이웃들]}
    coloring: 색칠 {정점: 색}
    k: 색 개수

    Returns: True if 유효한 k-색칠

    시간복잡도: O(V + E)
    &quot;&quot;&quot;
    # 1. 모든 정점이 색칠되었는가? - O(V)
    vertices = set(graph.keys())
    if set(coloring.keys()) != vertices:
        return False

    # 2. k개 이하의 색만 사용했는가? - O(V)
    colors_used = set(coloring.values())
    if len(colors_used) &gt; k:
        return False

    # 3. 인접한 정점이 다른 색인가? - O(E)
    for vertex in graph:
        vertex_color = coloring[vertex]

        for neighbor in graph[vertex]:
            neighbor_color = coloring[neighbor]

            # 같은 색이면 유효하지 않음!
            if vertex_color == neighbor_color:
                return False

    return True  # 유효한 색칠!

# 사용 예시
graph = {
    &#39;A&#39;: [&#39;B&#39;, &#39;C&#39;],
    &#39;B&#39;: [&#39;A&#39;, &#39;C&#39;, &#39;D&#39;],
    &#39;C&#39;: [&#39;A&#39;, &#39;B&#39;, &#39;D&#39;],
    &#39;D&#39;: [&#39;B&#39;, &#39;C&#39;]
}

# 누군가 &quot;이렇게 칠하면 돼!&quot;
proposed_coloring = {
    &#39;A&#39;: &#39;red&#39;,
    &#39;B&#39;: &#39;blue&#39;,
    &#39;C&#39;: &#39;green&#39;,
    &#39;D&#39;: &#39;red&#39;
}

k = 3  # 3색 사용

# 검증: 빠름! - O(V + E)
is_valid = graph_coloring_verifier(graph, proposed_coloring, k)
print(f&quot;유효한 {k}-색칠? {is_valid}&quot;)  # True

# 검증 과정 시각화
def graph_coloring_verifier_verbose(graph, coloring, k):
    &quot;&quot;&quot;검증 과정을 출력하는 버전&quot;&quot;&quot;
    print(f&quot;{k}-색칠 검증:&quot;)
    print(f&quot;  그래프: {graph}&quot;)
    print(f&quot;  색칠: {coloring}&quot;)

    vertices = set(graph.keys())

    # 1. 완전성 확인
    print(f&quot;\n1. 모든 정점 색칠?&quot;)
    if set(coloring.keys()) != vertices:
        print(f&quot;   ✗ 일부 정점 누락!&quot;)
        return False
    print(f&quot;   ✓ 모든 정점 색칠됨&quot;)

    # 2. 색 개수 확인
    colors_used = set(coloring.values())
    print(f&quot;\n2. 사용된 색: {colors_used}&quot;)
    print(f&quot;   허용: {k}개, 사용: {len(colors_used)}개&quot;)
    if len(colors_used) &gt; k:
        print(f&quot;   ✗ 너무 많은 색 사용!&quot;)
        return False
    print(f&quot;   ✓ 색 개수 OK&quot;)

    # 3. 인접성 확인
    print(f&quot;\n3. 인접 정점 색 확인:&quot;)
    for vertex in graph:
        vertex_color = coloring[vertex]

        for neighbor in graph[vertex]:
            neighbor_color = coloring[neighbor]

            print(f&quot;   {vertex}({vertex_color}) - {neighbor}({neighbor_color}): &quot;, end=&quot;&quot;)

            if vertex_color == neighbor_color:
                print(&quot;✗ 같은 색!&quot;)
                return False
            print(&quot;✓&quot;)

    print(f&quot;\n✓ 유효한 {k}-색칠!&quot;)
    return True

graph_coloring_verifier_verbose(graph, proposed_coloring, k)</code></pre>
<hr>
<h2 id="🤖-비결정적-튜링-기계">🤖 비결정적 튜링 기계</h2>
<h3 id="비결정성의-개념">비결정성의 개념</h3>
<p><strong>비결정적(Nondeterministic)</strong>이란 &quot;운이 좋으면&quot; 빠르게 푸는 것입니다.</p>
<pre><code>결정적 튜링 기계:
- 매 단계마다 선택지 하나
- 정해진 규칙대로 진행
- 현실의 컴퓨터

비결정적 튜링 기계:
- 매 단계마다 여러 선택지
- &quot;마법처럼&quot; 올바른 선택
- 이론적 모델</code></pre><p><strong>비유:</strong></p>
<pre><code>미로 탈출:

결정적 방법:
- 한 길씩 시도
- 막다른 길이면 되돌아가기
- 모든 길 확인
→ 시간 오래 걸림

비결정적 방법:
- &quot;운이 좋게&quot; 정답 길만 선택
- 한 번에 출구 도착
- 모든 분기점에서 정답 선택
→ 빠름!

현실:
비결정적 기계는 존재 안 함
하지만 이론적으로 유용!</code></pre><h3 id="np의-비결정적-정의">NP의 비결정적 정의</h3>
<pre><code>L ∈ NP ⟺ 비결정적 튜링 기계가 L을 다항 시간에 결정

의미: &quot;운이 좋으면&quot; 다항 시간에 풀 수 있음

검증자 정의와 동등: 비결정적 기계의 &quot;운 좋은 선택&quot; = 증명서</code></pre><p><strong>코드로 이해:</strong></p>
<pre><code class="language-python">def nondeterministic_subset_sum(numbers, target):
    &quot;&quot;&quot;
    비결정적 알고리즘 (의사 코드)

    현실에서는 불가능하지만 이론적 모델
    &quot;&quot;&quot;
    # 비결정적 선택: &quot;마법처럼&quot; 정답 부분집합 선택
    subset = nondeterministic_guess()  # 운 좋게 정답!

    # 검증: 다항 시간
    total = sum(numbers[i] for i in subset)
    return total == target

def deterministic_subset_sum(numbers, target):
    &quot;&quot;&quot;
    결정적 알고리즘 (현실)

    모든 경우를 시도해야 함
    &quot;&quot;&quot;
    n = len(numbers)

    # 모든 부분집합 확인 - O(2^n)
    for mask in range(1 &lt;&lt; n):
        subset = [i for i in range(n) if mask &amp; (1 &lt;&lt; i)]
        total = sum(numbers[i] for i in subset)

        if total == target:
            return True

    return False

# 차이:
# 비결정적: &quot;운 좋게&quot; 정답 선택 + 검증 = O(n)
# 결정적: 모든 경우 시도 = O(2^n)</code></pre>
<hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<p><strong>NP 문제 인식하기</strong></p>
<pre><code class="language-python">def is_np_problem_pattern(problem_description):
    &quot;&quot;&quot;
    문제가 NP일 가능성 확인

    패턴:
    1. &quot;존재하는가?&quot; 형태
    2. 검증이 쉬움
    3. 찾기가 어려움
    &quot;&quot;&quot;
    print(f&quot;문제: {problem_description}&quot;)

    # 패턴 1: 존재성 질문
    existence_keywords = [&#39;존재&#39;, &#39;there exists&#39;, &#39;할 수 있는가&#39;, &#39;possible&#39;]
    has_existence = any(kw in problem_description for kw in existence_keywords)

    if has_existence:
        print(&quot;✓ 존재성 질문 패턴&quot;)

    # 패턴 2: 조합/선택 문제
    combinatorial_keywords = [&#39;선택&#39;, &#39;조합&#39;, &#39;배치&#39;, &#39;경로&#39;, &#39;색칠&#39;]
    is_combinatorial = any(kw in problem_description for kw in combinatorial_keywords)

    if is_combinatorial:
        print(&quot;✓ 조합 문제 패턴&quot;)

    # 패턴 3: 제약 만족
    constraint_keywords = [&#39;조건&#39;, &#39;제약&#39;, &#39;만족&#39;, &#39;이하&#39;, &#39;이상&#39;]
    has_constraints = any(kw in problem_description for kw in constraint_keywords)

    if has_constraints:
        print(&quot;✓ 제약 만족 패턴&quot;)

    if has_existence and (is_combinatorial or has_constraints):
        print(&quot;\n→ NP 문제일 가능성 높음!&quot;)
        print(&quot;  확인: 검증 알고리즘을 빠르게 만들 수 있는가?&quot;)
    else:
        print(&quot;\n→ 추가 분석 필요&quot;)

# 예시
is_np_problem_pattern(&quot;모든 도시를 방문하는 1000km 이하 경로가 존재하는가?&quot;)
print()
is_np_problem_pattern(&quot;배열의 최댓값은?&quot;)</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>NP 클래스</strong></p>
<pre><code>정의: NP = 다항 시간에 검증할 수 있는 결정 문제들

형식: NP = {L | 다항 시간 검증자가 존재}

의미: &quot;답을 확인하기는 쉬운 문제&quot;</code></pre><p><strong>핵심 개념</strong></p>
<pre><code>증명서 (Certificate):
- 답의 증거
- 다항 길이

검증자 (Verifier):
- 증명서 확인
- 다항 시간

비결정성:
- &quot;운이 좋으면&quot; 빠르게
- 이론적 모델</code></pre><p><strong>P와의 관계</strong></p>
<pre><code>확실: P ⊆ NP
미지수: P = NP?

대부분 추측: P ≠ NP
- 확인은 쉽지만 찾기는 어려운 문제 존재
- 창조 vs 검증의 본질적 차이</code></pre><p><strong>대표 예시</strong></p>
<pre><code>NP 문제:
- 해밀턴 경로: 검증 O(V), 찾기 O(V!)
- 부분집합 합: 검증 O(n), 찾기 O(2^n)
- 그래프 색칠: 검증 O(V+E), 찾기 지수
- 스도쿠: 검증 O(n²), 찾기 어려움</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[08-04] NP-완전 (NP-Complete)</strong></p>
<ul>
<li>NP-완전의 정의: NP 중에서 가장 어려운 문제들</li>
<li>다항 시간 환원: 문제 간 난이도 비교 방법</li>
<li>Cook-Levin 정리: 첫 번째 NP-완전 문제 (SAT)</li>
<li>대표적인 NP-완전 문제: 외판원, 배낭, 그래프 색칠 등</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[08-02] 클래스 P</a><br><strong>다음 글</strong>: <a href="#">[08-04] NP-완전</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [08-02] 클래스 P (Polynomial Time)]]></title>
            <link>https://velog.io/@road_to_ai/08-02-%ED%81%B4%EB%9E%98%EC%8A%A4-P-Polynomial-Time</link>
            <guid>https://velog.io/@road_to_ai/08-02-%ED%81%B4%EB%9E%98%EC%8A%A4-P-Polynomial-Time</guid>
            <pubDate>Mon, 09 Mar 2026 12:24:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>P 클래스는 다항 시간에 해결할 수 있는 결정 문제들의 집합으로, &quot;효율적으로 풀 수 있는 문제&quot;를 의미합니다.</p>
</blockquote>
<hr>
<h2 id="🎯-p-클래스란-무엇인가">🎯 P 클래스란 무엇인가</h2>
<h3 id="p의-정의">P의 정의</h3>
<p><strong>P (Polynomial Time)</strong>는 <strong>다항 시간에 해결할 수 있는</strong> 결정 문제들의 집합입니다.</p>
<p>알고리즘이 문제를 해결하는 데 걸리는 시간 T(n)이 입력 크기에 대한 다항식 함수(n^k)보다 크지 않은 계산 복잡도를 의미 (단, k는 상수)</p>
<p><strong>직관적 이해:</strong></p>
<pre><code>P = &quot;효율적으로 풀 수 있는 문제들&quot;

특징:
- 답을 빠르게 계산할 수 있음
- 입력이 커져도 현실적 시간
- 실용적으로 사용 가능

예:
- 정렬: 가능! (O(n log n))
- 최단 경로: 가능! (O(E log V))
- 최대공약수: 가능! (O(log n))</code></pre><p><strong>실생활 비유:</strong></p>
<pre><code>도서관에서 책 찾기:

P에 속하는 방법:
- 컴퓨터 검색 시스템 사용
- 분류 번호로 찾기
→ 빠르게 찾을 수 있음!

P에 속하지 않는 방법:
- 모든 책장을 다 뒤지기
- 모든 책을 하나씩 확인
→ 너무 오래 걸림</code></pre><h3 id="형식적-정의">형식적 정의</h3>
<p><strong>수학적 정의:</strong></p>
<pre><code>P = {L | L은 다항 시간 튜링 기계로 결정 가능}

풀어서:
P는 다음 조건을 만족하는 언어 L의 집합:
- 어떤 튜링 기계 M이 존재하여
- 입력 w에 대해
- O(n^k) 시간 내에 (k는 상수)
- &quot;w ∈ L인가?&quot;를 판단할 수 있다</code></pre><p><strong>다항 시간이란:</strong></p>
<pre><code>다항식 형태의 시간복잡도:

T(n) = a_k × n^k + a_{k-1} × n^{k-1} + ... + a_1 × n + a_0

예:
O(1)        상수
O(log n)    로그 (다항보다 빠름, P에 포함)
O(n)        선형 ✓
O(n log n)  준선형 ✓
O(n²)       제곱 ✓
O(n³)       삼차 ✓
O(n^100)    100차 ✓ (느리지만 다항!)
O(2^n)      지수 ✗ (다항 아님!)
O(n!)       팩토리얼 ✗ (다항 아님!)

P에 속하려면:
어떤 k에 대해 O(n^k)이면 됨
(k가 크든 작든 상관없음)</code></pre><hr>
<h2 id="⏱️-왜-다항-시간을-효율적이라고-하는가">⏱️ 왜 다항 시간을 &quot;효율적&quot;이라고 하는가?</h2>
<h3 id="다항-vs-지수">다항 vs 지수</h3>
<p><strong>성장률 비교:</strong></p>
<pre><code class="language-python">import math

def compare_growth(n):
    &quot;&quot;&quot;
    다항 시간 vs 지수 시간 비교

    n이 커질수록 차이가 극명해짐
    &quot;&quot;&quot;
    polynomial = n ** 3      # O(n³) - 다항
    exponential = 2 ** n     # O(2^n) - 지수

    print(f&quot;n={n:3d}: 다항={polynomial:15,d}, 지수={exponential:15,d}&quot;)

    if exponential &lt; 10**15:  # 출력 가능한 범위
        ratio = exponential / polynomial if polynomial &gt; 0 else 0
        print(f&quot;       지수가 다항의 {ratio:,.0f}배&quot;)

# 비교
print(&quot;다항 시간 vs 지수 시간\n&quot;)
for n in [10, 20, 30, 40, 50]:
    compare_growth(n)
    print()

# 출력 예상:
# n= 10: 다항=          1,000, 지수=          1,024
#        지수가 다항의 1배
#
# n= 20: 다항=          8,000, 지수=      1,048,576
#        지수가 다항의 131배
#
# n= 30: 다항=         27,000, 지수=  1,073,741,824
#        지수가 다항의 39,768배
#
# n= 40: 다항=         64,000, 지수= 1,099,511,627,776
#        지수가 다항의 17,179,869,184배
#
# n= 50: 다항=        125,000, 지수= (천문학적 숫자)</code></pre>
<p><strong>실제 시간 비교:</strong></p>
<pre><code>입력 크기 n=50 일 때
(1초에 10억 연산 가정)

다항 시간 알고리즘:
- O(n):      50 / 10^9 = 0.00000005초
- O(n²):     2,500 / 10^9 = 0.0000025초
- O(n³):     125,000 / 10^9 = 0.000125초
- O(n^10):   약 0.1초
→ 모두 1초 이내! 실용적!

지수 시간 알고리즘:
- O(2^n):    2^50 / 10^9 = 1,125,899초
           = 약 13일!
- O(3^n):    3^50 / 10^9 = 약 2,000년!
→ 불가능!

결론:
다항은 느려 보여도 현실적
지수는 빠르게 불가능해짐</code></pre><h3 id="다항-시간의-좋은-성질">다항 시간의 좋은 성질</h3>
<p><strong>1. 합성에 닫혀 있음:</strong></p>
<pre><code>알고리즘 A: O(n²)
알고리즘 B: O(n³)

A 다음에 B 실행:
총 시간 = O(n²) + O(n³) = O(n³)
→ 여전히 다항!

A를 n번 실행:
총 시간 = n × O(n²) = O(n³)
→ 여전히 다항!

중첩 실행:
A의 각 단계에서 B 호출:
총 시간 = O(n²) × O(n³) = O(n^5)
→ 여전히 다항!</code></pre><p><strong>2. 하드웨어 독립적:</strong></p>
<pre><code>컴퓨터 A: 1초에 10억 연산
컴퓨터 B: 1초에 1조 연산 (1000배 빠름)

O(n²) 알고리즘:
- 컴퓨터 A: 1,000초
- 컴퓨터 B: 1초
→ 둘 다 현실적!

O(2^n) 알고리즘 (n=50):
- 컴퓨터 A: 13일
- 컴퓨터 B: 19분
→ 여전히 오래 걸림

다항 시간:
하드웨어 개선으로 해결 가능!

지수 시간:
하드웨어 개선으로도 한계</code></pre><p><strong>3. 안정성:</strong></p>
<pre><code>입력 증가에 대한 민감도:

다항 O(n²):
n → 2n이면
시간 → 4배

지수 O(2^n):
n → 2n이면
시간 → (2^n)² = 2^(2n)
→ 제곱배!

예: n=20 → n=40
- 다항: 4배 (400 → 1,600)
- 지수: 1,048,576배!</code></pre><hr>
<h2 id="📊-p-클래스-예시">📊 P 클래스 예시</h2>
<h3 id="예시-1-정렬-문제">예시 1: 정렬 문제</h3>
<pre><code class="language-python">def is_sortable_in_poly_time(arr):
    &quot;&quot;&quot;
    결정 문제: 배열을 다항 시간에 정렬할 수 있는가?

    답: 항상 Yes!

    이유: 병합 정렬이 O(n log n)
          O(n log n) &lt; O(n²) (다항)

    따라서 정렬 문제 ∈ P
    &quot;&quot;&quot;
    return True  # 항상 가능!

def merge_sort(arr):
    &quot;&quot;&quot;
    병합 정렬 - P 클래스 알고리즘

    시간복잡도: O(n log n)
    - 다항 시간!
    - P에 속함

    arr: 정렬할 배열

    Returns: 정렬된 배열
    &quot;&quot;&quot;
    # 기저 사례: 크기 1 이하면 이미 정렬됨
    if len(arr) &lt;= 1:
        return arr

    # 분할: 중간 지점 찾기
    mid = len(arr) // 2

    # 재귀: 왼쪽과 오른쪽 각각 정렬
    left = merge_sort(arr[:mid])    # O(n log n)
    right = merge_sort(arr[mid:])   # O(n log n)

    # 병합: 정렬된 두 배열 합치기
    return merge(left, right)       # O(n)

def merge(left, right):
    &quot;&quot;&quot;
    두 정렬된 배열을 하나로 병합

    시간복잡도: O(n)
    - n = len(left) + len(right)
    &quot;&quot;&quot;
    result = []
    i = j = 0

    # 양쪽 배열을 순회하며 작은 것부터 추가
    while i &lt; len(left) and j &lt; len(right):
        if left[i] &lt;= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1

    # 남은 원소들 추가
    result.extend(left[i:])  # .extend(left[i:]): 요소만 깔끔하게 추가, .append(left[i:]): 리스트 안에 리스트가 들어감
    result.extend(right[j:])

    return result

# 사용 예시
arr = [5, 2, 8, 1, 9, 3]
sorted_arr = merge_sort(arr)
print(f&quot;정렬 결과: {sorted_arr}&quot;)

# 시간 측정
import time
import random

n = 100000
large_arr = [random.randint(1, 1000000) for _ in range(n)]

start = time.time()
merge_sort(large_arr.copy())
elapsed = time.time() - start

print(f&quot;\nn={n:,} 정렬 시간: {elapsed:.4f}초&quot;)
print(&quot;다항 시간이므로 P에 속함!&quot;)</code></pre>
<h3 id="예시-2-최단-경로-다익스트라">예시 2: 최단 경로 (다익스트라)</h3>
<pre><code class="language-python">import heapq

def shortest_path_decision(graph, start, end, max_distance):
    &quot;&quot;&quot;
    결정 문제: start에서 end까지 max_distance 이하 경로가 있는가?

    해결: 다익스트라 알고리즘 사용
    시간복잡도: O((V + E) log V)
    - V: 정점 수, E: 간선 수
    - 다항 시간!

    따라서 최단 경로 문제 ∈ P

    graph: 그래프 {정점: [(이웃, 가중치), ...]}
    start: 시작 정점
    end: 도착 정점
    max_distance: 최대 허용 거리

    Returns: True if 경로 있음, False otherwise
    &quot;&quot;&quot;
    # 다익스트라 알고리즘으로 최단 거리 계산
    distances = dijkstra(graph, start)

    # end까지 거리가 max_distance 이하인가?
    if end not in distances:
        return False  # 경로 없음

    return distances[end] &lt;= max_distance

def dijkstra(graph, start):
    &quot;&quot;&quot;
    다익스트라 알고리즘

    시간복잡도: O((V + E) log V)
    - 우선순위 큐 사용
    - 각 정점: log V 시간
    - 각 간선: log V 시간
    - 총: (V + E) log V

    공간복잡도: O(V)
    &quot;&quot;&quot;
    # 초기화
    distances = {vertex: float(&#39;inf&#39;) for vertex in graph}
    distances[start] = 0

    # 우선순위 큐: (거리, 정점)
    pq = [(0, start)]
    visited = set()

    while pq:
        # 가장 가까운 미방문 정점 선택
        current_dist, current = heapq.heappop(pq)

        # 이미 방문했으면 스킵
        if current in visited:
            continue

        visited.add(current)

        # 이웃 정점들 확인
        for neighbor, weight in graph.get(current, []):
            distance = current_dist + weight

            # 더 짧은 경로 발견
            if distance &lt; distances[neighbor]:
                distances[neighbor] = distance
                heapq.heappush(pq, (distance, neighbor))

    return distances

# 사용 예시
graph = {
    &#39;A&#39;: [(&#39;B&#39;, 4), (&#39;C&#39;, 2)],
    &#39;B&#39;: [(&#39;C&#39;, 1), (&#39;D&#39;, 5)],
    &#39;C&#39;: [(&#39;D&#39;, 8), (&#39;E&#39;, 10)],
    &#39;D&#39;: [(&#39;E&#39;, 2)],
    &#39;E&#39;: []
}

# 결정 문제: A에서 E까지 15 이하 경로?
result = shortest_path_decision(graph, &#39;A&#39;, &#39;E&#39;, 15)
print(f&quot;A→E 15 이하 경로? {result}&quot;)  # True (A→C→D→E = 12)

# 실제 최단 거리들
distances = dijkstra(graph, &#39;A&#39;)
print(f&quot;\nA로부터의 최단 거리: {distances}&quot;)

# 다항 시간이므로 P에 속함!
print(&quot;\n최단 경로 문제 ∈ P&quot;)</code></pre>
<h3 id="예시-3-최대공약수">예시 3: 최대공약수</h3>
<pre><code class="language-python">def gcd_decision(a, b, k):
    &quot;&quot;&quot;
    결정 문제: gcd(a, b) = k인가?

    해결: 유클리드 알고리즘 사용
    시간복잡도: O(log min(a, b))
    - 로그는 다항보다 빠름!

    따라서 GCD 문제 ∈ P

    a, b: 두 정수
    k: 확인할 값

    Returns: True if gcd(a, b) = k
    &quot;&quot;&quot;
    return gcd(a, b) == k


def gcd(a, b):
    &quot;&quot;&quot;
    유클리드 알고리즘

    시간복잡도: O(log min(a, b))

    원리: gcd(a, b) = gcd(b, a % b)

    왜 빠른가?
    - 매 단계마다 숫자가 절반 이하로 줄어듦
    - 최대 log₂(min(a, b)) 단계

    예:
    gcd(48, 18)
    = gcd(18, 12)  # 48 % 18 = 12
    = gcd(12, 6)   # 18 % 12 = 6
    = gcd(6, 0)    # 12 % 6 = 0
    = 6

    3번 만에 완료!
    &quot;&quot;&quot;
    while b != 0:
        a, b = b, a % b
    return a


# 사용 예시
print(&quot;GCD 계산:&quot;)
print(f&quot;gcd(48, 18) = {gcd(48, 18)}&quot;)
print(f&quot;gcd(100, 35) = {gcd(100, 35)}&quot;)

# 결정 문제
print(f&quot;\ngcd(48, 18) = 6? {gcd_decision(48, 18, 6)}&quot;)
print(f&quot;gcd(48, 18) = 3? {gcd_decision(48, 18, 3)}&quot;)

# 큰 수도 빠르게 계산
import time

a = 123456789012345
b = 987654321098765

start = time.time()
result = gcd(a, b)
elapsed = time.time() - start

print(f&quot;\ngcd({a}, {b})&quot;)
print(f&quot;= {result}&quot;)
print(f&quot;계산 시간: {elapsed:.6f}초&quot;)
print(&quot;로그 시간이므로 P에 속함!&quot;)</code></pre>
<h3 id="예시-4-연결성-확인">예시 4: 연결성 확인</h3>
<pre><code class="language-python">def is_connected_decision(graph):
    &quot;&quot;&quot;
    결정 문제: 그래프가 연결되어 있는가?

    해결: BFS 또는 DFS 사용
    시간복잡도: O(V + E)
    - 다항 시간!

    따라서 연결성 문제 ∈ P

    graph: 그래프 {정점: [이웃들]}

    Returns: True if 모든 정점이 연결됨
    &quot;&quot;&quot;
    if not graph:
        return True

    # 임의의 시작 정점
    start = next(iter(graph))

    # BFS로 도달 가능한 정점들 찾기
    visited = bfs(graph, start)

    # 모든 정점을 방문했는가?
    return len(visited) == len(graph)

def bfs(graph, start):
    &quot;&quot;&quot;
    너비 우선 탐색 (BFS)

    시간복잡도: O(V + E)
    - 각 정점: 1번 방문
    - 각 간선: 1번 확인

    graph: 그래프
    start: 시작 정점

    Returns: 방문한 정점들의 집합
    &quot;&quot;&quot;
    from collections import deque

    visited = set()
    queue = deque([start])
    visited.add(start)

    while queue:
        # 큐에서 정점 꺼내기
        current = queue.popleft()

        # 이웃들 확인
        for neighbor in graph.get(current, []):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)

    return visited

# 사용 예시
# 연결된 그래프
connected_graph = {
    &#39;A&#39;: [&#39;B&#39;, &#39;C&#39;],
    &#39;B&#39;: [&#39;A&#39;, &#39;D&#39;],
    &#39;C&#39;: [&#39;A&#39;, &#39;D&#39;],
    &#39;D&#39;: [&#39;B&#39;, &#39;C&#39;]
}

# 연결 안 된 그래프
disconnected_graph = {
    &#39;A&#39;: [&#39;B&#39;],
    &#39;B&#39;: [&#39;A&#39;],
    &#39;C&#39;: [&#39;D&#39;],
    &#39;D&#39;: [&#39;C&#39;]
}

print(&quot;연결성 확인:&quot;)
print(f&quot;그래프 1 연결됨? {is_connected_decision(connected_graph)}&quot;)
print(f&quot;그래프 2 연결됨? {is_connected_decision(disconnected_graph)}&quot;)

# 방문한 정점들 확인
print(f&quot;\n그래프 1에서 A부터 도달 가능: {bfs(connected_graph, &#39;A&#39;)}&quot;)
print(f&quot;그래프 2에서 A부터 도달 가능: {bfs(disconnected_graph, &#39;A&#39;)}&quot;)

print(&quot;\nBFS는 O(V+E) 시간이므로 P에 속함!&quot;)</code></pre>
<hr>
<h2 id="🔒-p의-성질">🔒 P의 성질</h2>
<h3 id="닫힘-성질-closure-properties">닫힘 성질 (Closure Properties)</h3>
<p><strong>P는 다양한 연산에 대해 닫혀 있습니다.</strong></p>
<p><strong>1. 합집합 (Union):</strong></p>
<pre><code>L₁ ∈ P, L₂ ∈ P이면
L₁ ∪ L₂ ∈ P

증명:
- L₁ 판단: O(n^k₁)
- L₂ 판단: O(n^k₂)
- 합집합 판단: 둘 중 하나만 Yes면 Yes
  → max(O(n^k₁), O(n^k₂)) = O(n^max(k₁,k₂))
  → 여전히 다항!

코드:
```python
def union_decision(w, L1_decider, L2_decider):
    &quot;&quot;&quot;
    L₁ ∪ L₂ 판단

    w ∈ L₁ ∪ L₂ ⟺ w ∈ L₁ OR w ∈ L₂
    &quot;&quot;&quot;
    return L1_decider(w) or L2_decider(w)
    # 시간: O(n^k₁) + O(n^k₂) = O(n^max(k₁,k₂))</code></pre><p><strong>2. 교집합 (Intersection):</strong></p>
<pre><code>L₁ ∈ P, L₂ ∈ P이면
L₁ ∩ L₂ ∈ P

증명:
- 교집합 판단: 둘 다 Yes여야 Yes
  → O(n^k₁) + O(n^k₂) = O(n^max(k₁,k₂))
  → 여전히 다항!

코드:
```python
def intersection_decision(w, L1_decider, L2_decider):
    &quot;&quot;&quot;
    L₁ ∩ L₂ 판단

    w ∈ L₁ ∩ L₂ ⟺ w ∈ L₁ AND w ∈ L₂
    &quot;&quot;&quot;
    return L1_decider(w) and L2_decider(w)
    # 시간: O(n^k₁) + O(n^k₂)</code></pre><p><strong>3. 여집합 (Complement):</strong></p>
<pre><code>L ∈ P이면
L̄ (여집합) ∈ P

증명:
- L 판단: O(n^k)
- 여집합 판단: L의 반대
  → O(n^k)
  → 여전히 다항!

코드:
```python
def complement_decision(w, L_decider):
    &quot;&quot;&quot;
    L̄ 판단

    w ∈ L̄ ⟺ w ∉ L
    &quot;&quot;&quot;
    return not L_decider(w)
    # 시간: O(n^k)</code></pre><p><strong>4. 연결 (Concatenation):</strong></p>
<pre><code>L₁ ∈ P, L₂ ∈ P이면
L₁ · L₂ ∈ P

L₁ · L₂ = {xy | x ∈ L₁, y ∈ L₂}

증명:
- 입력 w를 모든 가능한 위치에서 분할
- 각 분할에 대해 L₁, L₂ 판단
- 분할 개수: O(n)
- 각 판단: O(n^k₁) + O(n^k₂)
- 총: O(n) × O(n^max(k₁,k₂)) = O(n^(max(k₁,k₂)+1))
- 여전히 다항!

코드:
```python
def concatenation_decision(w, L1_decider, L2_decider):
    &quot;&quot;&quot;
    L₁ · L₂ 판단

    w ∈ L₁ · L₂ ⟺ ∃i: w[:i] ∈ L₁ AND w[i:] ∈ L₂
    &quot;&quot;&quot;
    # 모든 분할 시도
    for i in range(len(w) + 1):
        left = w[:i]
        right = w[i:]

        # 왼쪽은 L₁에, 오른쪽은 L₂에?
        if L1_decider(left) and L2_decider(right):
            return True

    return False
    # 시간: O(n) × (O(n^k₁) + O(n^k₂))</code></pre><h3 id="p--co-p">P = co-P</h3>
<p>어떤 문제 L이 P에 속한다면, 그 문제의 여집합(반대 질문)인 L̄도 항상 P에 속한다는 의미로 P 클래스에서는 이 관계가 항상 성립합니다.</p>
<ul>
<li>co-P는 P의 여집합 (Complementary)을 의미</li>
</ul>
<p><strong>중요한 성질:</strong></p>
<pre><code>정리: P = co-P

의미: L ∈ P이면 L̄ ∈ P

즉: &quot;예&quot;라고 판단하는 것과 &quot;아니오&quot;라고 판단하는 것이 P에서는 똑같이 쉬움!

이유: 답을 뒤집으면 됨!</code></pre><p><strong>예시:</strong></p>
<pre><code class="language-python"># L: 짝수 집합
def is_even(n):
    &quot;&quot;&quot;L: 짝수인가?&quot;&quot;&quot;
    return n % 2 == 0
    # P에 속함

# L̄: 홀수 집합
def is_odd(n):
    &quot;&quot;&quot;L̄: 홀수인가? (짝수가 아닌가?)&quot;&quot;&quot;
    return not is_even(n)
    # 여전히 P에 속함!

# 둘 다 같은 시간
# → P = co-P</code></pre>
<hr>
<h2 id="🎯-p의-중요성">🎯 P의 중요성</h2>
<h3 id="실무적-의미">실무적 의미</h3>
<pre><code>문제가 P에 속한다 = &quot;효율적으로 풀 수 있다&quot;

결과:
1. 알고리즘 존재
   - 다항 시간 알고리즘이 있음
   - 찾으면 됨!

2. 확장 가능
   - 입력이 커져도 OK
   - 실용적 사용 가능

3. 최적화 가능
   - 더 나은 알고리즘 찾기
   - 상수 계수 개선

예:
정렬 ∈ P
→ 병합 정렬, 퀵 정렬 등
→ 큰 데이터도 정렬 가능
→ 실무에서 널리 사용</code></pre><h3 id="이론적-의미">이론적 의미</h3>
<pre><code>P는 복잡도 이론의 기준점

P의 역할:
1. &quot;쉬운 문제&quot;의 정의
2. NP와 비교 대상
3. P vs NP 문제의 한 축

질문:
&quot;모든 NP 문제가 P인가?&quot;
→ P = NP?
→ 최대 미스터리!</code></pre><hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<p><strong>문제가 P에 속하는지 확인하기</strong></p>
<pre><code class="language-python">def check_if_in_P(problem_name):
    &quot;&quot;&quot;
    문제가 P에 속하는지 확인하는 사고 과정
    &quot;&quot;&quot;
    print(f&quot;문제: {problem_name}&quot;)

    # 1단계: 다항 시간 알고리즘이 알려져 있나?
    known_P_problems = {
        &#39;정렬&#39;: &#39;병합 정렬 O(n log n)&#39;,
        &#39;최단 경로&#39;: &#39;다익스트라 O((V+E) log V)&#39;,
        &#39;최대 유량&#39;: &#39;에드몬즈-카프 O(VE²)&#39;,
        &#39;최대공약수&#39;: &#39;유클리드 O(log n)&#39;,
        &#39;소수 판별&#39;: &#39;AKS O((log n)^6)&#39;,
        &#39;이진 탐색&#39;: &#39;O(log n)&#39;,
        &#39;연결성&#39;: &#39;BFS/DFS O(V+E)&#39;
    }

    if problem_name in known_P_problems:
        algo = known_P_problems[problem_name]
        print(f&quot;✓ P에 속함!&quot;)
        print(f&quot;  알고리즘: {algo}&quot;)
        return True

    # 2단계: 비슷한 문제가 P인가?
    print(&quot;? 알려진 P 문제와 비교 필요&quot;)
    print(&quot;  - 환원 가능한가?&quot;)
    print(&quot;  - 유사한 기법 적용 가능한가?&quot;)

    # 3단계: NP-완전으로 알려졌나?
    known_NPC = {
        &#39;외판원&#39;, &#39;배낭&#39;, &#39;그래프 색칠&#39;,
        &#39;해밀턴 경로&#39;, &#39;부분집합 합&#39;
    }

    if problem_name in known_NPC:
        print(&quot;✗ NP-완전!&quot;)
        print(&quot;  P에 속할 가능성 낮음&quot;)
        return False

    print(&quot;? 추가 연구 필요&quot;)
    return None

# 예시
check_if_in_P(&#39;정렬&#39;)
print()
check_if_in_P(&#39;외판원&#39;)</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>P 클래스</strong></p>
<pre><code>정의: P = 다항 시간에 해결 가능한 결정 문제들

형식: P = {L | L은 O(n^k) 튜링 기계로 결정 가능}

의미: &quot;효율적으로 풀 수 있는 문제&quot;</code></pre><p><strong>다항 시간</strong></p>
<pre><code>포함:
O(1), O(log n), O(n), O(n log n),
O(n²), O(n³), ..., O(n^k)

제외:
O(2^n), O(n!), O(n^n)

이유:
다항은 현실적
지수는 빠르게 불가능</code></pre><p><strong>P의 성질</strong></p>
<pre><code>닫힘 성질:
- 합집합 ✓
- 교집합 ✓
- 여집합 ✓
- 연결 ✓

특수 성질:
- P = co-P
- 합성에 안정적
- 하드웨어 독립적</code></pre><p><strong>대표 예시</strong></p>
<pre><code>P에 속하는 문제:
- 정렬: O(n log n)
- 최단 경로: O((V+E) log V)
- 최대공약수: O(log n)
- 연결성: O(V+E)
- 소수 판별: O((log n)^6)</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[08-03] 클래스 NP (Nondeterministic Polynomial Time)</strong></p>
<ul>
<li>NP 클래스의 정의: 다항 시간에 검증할 수 있는 문제들</li>
<li>비결정적 튜링 기계: &quot;운이 좋으면&quot; 빠르게 푸는 기계</li>
<li>증명자와 검증자: 답을 제시하는 사람과 확인하는 사람</li>
<li>P와 NP의 관계: P ⊆ NP는 확실, P = NP는 미지수</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[08-01] 결정 문제</a><br><strong>다음 글</strong>: <a href="#">[08-03] 클래스 NP</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [08-01] 결정 문제 (Decision Problems)]]></title>
            <link>https://velog.io/@road_to_ai/08-01-%EA%B2%B0%EC%A0%95-%EB%AC%B8%EC%A0%9C-Decision-Problems</link>
            <guid>https://velog.io/@road_to_ai/08-01-%EA%B2%B0%EC%A0%95-%EB%AC%B8%EC%A0%9C-Decision-Problems</guid>
            <pubDate>Sun, 08 Mar 2026 03:19:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>결정 문제는 답이 Yes 또는 No인 문제로, 계산 복잡도 이론의 기본 단위입니다.</p>
</blockquote>
<hr>
<h2 id="🎯-계산-복잡도-이론이란">🎯 계산 복잡도 이론이란?</h2>
<h3 id="섹션-개요">섹션 개요</h3>
<p>알고리즘 분석에서는 <strong>주어진 알고리즘</strong>의 효율성을 측정했습니다.</p>
<pre><code>알고리즘 분석:
&quot;이 알고리즘이 얼마나 빠른가?&quot;

예:
- 병합 정렬: O(n log n)
- 버블 정렬: O(n²)
→ 병합 정렬이 더 빠르다!</code></pre><p>계산 복잡도 이론에서는 <strong>문제 자체</strong>의 난이도를 분석합니다.</p>
<pre><code>복잡도 이론:
&quot;이 문제를 효율적으로 풀 수 있는가?&quot;

예:
- 정렬 문제: 효율적 알고리즘 존재 (O(n log n))
- 외판원 문제: 효율적 알고리즘이 없을 수도 있음
→ 문제의 본질적 어려움!</code></pre><h3 id="핵심-질문들">핵심 질문들</h3>
<p><strong>이번 섹션에서 다룰 근본적 질문:</strong></p>
<pre><code>질문 1: 어떤 문제들을 효율적으로 풀 수 있는가?
→ P 클래스

질문 2: 답은 빠르게 확인할 수 있지만 찾기는 어려운 문제는?
→ NP 클래스

질문 3: 가장 어려운 문제들은 무엇인가?
→ NP-완전

질문 4: 컴퓨터가 절대 풀 수 없는 문제도 있는가?
→ 계산 불가능성</code></pre><h3 id="왜-중요한가">왜 중요한가?</h3>
<p><strong>실무적 중요성:</strong></p>
<pre><code>새로운 문제를 만났을 때:

시나리오 1: 문제가 P에 속함
→ &quot;효율적인 알고리즘이 존재한다&quot;
→ 열심히 찾으면 된다!
→ 예: 정렬, 최단 경로

시나리오 2: 문제가 NP-완전
→ &quot;효율적인 알고리즘이 없을 가능성 높음&quot;
→ 근사 알고리즘 사용
→ 또는 작은 입력만 처리
→ 예: 외판원, 배낭

시나리오 3: 문제가 계산 불가능
→ &quot;컴퓨터로 절대 풀 수 없음&quot;
→ 다른 접근 필요
→ 예: 정지 문제</code></pre><p><strong>이론적 중요성:</strong></p>
<pre><code>P vs NP 문제:
- 클레이 수학연구소의 7대 난제
- 해결하면 100만 달러 상금
- 컴퓨터 과학 최대의 미스터리

의미:
&quot;빠르게 확인할 수 있으면
 빠르게 찾을 수도 있는가?&quot;

영향:
- 암호학: 해결되면 현재 암호 체계 붕괴
- 최적화: 많은 문제가 쉬워짐
- 수학: 자동 증명 가능</code></pre><h3 id="이번-섹션-구성">이번 섹션 구성</h3>
<pre><code>08-01. 결정 문제 (Decision Problems)
       ↓ 문제를 Yes/No로 표현

08-02. 클래스 P
       ↓ 효율적으로 풀 수 있는 문제들

08-03. 클래스 NP
       ↓ 효율적으로 검증할 수 있는 문제들

08-04. NP-완전 (NP-Complete)
       ↓ 가장 어려운 NP 문제들

08-05. NP-난해 (NP-Hard)
       ↓ NP-완전만큼 또는 더 어려운 문제들

08-06. 다항 시간 환원 (Polynomial-time Reduction)
       ↓ 문제들을 비교하는 방법

08-07. 계산 불가능성 (Undecidability)
       ↓ 풀 수 없는 문제들

08-08. 정지 문제 (Halting Problem)
       ↓ 대표적인 불가능 문제

08-09. 기타 복잡도 클래스
       ↓ co-NP, PSPACE, EXPTIME 등</code></pre><hr>
<h2 id="🎯-결정-문제란-무엇인가">🎯 결정 문제란 무엇인가</h2>
<h3 id="결정-문제의-정의">결정 문제의 정의</h3>
<p><strong>결정 문제(Decision Problem)</strong>는 답이 <strong>Yes</strong> 또는 <strong>No</strong>인 문제입니다.</p>
<p><strong>실생활 비유:</strong></p>
<pre><code>일반 질문 vs 결정 문제:

일반 질문:
&quot;이 배열을 정렬하면?&quot;
→ 답: [1, 2, 3, 4, 5]
→ 답이 배열 (복잡)

결정 문제:
&quot;이 배열이 정렬되어 있나?&quot;
→ 답: Yes 또는 No
→ 답이 단순!

일반 질문:
&quot;서울에서 부산까지 최단 경로는?&quot;
→ 답: 특정 경로
→ 답이 경로 (복잡)

결정 문제:
&quot;서울에서 부산까지 300km 이내 경로가 있나?&quot;
→ 답: Yes 또는 No
→ 답이 단순!</code></pre><p><strong>왜 Yes/No로 제한할까?</strong></p>
<pre><code>이유 1: 이론적 분석이 쉬움
- 답의 형태가 단순
- 수학적 모델링 용이
- 복잡도 비교 명확

이유 2: 충분히 강력함
- 모든 문제를 결정 문제로 변환 가능
- 결정 문제 풀면 원래 문제도 풀 수 있음
- (나중에 증명)

이유 3: 본질에 집중
- 문제의 핵심 어려움 파악
- 불필요한 복잡성 제거</code></pre><h3 id="형식적-정의">형식적 정의</h3>
<pre><code>결정 문제:
- 입력: 문자열 w
- 질문: w가 조건 C를 만족하는가?
- 출력: Yes 또는 No

수학적:
L = {w | w는 조건 C를 만족}

L은 &quot;언어(Language)&quot;라고 부름
결정 문제 = 언어 인식 문제</code></pre><p><strong>예시:</strong></p>
<pre><code class="language-python"># 결정 문제 1: 짝수 판별
def is_even(n):
    &quot;&quot;&quot;
    결정 문제: n이 짝수인가?

    입력: 정수 n
    출력: Yes (True) 또는 No (False)

    언어 표현:
    L_even = {0, 2, 4, 6, 8, ...}
           = {n | n % 2 == 0}
    &quot;&quot;&quot;
    return n % 2 == 0

# 테스트
print(is_even(4))   # True (Yes)
print(is_even(7))   # False (No)


# 결정 문제 2: 정렬 여부
def is_sorted(arr):
    &quot;&quot;&quot;
    결정 문제: 배열이 정렬되어 있는가?

    입력: 배열
    출력: Yes (True) 또는 No (False)

    언어 표현:
    L_sorted = {모든 정렬된 배열}
    &quot;&quot;&quot;
    for i in range(len(arr) - 1):
        if arr[i] &gt; arr[i + 1]:
            return False  # No
    return True  # Yes

# 테스트
print(is_sorted([1, 2, 3, 4]))  # True
print(is_sorted([1, 3, 2, 4]))  # False</code></pre>
<hr>
<h2 id="🔄-최적화-문제-vs-결정-문제">🔄 최적화 문제 vs 결정 문제</h2>
<h3 id="문제-변환">문제 변환</h3>
<p>대부분의 실무 문제는 <strong>최적화 문제</strong>입니다. 하지만 이를 <strong>결정 문제</strong>로 변환할 수 있습니다.</p>
<p><strong>변환 패턴:</strong></p>
<pre><code>최적화 문제:
&quot;최소/최대 값은?&quot;

↓ 변환

결정 문제:
&quot;값이 k 이하/이상인가?&quot;</code></pre><h3 id="예시-1-최단-경로">예시 1: 최단 경로</h3>
<pre><code class="language-python"># 최적화 문제: 최단 경로 찾기
def shortest_path(graph, start, end):
    &quot;&quot;&quot;
    최적화 문제

    질문: start에서 end까지의 최단 거리는?
    답: 숫자 (예: 42km)

    복잡: 경로를 찾아야 함
    &quot;&quot;&quot;
    # 다익스트라 알고리즘 등 사용
    # ...
    return distance


# 결정 문제: 경로 존재 여부
def has_path_within(graph, start, end, max_distance):
    &quot;&quot;&quot;
    결정 문제

    질문: start에서 end까지 max_distance 이하의 경로가 있는가?
    답: Yes 또는 No

    단순: 존재 여부만 확인
    &quot;&quot;&quot;
    shortest = shortest_path(graph, start, end)
    return shortest &lt;= max_distance  # Yes or No


# 사용 예시
graph = {
    &#39;A&#39;: [(&#39;B&#39;, 10), (&#39;C&#39;, 30)],
    &#39;B&#39;: [(&#39;D&#39;, 20)],
    &#39;C&#39;: [(&#39;D&#39;, 5)],
    &#39;D&#39;: []
}

# 최적화 문제
distance = shortest_path(graph, &#39;A&#39;, &#39;D&#39;)
print(f&quot;최단 거리: {distance}km&quot;)  # 30km

# 결정 문제
result = has_path_within(graph, &#39;A&#39;, &#39;D&#39;, 25)
print(f&quot;25km 이내 경로 있나? {result}&quot;)  # No

result = has_path_within(graph, &#39;A&#39;, &#39;D&#39;, 35)
print(f&quot;35km 이내 경로 있나? {result}&quot;)  # Yes</code></pre>
<h3 id="예시-2-외판원-문제-tsp">예시 2: 외판원 문제 (TSP)</h3>
<pre><code class="language-python"># 최적화 문제: 최단 순회 찾기
def tsp_optimization(cities, distances):
    &quot;&quot;&quot;
    최적화 TSP (Traveling Salesman Problem)

    질문: 모든 도시를 방문하는 최단 경로는?
    답: 경로와 거리

    매우 어려움!
    &quot;&quot;&quot;
    # 모든 순열 확인하거나 휴리스틱 사용
    best_tour = None
    best_distance = float(&#39;inf&#39;)

    # ... 복잡한 알고리즘 ...

    return best_tour, best_distance


# 결정 문제: 제한된 순회 존재 여부
def tsp_decision(cities, distances, max_distance):
    &quot;&quot;&quot;
    결정 TSP

    질문: 총 거리가 max_distance 이하인 순회가 있는가?
    답: Yes 또는 No

    여전히 어렵지만 형태는 단순!
    &quot;&quot;&quot;
    tour, distance = tsp_optimization(cities, distances)
    return distance &lt;= max_distance  # Yes or No


# 사용 예시
cities = [&#39;서울&#39;, &#39;부산&#39;, &#39;대구&#39;, &#39;광주&#39;]
distances = [
    [0, 400, 300, 250],
    [400, 0, 150, 300],
    [300, 150, 0, 200],
    [250, 300, 200, 0]
]

# 최적화 문제
tour, distance = tsp_optimization(cities, distances)
print(f&quot;최단 순회: {tour}, 거리: {distance}km&quot;)

# 결정 문제
result = tsp_decision(cities, distances, 1000)
print(f&quot;1000km 이내 순회 있나? {result}&quot;)  # Yes or No</code></pre>
<h3 id="왜-결정-문제가-충분한가">왜 결정 문제가 충분한가?</h3>
<p><strong>핵심 정리:</strong></p>
<pre><code>정리:
최적화 문제를 풀 수 있으면 → 결정 문제도 풀 수 있다

증명:
최적값을 알면 → &quot;값이 k 이하인가?&quot;에 답할 수 있음

역방향도 가능:
결정 문제를 여러 번 풀면 → 최적값을 찾을 수 있다 (이진 탐색)</code></pre><p><strong>이진 탐색으로 최적값 찾기:</strong></p>
<pre><code class="language-python">def find_shortest_path_value(graph, start, end):
    &quot;&quot;&quot;
    결정 문제를 여러 번 풀어서 최적값 찾기

    전략:
    1. 가능한 거리 범위 설정
    2. 이진 탐색으로 최소 거리 찾기
    3. 결정 문제만 사용!
    &quot;&quot;&quot;
    # 1. 범위 설정
    low = 0
    high = 10000  # 충분히 큰 값

    # 2. 이진 탐색
    result = high

    while low &lt;= high:
        mid = (low + high) // 2

        # 결정 문제: mid 이하 경로 있나?
        if has_path_within(graph, start, end, mid):
            result = mid  # 가능! 더 작은 값 시도
            high = mid - 1
        else:
            low = mid + 1  # 불가능! 더 큰 값 필요

    return result

# 사용
graph = {
    &#39;A&#39;: [(&#39;B&#39;, 10), (&#39;C&#39;, 30)],
    &#39;B&#39;: [(&#39;D&#39;, 20)],
    &#39;C&#39;: [(&#39;D&#39;, 5)],
    &#39;D&#39;: []
}

min_distance = find_shortest_path_value(graph, &#39;A&#39;, &#39;D&#39;)
print(f&quot;최단 거리: {min_distance}&quot;)  # 30

# 결정 문제만으로 최적값을 찾았다!
# 이진 탐색 횟수: O(log max_distance)</code></pre>
<p><strong>결론:</strong></p>
<pre><code>결정 문제가 어려우면 → 최적화 문제도 어렵다

결정 문제가 쉬우면 → 최적화 문제도 쉽다

따라서:
결정 문제의 복잡도 = 원래 문제의 복잡도</code></pre><hr>
<h2 id="📚-형식-언어로서의-문제">📚 형식 언어로서의 문제</h2>
<h3 id="언어의-개념">언어의 개념</h3>
<p><strong>형식 언어(Formal Language)</strong>는 문자열의 집합입니다.</p>
<pre><code>알파벳:
Σ = {0, 1}  (또는 다른 기호들)

문자열:
w = &quot;0110&quot;  (알파벳의 기호들을 나열)

언어:
L = {모든 짝수 개의 0을 가진 문자열}
  = {&quot;&quot;, &quot;00&quot;, &quot;11&quot;, &quot;0011&quot;, &quot;1100&quot;, ...}</code></pre><p><strong>결정 문제 = 언어 인식:</strong></p>
<pre><code>문제: 주어진 문자열 w가 언어 L에 속하는가?
답: Yes (w ∈ L) 또는 No (w ∉ L)

예:
L = {짝수를 표현하는 이진 문자열}
w = &quot;100&quot; (이진수 4)
→ 4는 짝수 → Yes</code></pre><h3 id="예시-소수-판별">예시: 소수 판별</h3>
<pre><code class="language-python">def is_prime_language(n):
    &quot;&quot;&quot;
    소수 판별을 언어 문제로

    언어 정의:
    L_prime = {2, 3, 5, 7, 11, 13, ...}
            = {모든 소수}

    결정 문제:
    입력 n이 L_prime에 속하는가?
    &quot;&quot;&quot;
    if n &lt; 2:
        return False  # 1 이하는 소수 아님

    # 소수 판별
    for i in range(2, int(n ** 0.5) + 1):  # int(n ** 0.5): 소수(Prime Number) 판별을 할 때 사용(sqrt{n})
        if n % i == 0:
            return False  # 약수 발견, 소수 아님

    return True  # 소수!

# 언어 관점에서 보기
print(&quot;2 ∈ L_prime?&quot;, is_prime_language(2))    # True
print(&quot;4 ∈ L_prime?&quot;, is_prime_language(4))    # False
print(&quot;17 ∈ L_prime?&quot;, is_prime_language(17))  # True

# 언어:
L_prime = {2, 3, 5, 7, 11, 13, 17, 19, 23, ...}</code></pre>
<h3 id="왜-언어로-표현하는가">왜 언어로 표현하는가?</h3>
<p><strong>수학적 엄밀성:</strong></p>
<pre><code>언어 표현:
- 집합론 사용
- 명확한 정의
- 증명 가능

예:
L₁ = {정렬된 배열}
L₂ = {소수}
L₁ ∩ L₂ = ?  (집합 연산 가능)</code></pre><p><strong>튜링 기계와 연결:</strong></p>
<pre><code>튜링 기계:
- 입력 문자열 받음
- Yes/No 출력

언어:
- 튜링 기계가 Yes라고 하는 문자열들
- L = {w | 튜링 기계 M이 w를 수락}

결정 문제 = 언어 = 튜링 기계
→ 세 가지가 같은 개념!</code></pre><hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<p><strong>문제를 결정 문제로 변환하기</strong></p>
<pre><code class="language-python"># 패턴 1: 존재 문제
# &quot;~를 찾아라&quot; → &quot;~가 존재하는가?&quot;

# 최적화: 해밀턴 경로 찾기
def find_hamiltonian_path(graph):
    &quot;&quot;&quot;모든 정점을 한 번씩 방문하는 경로 찾기&quot;&quot;&quot;
    # ...

# 결정: 해밀턴 경로 존재 여부
def has_hamiltonian_path(graph):
    &quot;&quot;&quot;해밀턴 경로가 존재하는가?&quot;&quot;&quot;
    # ...
    return True  # or False


# 패턴 2: 제한 조건 추가
# &quot;최소/최대는?&quot; → &quot;k 이하/이상인가?&quot;

# 최적화: 최소 색칠 수
def min_coloring(graph):
    &quot;&quot;&quot;그래프를 칠하는데 필요한 최소 색 개수&quot;&quot;&quot;
    # ...

# 결정: k색으로 칠할 수 있는가?
def can_color_with_k(graph, k):
    &quot;&quot;&quot;k개 색으로 그래프를 칠할 수 있는가?&quot;&quot;&quot;
    # ...
    return True  # or False</code></pre>
<p><strong>실무에서 결정 문제 활용</strong></p>
<pre><code class="language-python"># 타당성 검사 (Feasibility Check)
def is_schedule_feasible(tasks, deadline):
    &quot;&quot;&quot;
    주어진 마감일 내에 모든 작업 완료 가능한가?

    실무 활용:
    - 프로젝트 계획 검증
    - 리소스 할당 확인
    - 납기일 체크
    &quot;&quot;&quot;
    total_time = sum(task.duration for task in tasks)
    return total_time &lt;= deadline  # Yes or No


# 제약 만족 (Constraint Satisfaction)
def satisfies_constraints(assignment, constraints):
    &quot;&quot;&quot;
    주어진 할당이 모든 제약을 만족하는가?

    실무 활용:
    - 시간표 작성
    - 자원 배치
    - 규칙 검증
    &quot;&quot;&quot;
    for constraint in constraints:
        if not constraint.check(assignment):
            return False  # 제약 위반
    return True  # 모든 제약 만족</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>결정 문제</strong></p>
<pre><code>정의:
- 답이 Yes 또는 No인 문제
- 형식: &quot;조건 C를 만족하는가?&quot;
- 언어: L = {조건 C를 만족하는 입력들}

특징:
- 이론적 분석 용이
- 모든 문제를 변환 가능
- 본질에 집중</code></pre><p><strong>최적화 vs 결정</strong></p>
<pre><code>최적화 문제:
&quot;최소/최대 값은?&quot;
→ 복잡한 답

결정 문제:
&quot;값이 k 이하/이상인가?&quot;
→ Yes/No

관계:
최적화 ≡ 결정
(이진 탐색으로 변환 가능)</code></pre><p><strong>언어 표현</strong></p>
<pre><code>결정 문제 = 언어 인식
- L = {조건 만족하는 문자열}
- 문제: w ∈ L인가?
- 튜링 기계와 연결</code></pre><p><strong>실무 활용</strong></p>
<pre><code>타당성 검사:
- 가능한가?
- 만족하는가?

제약 조건:
- 규칙 준수?
- 한계 내?</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[08-02] 클래스 P (Polynomial Time)</strong></p>
<ul>
<li>P 클래스의 엄밀한 정의: 다항 시간 튜링 기계로 풀 수 있는 문제들</li>
<li>다항 시간의 의미: 왜 O(n^k)를 &quot;효율적&quot;이라고 하는가</li>
<li>P 클래스 예시: 정렬, 최단 경로, 최대 유량 등</li>
<li>P의 성질: 닫힘 성질과 안정성</li>
</ul>
<hr>
<p><strong>이전 섹션</strong>: <a href="#">[07-06] 상각 분석</a><br><strong>다음 글</strong>: <a href="#">[08-02] 클래스 P</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [07-06] 상각 분석 (Amortized Analysis)]]></title>
            <link>https://velog.io/@road_to_ai/07-06-%EC%83%81%EA%B0%81-%EB%B6%84%EC%84%9D-Amortized-Analysis</link>
            <guid>https://velog.io/@road_to_ai/07-06-%EC%83%81%EA%B0%81-%EB%B6%84%EC%84%9D-Amortized-Analysis</guid>
            <pubDate>Fri, 06 Mar 2026 13:52:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>상각 분석은 연속된 연산들의 평균 비용을 계산하여, 가끔 발생하는 비싼 연산의 영향을 전체적으로 분석하는 기법입니다.</p>
</blockquote>
<hr>
<h2 id="🎯-상각-분석이란-무엇인가">🎯 상각 분석이란 무엇인가</h2>
<h3 id="상각-분석의-기본-개념">상각 분석의 기본 개념</h3>
<p><strong>상각(償却, Amortize)</strong>의 사전적 의미는 &quot;빚을 나누어 갚다&quot;입니다. </p>
<p>프로그래밍에서는 <strong>&quot;비싼 연산의 비용을 여러 연산에 나누어 계산한다&quot;</strong>는 의미입니다.</p>
<p><strong>실생활 비유</strong>:</p>
<pre><code>자동차 유지비 계산:

방법 1: 매월 비용 따로 계산
- 1월: 10만원 (기름값)
- 2월: 10만원
- 3월: 10만원
- 4월: 410만원 (보험료 400만원 + 기름값)
- 5월: 10만원
→ &quot;4월이 너무 비싸다!&quot;

방법 2: 1년 평균으로 계산 (상각)
- 총 비용: 450만원
- 월 평균: 450 ÷ 12 = 37.5만원
→ &quot;매달 평균 37.5만원&quot;

상각 분석은 방법 2처럼 비싼 연산을 평균으로 계산!</code></pre><p><strong>프로그래밍 예시</strong>:</p>
<pre><code class="language-python"># Python의 list를 사용해 봅시다
arr = []

# 1번째 추가
arr.append(1)  # 빠름 (0.001초)
print(f&quot;크기: {len(arr)}&quot;)  # 1

# 2번째 추가
arr.append(2)  # 빠름 (0.001초)
print(f&quot;크기: {len(arr)}&quot;)  # 2

# 3번째 추가
arr.append(3)  # 빠름 (0.001초)
print(f&quot;크기: {len(arr)}&quot;)  # 3

# 4번째 추가 - 갑자기 느림!
arr.append(4)  # 느림 (0.100초) ← 왜?
print(f&quot;크기: {len(arr)}&quot;)  # 4
# 이유: 내부적으로 더 큰 배열을 만들고 기존 데이터를 복사

# 5번째 추가
arr.append(5)  # 다시 빠름 (0.001초)</code></pre>
<p><strong>패턴 발견:</strong></p>
<pre><code>대부분은 빠름 (0.001초)
가~끔만 느림 (0.100초)

100번 추가하면?
- 빠른 거: 95번
- 느린 거: 5번
→ 평균적으로는 빠르다!</code></pre><h3 id="왜-상각-분석이-필요한가">왜 상각 분석이 필요한가?</h3>
<p><strong>문제 상황: 너무 비관적인 분석</strong></p>
<pre><code>상황: 리스트에 100개 추가하기

방법 1: &quot;최악의 경우만 보기&quot; (너무 비관적)
-----------------------------------------------
&quot;append가 최악의 경우 느리네?
그럼 100번 하면 엄청 느리겠다!&quot;

계산:
- append 1번: 최악 0.1초
- 100번: 0.1 × 100 = 10초!
→ &quot;와... 10초나 걸려? 쓰지 말아야겠다&quot;

실제로 측정해보니:
- 100번 append: 0.2초
→ &quot;어? 10초가 아니라 0.2초네?&quot;


방법 2: &quot;상각 분석&quot; (현실적)
-----------------------------------------------
&quot;느린 경우는 가끔만 발생하니까
전체 평균을 보자!&quot;

계산:
- 빠른 append: 95번 × 0.001초 = 0.095초
- 느린 append: 5번 × 0.02초 = 0.1초
- 총: 0.195초
- 평균: 0.195 ÷ 100 = 0.00195초

→ &quot;평균적으로 매우 빠르네!&quot;</code></pre><p><strong>왜 차이가 날까?</strong></p>
<pre><code>핵심: &quot;느린 경우가 자주 발생하지 않는다&quot;

예시: 1,000번 append
---------------------------------------
1번째: 빠름
2번째: 빠름
3번째: 느림 ← 확장!
4번째: 빠름
5번째: 빠름
6번째: 빠름
7번째: 느림 ← 확장!
...
(중간 생략)
...
1000번째: 빠름

확장 횟수: 약 10번만!
빠른 경우: 990번

→ 대부분은 빠르다!</code></pre><p><strong>상각 vs 평균의 차이 (쉽게!)</strong></p>
<pre><code>쉬운 비유: 버스 타기

평균 경우 분석 (Average Case):
---------------------------------------
&quot;버스가 평균적으로 몇 분에 올까?&quot;

전제:
- 버스는 랜덤하게 온다
- 5분에 한 대씩 온다고 가정

계산:
- 평균 대기 시간: 2.5분
- 확률 분포를 가정함!

예: &quot;운이 좋으면 바로 오고, 나쁘면 5분 기다림&quot;


상각 분석 (Amortized):
---------------------------------------
&quot;버스를 10번 타는데 총 얼마나 걸릴까?&quot;

전제:
- 확률 가정 없음
- 실제 발생하는 일만 봄

계산:
1번: 1분 대기
2번: 1분 대기
3번: 1분 대기
4번: 5분 대기 ← 한 번만 오래 걸림
5번: 1분 대기
...
10번: 1분 대기

총 시간: 20분
평균: 20분 ÷ 10번 = 2분

예: &quot;가끔 오래 걸리지만, 여러 번 타면 평균 2분&quot;</code></pre><p><strong>차이 요약</strong></p>
<pre><code>평균 경우:
- &quot;운이 어떨까?&quot; (확률)
- 입력이 랜덤하다고 가정
- 예: &quot;절반쯤에 있을 거야&quot;

상각:
- &quot;여러 번 하면 평균이 어떨까?&quot; (실제)
- 확률 가정 안 함
- 예: &quot;100번 했더니 이렇게 걸렸어&quot;

쉽게:
평균 = 예측 (운에 달림)
상각 = 측정 (실제 평균)</code></pre><p><strong>실생활 예시</strong></p>
<pre><code>영화관 줄서기

평균 경우:
&quot;줄이 평균적으로 몇 명일까?&quot;
→ 예측, 운에 달림

상각:
&quot;나는 한 달에 4번 가는데, 평균 대기 시간은?&quot;
1번째: 5분
2번째: 3분
3번째: 20분 ← 한 번만 오래 걸림
4번째: 2분
평균: 30분 ÷ 4번 = 7.5분
→ 실제 측정, 여러 번 평균</code></pre><hr>
<h2 id="📊-집계-방법-aggregate-method">📊 집계 방법 (Aggregate Method)</h2>
<h3 id="집계-방법이란">집계 방법이란?</h3>
<p><strong>집계 방법</strong>은 가장 직관적인 상각 분석 방법입니다.</p>
<pre><code>방법:
1. n번 연산의 총 비용 계산
2. n으로 나누기
3. 연산 1번의 상각 비용

공식:
상각 비용 = (총 비용) / (연산 횟수)</code></pre><h3 id="예제-1-동적-배열">예제 1: 동적 배열</h3>
<p><strong>문제 상황</strong>:</p>
<pre><code class="language-python"># 동적 배열 (크기가 자동으로 늘어남)
class DynamicArray:
    &quot;&quot;&quot;   
    일반 배열: 크기 고정
    [1, 2, 3]  ← 3개만 저장 가능

    동적 배열: 크기 자동 증가
    [1, 2, 3]  ← 가득 참
    → 더 큰 배열로 자동 확장!
    [1, 2, 3, _, _, _]  ← 새 배열 (크기 2배)
    &quot;&quot;&quot;

    def __init__(self):
        self.capacity = 1  # 현재 배열 크기
        self.size = 0      # 실제 저장된 원소 개수
        self.array = [None] * self.capacity

    def append(self, item):
        &quot;&quot;&quot;
        원소 추가

        케이스 1: 공간 있음 → 빠름 (O(1))
        케이스 2: 공간 없음 → 느림 (O(n))
                  새 배열 만들고 복사해야 함
        &quot;&quot;&quot;
        # 1. 배열이 가득 찼는지 확인
        if self.size == self.capacity:
            # 가득 참! 크기를 2배로 확장
            self._resize()

        # 2. 원소 추가 (빠름)
        self.array[self.size] = item
        self.size += 1

    def _resize(self):
        &quot;&quot;&quot;
        배열 크기를 2배로 확장

        과정:
        1. 2배 큰 새 배열 만들기
        2. 기존 원소들을 새 배열로 복사
        3. 기존 배열 버리기

        시간: O(n) - n개 원소 복사
        &quot;&quot;&quot;
        # 1. 새 크기 = 기존 크기 × 2
        self.capacity *= 2

        # 2. 새 배열 생성
        new_array = [None] * self.capacity

        # 3. 기존 원소들 복사 (O(n) 시간)
        for i in range(self.size):
            new_array[i] = self.array[i]

        # 4. 새 배열로 교체
        self.array = new_array

# 사용 예시
arr = DynamicArray()

# 1번째 append
arr.append(1)  # 크기 1 → 2로 확장 (1개 복사)

# 2번째 append  
arr.append(2)  # 공간 있음 (빠름)

# 3번째 append
arr.append(3)  # 크기 2 → 4로 확장 (2개 복사)

# 4번째 append
arr.append(4)  # 공간 있음 (빠름)

# 5번째 append
arr.append(5)  # 크기 4 → 8로 확장 (4개 복사)</code></pre>
<p><strong>상각 분석 (집계 방법)</strong>:</p>
<pre><code>n번 append할 때 총 비용 계산:

예: n = 8번 append

연산    크기 확장?    복사 개수    비용
------------------------------------------------
1       1 → 2        1           1
2       없음         0           1
3       2 → 4        2           1 + 2 = 3
4       없음         0           1
5       4 → 8        4           1 + 4 = 5
6       없음         0           1
7       없음         0           1
8       없음         0           1

총 비용 = 1 + 1 + 3 + 1 + 5 + 1 + 1 + 1 = 14

일반화:
n번 append 시, 확장은 log₂(n)번 발생
복사 총 개수: 1 + 2 + 4 + ... + n/2
            = 2^0 + 2^1 + 2^2 + ... + 2^(log n - 1)
            = n - 1
            &lt; n

추가 연산: n번 (각 append마다 1번)

총 비용 = n (추가) + n (복사) = 2n

상각 비용 = 2n / n = 2 = O(1)

결론: append 1번의 상각 비용은 O(1)</code></pre><p><strong>증명</strong>:</p>
<pre><code class="language-python">def count_operations(n):
    &quot;&quot;&quot;
    n번 append 시 총 연산 횟수 계산

    n: append 횟수

    Returns: 총 연산 횟수 (복사 + 추가)
    &quot;&quot;&quot;
    total_ops = 0  # 총 연산 횟수
    capacity = 1   # 현재 배열 크기

    for i in range(1, n + 1):
        # 1. 추가 연산 (항상 1번)
        total_ops += 1

        # 2. 확장이 필요한가?
        if i &gt; capacity:
            # 확장 필요! 기존 원소들 복사
            total_ops += capacity  # capacity개 복사
            capacity *= 2  # 크기 2배로
            print(f&quot;{i}번째 append: 확장 발생! &quot;
                  f&quot;{capacity//2} → {capacity} &quot;
                  f&quot;(복사: {capacity//2}개)&quot;)

    return total_ops

# 테스트
n = 16
total = count_operations(n)
print(f&quot;\n{n}번 append 총 연산: {total}&quot;)
print(f&quot;평균 (상각): {total/n:.2f}&quot;)

# 출력:
# 1번째 append: 확장 발생! 1 → 2 (복사: 1개)
# 3번째 append: 확장 발생! 2 → 4 (복사: 2개)
# 5번째 append: 확장 발생! 4 → 8 (복사: 4개)
# 9번째 append: 확장 발생! 8 → 16 (복사: 8개)
# 
# 16번 append 총 연산: 31
# 평균 (상각): 1.94  ← 거의 2 (O(1))</code></pre>
<h3 id="예제-2-스택의-다중-pop">예제 2: 스택의 다중 Pop</h3>
<p><strong>문제 상황</strong>:</p>
<pre><code class="language-python">class Stack:
    &quot;&quot;&quot;
    스택: 후입선출(LIFO) 자료구조

    연산:
    - push(x): 원소 추가 - O(1)
    - pop(): 원소 제거 - O(1)
    - multipop(k): k개 제거 - O(k) ← 분석 대상
    &quot;&quot;&quot;

    def __init__(self):
        self.items = []  # 내부적으로 리스트 사용

    def push(self, item):
        &quot;&quot;&quot;
        원소 추가

        시간: O(1) - 리스트 끝에 추가
        &quot;&quot;&quot;
        self.items.append(item)

    def pop(self):
        &quot;&quot;&quot;
        원소 제거 및 반환

        시간: O(1) - 리스트 끝에서 제거

        Returns: 제거된 원소 (스택이 비어 있으면 None)
        &quot;&quot;&quot;
        if not self.is_empty():
            return self.items.pop()
        return None

    def multipop(self, k):
        &quot;&quot;&quot;
        k개 원소를 한 번에 제거

        k: 제거할 원소 개수

        시간: O(min(k, n))
              k개 제거하려 해도 n개 밖에 없으면 n개 만
        &quot;&quot;&quot;
        result = []

        # k번 반복하거나 스택이 빌 때까지
        for _ in range(k):
            if self.is_empty():
                break  # 스택이 비었으면 중단
            result.append(self.pop())

        return result

    def is_empty(self):
        &quot;&quot;&quot;스택이 비어 있는지 확인&quot;&quot;&quot;
        return len(self.items) == 0

# 사용 예시
stack = Stack()

# push 5번
stack.push(1)
stack.push(2)
stack.push(3)
stack.push(4)
stack.push(5)
# 스택: [1, 2, 3, 4, 5]

# multipop(3) - 3개 제거
removed = stack.multipop(3)
print(f&quot;제거된 원소: {removed}&quot;)  # [5, 4, 3]
# 스택: [1, 2]

# multipop(10) - 10개 제거하려 했지만 2개만 있음
removed = stack.multipop(10)
print(f&quot;제거된 원소: {removed}&quot;)  # [2, 1]
# 스택: []</code></pre>
<p><strong>상각 분석</strong>:</p>
<pre><code>n번의 연산 (push, pop, multipop 혼합)

질문:
multipop(k)은 O(k)인데,
n번 연산하면 O(nk)?

분석:
핵심 통찰: &quot;원소는 push로만 추가되고, 한 번 제거되면 다시 제거할 수 없다&quot;

n번 연산 중:
- push: 최대 n번 → n개 원소 추가
- pop/multipop: 최대 n개만 제거 가능
  (추가된 것만 제거 가능하므로)

총 비용:
- push: n번 × O(1) = O(n)
- pop: 총 n개 제거 × O(1) = O(n)

총 = O(n) + O(n) = O(2n) = O(n)

상각 비용 = O(n) / n = O(1)

각 연산의 상각 비용: O(1)!</code></pre><p><strong>예시로 확인</strong>:</p>
<pre><code class="language-python">def simulate_stack_operations(operations):
    &quot;&quot;&quot;
    스택 연산 시뮬레이션 및 비용 계산

    operations: 연산 리스트   예: [(&#39;push&#39;, 1), (&#39;multipop&#39;, 3), ...]

    Returns: 총 연산 비용
    &quot;&quot;&quot;
    stack = Stack()
    total_cost = 0  # 총 비용

    for op_type, value in operations:
        if op_type == &#39;push&#39;:
            # push: 비용 1
            stack.push(value)
            total_cost += 1
            print(f&quot;push({value}): 비용 +1, 총 {total_cost}&quot;)

        elif op_type == &#39;multipop&#39;:
            # multipop(k): 비용 = 실제 제거된 개수
            before_size = len(stack.items)
            stack.multipop(value)
            after_size = len(stack.items)

            removed_count = before_size - after_size
            total_cost += removed_count
            print(f&quot;multipop({value}): &quot;
                  f&quot;{removed_count}개 제거, &quot;
                  f&quot;비용 +{removed_count}, &quot;
                  f&quot;총 {total_cost}&quot;)

    return total_cost

# 테스트
operations = [
    (&#39;push&#39;, 1),
    (&#39;push&#39;, 2),
    (&#39;push&#39;, 3),
    (&#39;push&#39;, 4),
    (&#39;push&#39;, 5),      # 5번 push: 비용 5
    (&#39;multipop&#39;, 3),  # 3개 제거: 비용 3
    (&#39;push&#39;, 6),
    (&#39;push&#39;, 7),      # 2번 push: 비용 2
    (&#39;multipop&#39;, 10), # 4개만 있음: 비용 4
]

total = simulate_stack_operations(operations)
n = len(operations)
print(f&quot;\n총 {n}번 연산, 총 비용: {total}&quot;)
print(f&quot;상각 비용: {total/n:.2f}&quot;)

# 출력:
# push(1): 비용 +1, 총 1
# push(2): 비용 +1, 총 2
# push(3): 비용 +1, 총 3
# push(4): 비용 +1, 총 4
# push(5): 비용 +1, 총 5
# multipop(3): 3개 제거, 비용 +3, 총 8
# push(6): 비용 +1, 총 9
# push(7): 비용 +1, 총 10
# multipop(10): 4개만 있음, 비용 +4, 총 14
#
# 총 9번 연산, 총 비용: 14
# 상각 비용: 1.56  ← 거의 1.5 (O(1))</code></pre>
<hr>
<h2 id="💰-회계-방법-accounting-method">💰 회계 방법 (Accounting Method)</h2>
<h3 id="회계-방법이란">회계 방법이란?</h3>
<p><strong>회계 방법</strong>은 각 연산에 &quot;크레딧(신용)&quot;을 할당하는 방법입니다.</p>
<pre><code>비유: 은행 계좌

저렴한 연산:
- 실제 비용: 1원
- 청구 비용: 3원
- 차액 2원 → 저축!

비싼 연산:
- 실제 비용: 100원
- 청구 비용: 3원
- 부족분 97원 → 저축한 돈으로 지불!

핵심:
&quot;저축한 돈(크레딧)이 항상 0 이상이면 청구 비용이 상각 비용!&quot;</code></pre><p><strong>원리</strong>:</p>
<pre><code>각 연산에 상각 비용 할당:
- 실제 비용보다 많이 청구
- 여분의 크레딧 저축
- 비싼 연산 시 크레딧 사용

조건:
크레딧 ≥ 0 (항상!)
→ 상각 비용으로 모든 실제 비용 충당 가능</code></pre><h3 id="예제-동적-배열-회계-방법">예제: 동적 배열 (회계 방법)</h3>
<pre><code class="language-python">class DynamicArrayWithCredit:
    &quot;&quot;&quot;
    동적 배열 - 크레딧 개념으로 분석

    비용 모델:
    - 원소 추가: 1 크레딧
    - 원소 복사: 1 크레딧

    상각 비용 (청구 비용):
    - append 한 번: 3 크레딧

    크레딧 사용:
    - 추가 시: 1 사용
    - 나머지 2: 저축 (미래의 복사에 사용)
    &quot;&quot;&quot;

    def __init__(self):
        self.capacity = 1
        self.size = 0
        self.array = [None] * self.capacity
        self.total_credit = 0  # 저축된 크레딧 (설명용)

    def append(self, item):
        &quot;&quot;&quot;
        원소 추가 (상각 비용: 3)

        크레딧 사용:
        1. 추가 작업: 1 크레딧
        2. 저축: 2 크레딧 (미래의 복사용)
        &quot;&quot;&quot;
        print(f&quot;\n--- append({item}) ---&quot;)
        print(f&quot;청구: 3 크레딧&quot;)

        # 1. 추가 작업에 1 크레딧 사용
        print(f&quot;사용: 1 크레딧 (추가 작업)&quot;)
        actual_cost = 1

        # 2. 확장이 필요한가?
        if self.size == self.capacity:
            print(f&quot;확장 필요! {self.capacity} → {self.capacity * 2}&quot;)

            # 확장 시 복사 비용: size개 복사
            copy_cost = self.size
            print(f&quot;복사 비용: {copy_cost} 크레딧&quot;)
            print(f&quot;저축된 크레딧으로 지불: {copy_cost} 크레딧&quot;)

            # 실제로 저축된 크레딧 사용
            self.total_credit -= copy_cost
            actual_cost += copy_cost

            self._resize()

        # 3. 원소 추가
        self.array[self.size] = item
        self.size += 1

        # 4. 나머지 크레딧 저축
        # 청구 3 - 사용 actual_cost = 저축
        saved = 3 - actual_cost
        self.total_credit += saved
        print(f&quot;저축: {saved} 크레딧&quot;)
        print(f&quot;현재 저축 총액: {self.total_credit} 크레딧&quot;)

        # 검증: 크레딧은 항상 0 이상!
        assert self.total_credit &gt;= 0, &quot;크레딧 부족!&quot;

    def _resize(self):
        &quot;&quot;&quot;배열 크기 2배로 확장&quot;&quot;&quot;
        self.capacity *= 2
        new_array = [None] * self.capacity
        for i in range(self.size):
            new_array[i] = self.array[i]
        self.array = new_array

# 시뮬레이션
arr = DynamicArrayWithCredit()

# 8번 append 실행
for i in range(1, 9):
    arr.append(i)

# 출력 예시:
# --- append(1) ---
# 청구: 3 크레딧
# 사용: 1 크레딧 (추가 작업)
# 확장 필요! 1 → 2
# 복사 비용: 1 크레딧
# 저축된 크레딧으로 지불: 1 크레딧
# 저축: 1 크레딧  (3 - 1 - 1 = 1)
# 현재 저축 총액: 1 크레딧
#
# --- append(2) ---
# 청구: 3 크레딧
# 사용: 1 크레딧 (추가 작업)
# 저축: 2 크레딧
# 현재 저축 총액: 3 크레딧
#
# --- append(3) ---
# 청구: 3 크레딧
# 사용: 1 크레딧 (추가 작업)
# 확장 필요! 2 → 4
# 복사 비용: 2 크레딧
# 저축된 크레딧으로 지불: 2 크레딧
# 저축: 0 크레딧  (3 - 1 - 2 = 0)
# 현재 저축 총액: 1 크레딧
# ...

# 결론: 크레딧이 항상 0 이상!
# → 상각 비용 3 크레딧 = O(1) 성공!</code></pre>
<p><strong>왜 3 크레딧인가?</strong></p>
<pre><code>분석:

append 시:
1. 추가 작업: 1 크레딧 (항상)
2. 저축: 2 크레딧

저축된 2 크레딧의 용도:
- 이 원소를 복사할 때: 1 크레딧
- 이전 원소 하나를 복사할 때: 1 크레딧

확장 시 복사되는 원소들:
- 각 원소는 추가될 때 저축한 1 크레딧
- 이전 원소들도 각자 저축한 1 크레딧
→ 복사 비용 완전히 충당!

수학적:
n개 원소가 있을 때 확장하면
- 복사 비용: n 크레딧
- 저축: n개 원소 × 1 크레딧 = n 크레딧
→ 정확히 일치!</code></pre><hr>
<h2 id="⚡-포텐셜-방법-potential-method">⚡ 포텐셜 방법 (Potential Method)</h2>
<h3 id="포텐셜-방법이란">포텐셜 방법이란?</h3>
<p><strong>포텐셜 방법</strong>은 가장 수학적인 방법으로, &quot;잠재 에너지&quot; 개념을 사용합니다.</p>
<pre><code>비유: 물리학의 위치 에너지

공을 높이 들어올림:
- 에너지를 저장 (포텐셜 증가)
- 나중에 떨어뜨리면 에너지 방출

알고리즘:
- 저렴한 연산: 포텐셜 증가 (에너지 저장)
- 비싼 연산: 포텐셜 감소 (에너지 사용)</code></pre><p><strong>정의</strong>:</p>
<pre><code>Φ(D): 자료구조 D의 포텐셜 함수

조건:
1. Φ(D₀) = 0 (초기 상태)
2. Φ(Dᵢ) ≥ 0 (항상 음수 아님)

상각 비용:
ĉᵢ = cᵢ + Φ(Dᵢ) - Φ(Dᵢ₋₁)

여기서:
- cᵢ: i번째 연산의 실제 비용
- Φ(Dᵢ) - Φ(Dᵢ₋₁): 포텐셜 변화</code></pre><h3 id="예제-동적-배열-포텐셜-방법">예제: 동적 배열 (포텐셜 방법)</h3>
<p><strong>포텐셜 함수 정의</strong>:</p>
<pre><code>Φ(D) = 2 × size - capacity

직관:
- size가 capacity에 가까우면 → Φ 증가 (확장이 임박, 에너지 저장)
- 확장 직후 → Φ 감소 (에너지 방출)</code></pre><p><strong>분석</strong>:</p>
<pre><code class="language-python">class DynamicArrayWithPotential:
    &quot;&quot;&quot;
    동적 배열 - 포텐셜 방법으로 분석

    포텐셜 함수:
    Φ(D) = 2 × size - capacity

    초기: size=0, capacity=1
    → Φ = 2×0 - 1 = -1 (음수!)

    수정: Φ(D) = 2 × size - capacity + 1
    → 초기 Φ = 2×0 - 1 + 1 = 0 ✓
    &quot;&quot;&quot;

    def __init__(self):
        self.capacity = 1
        self.size = 0
        self.array = [None] * self.capacity

    def potential(self):
        &quot;&quot;&quot;
        현재 포텐셜 계산

        Returns: 2 × size - capacity + 1
        &quot;&quot;&quot;
        return 2 * self.size - self.capacity + 1

    def append_with_analysis(self, item):
        &quot;&quot;&quot;
        append 연산의 상각 비용 계산

        상각 비용 = 실제 비용 + 포텐셜 변화
        &quot;&quot;&quot;
        print(f&quot;\n--- append({item}) ---&quot;)

        # 1. 현재 포텐셜
        phi_before = self.potential()
        print(f&quot;이전 포텐셜: {phi_before}&quot;)
        print(f&quot;  (size={self.size}, capacity={self.capacity})&quot;)

        # 2. 실제 비용 계산
        actual_cost = 1  # 추가 작업

        if self.size == self.capacity:
            # 확장 필요
            print(f&quot;확장 발생!&quot;)
            actual_cost += self.size  # 복사 비용
            self._resize()

        # 3. 원소 추가
        self.array[self.size] = item
        self.size += 1

        # 4. 새 포텐셜
        phi_after = self.potential()
        print(f&quot;이후 포텐셜: {phi_after}&quot;)
        print(f&quot;  (size={self.size}, capacity={self.capacity})&quot;)

        # 5. 상각 비용
        phi_change = phi_after - phi_before
        amortized_cost = actual_cost + phi_change

        print(f&quot;실제 비용: {actual_cost}&quot;)
        print(f&quot;포텐셜 변화: {phi_change}&quot;)
        print(f&quot;상각 비용: {amortized_cost}&quot;)

        return amortized_cost

    def _resize(self):
        &quot;&quot;&quot;배열 크기 2배로 확장&quot;&quot;&quot;
        self.capacity *= 2
        new_array = [None] * self.capacity
        for i in range(self.size):
            new_array[i] = self.array[i]
        self.array = new_array

# 시뮬레이션
arr = DynamicArrayWithPotential()
total_amortized = 0

for i in range(1, 9):
    cost = arr.append_with_analysis(i)
    total_amortized += cost

print(f&quot;\n총 상각 비용: {total_amortized}&quot;)
print(f&quot;평균: {total_amortized / 8:.2f}&quot;)

# 출력 예시:
# --- append(1) ---
# 이전 포텐셜: 0
#   (size=0, capacity=1)
# 확장 발생!
# 이후 포텐셜: 2
#   (size=1, capacity=2)
# 실제 비용: 1  (확장 시 복사할 원소 없음)
# 포텐셜 변화: 2
# 상각 비용: 3
#
# --- append(2) ---
# 이전 포텐셜: 2
#   (size=1, capacity=2)
# 이후 포텐셜: 3
#   (size=2, capacity=2)
# 실제 비용: 1
# 포텐셜 변화: 1
# 상각 비용: 2
#
# --- append(3) ---
# 이전 포텐셜: 3
#   (size=2, capacity=2)
# 확장 발생!
# 이후 포텐셜: 2
#   (size=3, capacity=4)
# 실제 비용: 3  (1 추가 + 2 복사)
# 포텐셜 변화: -1
# 상각 비용: 2
# ...</code></pre>
<p><strong>수학적 분석</strong>:</p>
<pre><code>케이스 1: 확장 없음 (size &lt; capacity)

실제 비용: c = 1
포텐셜 변화: Φ(Dᵢ) - Φ(Dᵢ₋₁)
           = (2×size - capacity + 1) - (2×(size-1) - capacity + 1)
           = 2×size - 2×size + 2
           = 2

상각 비용: ĉ = 1 + 2 = 3


케이스 2: 확장 발생 (size = capacity)

실제 비용: c = 1 + size (추가 + 복사)

포텐셜 변화:
  이전: Φ(Dᵢ₋₁) = 2×size - size + 1 = size + 1
  이후: Φ(Dᵢ) = 2×(size+1) - 2×size + 1
              = 2×size + 2 - 2×size + 1
              = 3

  변화: 3 - (size + 1) = 2 - size

상각 비용: ĉ = (1 + size) + (2 - size)
            = 3

결론: 모든 경우 상각 비용 = 3 = O(1)!</code></pre><hr>
<h2 id="📊-세-가지-방법-비교">📊 세 가지 방법 비교</h2>
<h3 id="비교-표">비교 표</h3>
<pre><code>방법          직관성    수학적 엄밀성    사용 난이도
----------------------------------------------------------
집계 방법     높음      중간            쉬움
회계 방법     중간      중간            중간
포텐셜 방법   낮음      높음            어려움

추천:
- 입문: 집계 방법
- 실무: 회계 방법
- 연구: 포텐셜 방법</code></pre><h3 id="동일한-문제-다른-접근">동일한 문제, 다른 접근</h3>
<pre><code class="language-python"># 동적 배열의 상각 비용: 모두 O(1)

# 1. 집계 방법
# &quot;n번 연산의 총 비용을 세자&quot;
# 총 비용: 2n
# 평균: 2n/n = 2 = O(1)

# 2. 회계 방법
# &quot;각 연산에 3 크레딧을 청구하자&quot;
# 1 크레딧: 추가
# 2 크레딧: 저축 (복사용)
# 항상 충분! → O(1)

# 3. 포텐셜 방법
# &quot;Φ(D) = 2×size - capacity + 1&quot;
# 실제 비용 + 포텐셜 변화 = 3
# → O(1)

# 결론: 모두 같은 답! O(1)</code></pre>
<hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<p><strong>언제 상각 분석을 사용하는가?</strong></p>
<pre><code class="language-python"># 사용해야 할 때:
# 1. 가끔 비싼 연산이 있는 경우
#    예: 동적 배열 append

# 2. 연속된 연산을 분석할 때
#    예: n번 연산의 총 비용

# 3. 최악의 경우가 너무 비관적일 때
#    예: multipop은 O(k)지만 상각 O(1)


# 사용하지 않아야 할 때:
# 1. 단일 연산만 볼 때
#    예: 한 번만 append → O(n) 가능

# 2. 최악의 경우가 중요할 때
#    예: 실시간 시스템 (지연 시간 보장)

# 3. 독립적인 연산들
#    예: 서로 영향 없는 연산</code></pre>
<p><strong>Python 내장 자료구조</strong></p>
<pre><code class="language-python"># Python의 list
# - append: 상각 O(1)
# - insert(0, x): O(n) (상각 아님!)
arr = []
arr.append(1)  # 빠름 (상각 O(1))
arr.insert(0, 1)  # 느림 (O(n))

# collections.deque (양방향 큐)
# - append: O(1)
# - appendleft: O(1) (양쪽 모두 빠름!)
from collections import deque
dq = deque()
dq.append(1)  # 빠름
dq.appendleft(1)  # 빠름!

# dict
# - 삽입: 상각 O(1)
# - 조회: 평균 O(1), 최악 O(n)
d = {}
d[&#39;key&#39;] = &#39;value&#39;  # 상각 O(1)</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>상각 분석의 본질</strong></p>
<pre><code>목적:
- 여러 연산의 평균 비용 계산
- 가끔 발생하는 비싼 연산 포함

vs 평균 경우:
- 평균: 확률 분포 가정
- 상각: 확률 무관, 연속 연산</code></pre><p><strong>세 가지 방법</strong></p>
<pre><code>1. 집계 방법 (Aggregate):
   총 비용 / 연산 횟수
   → 가장 직관적

2. 회계 방법 (Accounting):
   크레딧으로 비용 관리
   → 실무적

3. 포텐셜 방법 (Potential):
   수학적 함수로 분석
   → 가장 엄밀</code></pre><p><strong>대표 예시</strong></p>
<pre><code>동적 배열 append:
- 최악: O(n) (재할당)
- 상각: O(1)
- 이유: 재할당이 드물게 발생

스택 multipop:
- 최악: O(k)
- 상각: O(1)
- 이유: 추가된 것만 제거 가능</code></pre><p><strong>실무 적용</strong></p>
<pre><code>Python list:
- append: 상각 O(1) ✓
- insert(0): O(n) (상각 아님) ✗

실시간 시스템:
- 상각 분석 신중히
- 최악의 경우도 중요!</code></pre><p><strong>알고리즘 분석 섹션 완료!</strong></p>
<p>지금까지 배운 내용:</p>
<ul>
<li>시간 복잡도: 알고리즘이 얼마나 빠른가</li>
<li>공간 복잡도: 메모리를 얼마나 사용하는가</li>
<li>점근적 표기법: Big-O, Big-Ω, Big-Θ의 수학적 의미</li>
<li>최선/평균/최악 분석: 입력에 따른 성능 차이</li>
<li>상수 계수와 실제 성능: 이론과 실제의 간극</li>
<li>상각 분석: 여러 연산의 평균 비용</li>
</ul>
<hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>다음 섹션: [08] 계산 복잡도 이론</strong></p>
<p>이제 알고리즘이 아닌 <strong>문제 자체</strong>의 난이도를 분석합니다.</p>
<p><strong>[08-01] 결정 문제 (Decision Problems)</strong></p>
<ul>
<li>결정 문제의 정의: 답이 Yes 또는 No인 문제로 복잡도 이론의 기본 단위</li>
<li>최적화 문제 변환: &quot;최단 경로는?&quot;을 &quot;k 이하 경로가 있나?&quot;로 바꾸는 방법</li>
<li>형식 언어 표현: 문제를 수학적으로 엄밀하게 정의하는 집합론적 접근</li>
<li>이진 탐색 활용: 결정 문제를 반복해서 풀어 최적값 찾기</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[07-05] 상수 계수와 실제 성능</a><br><strong>다음 글</strong>: <a href="#">[08-01] 결정 문제</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [07-05] 상수 계수와 실제 성능]]></title>
            <link>https://velog.io/@road_to_ai/07-05-%EC%83%81%EC%88%98-%EA%B3%84%EC%88%98%EC%99%80-%EC%8B%A4%EC%A0%9C-%EC%84%B1%EB%8A%A5</link>
            <guid>https://velog.io/@road_to_ai/07-05-%EC%83%81%EC%88%98-%EA%B3%84%EC%88%98%EC%99%80-%EC%8B%A4%EC%A0%9C-%EC%84%B1%EB%8A%A5</guid>
            <pubDate>Fri, 06 Mar 2026 09:13:10 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Big-O 표기법은 점근적 성능을 나타내지만, 실제 성능은 상수 계수, 캐시, 하드웨어 특성 등 여러 요인에 영향을 받습니다.</p>
</blockquote>
<hr>
<h2 id="🎯-이론과-실제의-차이">🎯 이론과 실제의 차이</h2>
<h3 id="big-o의-한계">Big-O의 한계</h3>
<p><strong>Big-O는 중요하지만 전부가 아닙니다.</strong></p>
<p><strong>실생활 비유</strong>:</p>
<pre><code>두 개의 자동차:

자동차 A:
- 최고 속도: 200km/h (이론)
- 실제 평균: 60km/h (시내 주행)

자동차 B:
- 최고 속도: 150km/h (이론)
- 실제 평균: 80km/h (고속도로)

이론상 A가 빠르지만
실제로는 B가 더 빠를 수 있음!

→ 사용 환경이 중요!</code></pre><p><strong>알고리즘 예시</strong>:</p>
<pre><code class="language-python"># 알고리즘 A: O(n)
def algorithm_a(arr):
    result = 0
    for i in range(len(arr)):
        result += arr[i] * 1000  # 상수 계수 큼
        result += compute_heavy(arr[i])
        result += another_operation(arr[i])
    return result

# 알고리즘 B: O(n log n)
def algorithm_b(arr):
    return efficient_sort(arr)  # 최적화된 구현

# n = 1000일 때
# A: 1000번 × (무거운 연산 3개) ≈ 3초
# B: 1000 × log(1000) ≈ 10000번 × (가벼운 연산) ≈ 0.01초

# 이론: A가 빠름 (O(n) &lt; O(n log n))
# 실제: B가 훨씬 빠름!</code></pre>
<h3 id="왜-이론과-실제가-다른가">왜 이론과 실제가 다른가?</h3>
<p><strong>Big-O가 무시하는 것들</strong>:</p>
<pre><code>1. 상수 계수:
   O(n) vs O(100n) → 둘 다 O(n)
   실제로는 100배 차이!

2. 낮은 차수 항:
   O(n² + 1000n) → O(n²)
   n이 작으면 1000n이 중요!

3. 하드웨어 특성:
   - 캐시 효율
   - 메모리 접근 패턴
   - CPU 파이프라인

4. 구현 품질:
   - 최적화 수준
   - 언어 특성
   - 라이브러리 효율

5. 입력 크기:
   n이 작으면 점근적 성능 의미 없음</code></pre><hr>
<h2 id="📏-상수-계수의-영향">📏 상수 계수의 영향</h2>
<h3 id="상수-계수란">상수 계수란?</h3>
<p><strong>상수 계수(Constant Factor)</strong>는 Big-O에서 무시되는 상수 배수입니다.</p>
<pre><code>실제 시간 = c × f(n) + 낮은 차수 항

예:
T(n) = 5n² + 3n + 100

Big-O: O(n²)
하지만:
- 계수 5는 중요!
- 3n도 작은 n에서 중요!
- 100도 매우 작은 n에서 중요!</code></pre><h3 id="상수-계수-비교">상수 계수 비교</h3>
<p><strong>예시 1: 선형 탐색 vs 이진 탐색</strong></p>
<pre><code class="language-python">import time

# 선형 탐색: O(n), 상수 작음
def linear_search(arr, target):
    &quot;&quot;&quot;
    실제 시간: 1 × n
    간단한 연산
    &quot;&quot;&quot;
    for i in range(len(arr)):
        if arr[i] == target:  # 1번 비교
            return i
    return -1

# 이진 탐색: O(log n), 상수 큼
def binary_search(arr, target):
    &quot;&quot;&quot;
    실제 시간: 10 × log n
    복잡한 연산 (인덱스 계산, 비교)
    &quot;&quot;&quot;
    left, right = 0, len(arr) - 1

    while left &lt;= right:
        mid = (left + right) // 2  # 나눗셈

        if arr[mid] == target:
            return mid
        elif arr[mid] &lt; target:
            left = mid + 1
        else:
            right = mid - 1

    return -1

# 벤치마크
arr = list(range(100))  # n = 100
target = 99

# 선형 탐색: 100번 비교
# 이진 탐색: log₂(100) ≈ 7번, 하지만 각 비교가 복잡

# n이 작으면 선형이 더 빠를 수 있음!
# 교차점: n ≈ 50~100</code></pre>
<p><strong>실제 측정</strong>:</p>
<pre><code class="language-python">import time
import random

def benchmark():
    sizes = [10, 50, 100, 500, 1000, 5000]

    for n in sizes:
        arr = sorted(random.sample(range(n*10), n))
        target = arr[-1]  # 최악의 경우

        # 선형 탐색 측정
        start = time.perf_counter()
        for _ in range(10000):
            linear_search(arr, target)
        linear_time = time.perf_counter() - start

        # 이진 탐색 측정
        start = time.perf_counter()
        for _ in range(10000):
            binary_search(arr, target)
        binary_time = time.perf_counter() - start

        print(f&quot;n={n:5d}: 선형={linear_time:.4f}s, 이진={binary_time:.4f}s&quot;)

benchmark()

# 예상 출력:
# n=   10: 선형=0.0012s, 이진=0.0018s  ← 선형이 빠름!
# n=   50: 선형=0.0058s, 이진=0.0045s  ← 비슷
# n=  100: 선형=0.0115s, 이진=0.0048s  ← 이진이 빠름
# n=  500: 선형=0.0580s, 이진=0.0051s  ← 이진이 훨씬 빠름
# n= 1000: 선형=0.1160s, 이진=0.0054s
# n= 5000: 선형=0.5800s, 이진=0.0059s</code></pre>
<p><strong>예시 2: 삽입 정렬 vs 병합 정렬</strong></p>
<pre><code class="language-python">def insertion_sort(arr):
    &quot;&quot;&quot;
    시간: O(n²)
    실제: c₁ × n²  (c₁ 작음)

    장점:
    - 간단한 연산
    - 캐시 친화적
    - 오버헤드 작음
    &quot;&quot;&quot;
    for i in range(1, len(arr)):
        key = arr[i]
        j = i - 1
        while j &gt;= 0 and arr[j] &gt; key:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key

def merge_sort(arr):
    &quot;&quot;&quot;
    시간: O(n log n)
    실제: c₂ × n log n  (c₂큼)

    단점:
    - 재귀 오버헤드
    - 추가 메모리 할당
    - 병합 연산 복잡
    &quot;&quot;&quot;
    if len(arr) &lt;= 1:
        return arr

    mid = len(arr) // 2
    left = merge_sort(arr[:mid])  # 배열 복사 오버헤드
    right = merge_sort(arr[mid:])

    return merge(left, right)  # 병합 오버헤드

# 교차점 계산:
# c₁ × n² = c₂ × n log n
# n = c₂/c₁ × log n

# 실제 측정 결과:
# c₁ ≈ 0.1, c₂ ≈ 2
# 교차점: n ≈ 40~60

# n &lt; 50: 삽입 정렬이 빠름
# n &gt; 50: 병합 정렬이 빠름

# Python의 Timsort:
# → 작은 부분은 삽입 정렬 사용!</code></pre>
<hr>
<h2 id="💾-캐시와-메모리-지역성-locality">💾 캐시와 메모리 지역성 (Locality)</h2>
<h3 id="메모리-계층-구조">메모리 계층 구조</h3>
<pre><code>속도                용량              비용
빠름 ↑                              비쌈 ↑
     레지스터         수십 바이트      매우 비쌈
     L1 캐시         32-64KB         비쌈
     L2 캐시         256KB-512KB     비쌈
     L3 캐시         8-32MB          중간
     RAM            8-64GB           저렴
     SSD            256GB-2TB        저렴
     HDD            1-10TB           매우 저렴
느림 ↓               많음 ↓          싸짐 ↓

접근 시간:
레지스터: 1 사이클
L1 캐시: 3-4 사이클
L2 캐시: 10-20 사이클
L3 캐시: 40-75 사이클
RAM: 200-300 사이클
SSD: 100,000 사이클
HDD: 10,000,000 사이클

→ 캐시 히트/미스가 성능에 큰 영향!
  1. 캐시 히트 (Cache Hit) : CPU가 참조하고자 하는 데이터가 캐시 메모리에 이미 존재할 때
  2. 캐시 미스 (Cache Miss) : CPU가 찾는 데이터가 캐시 메모리에 없어서, RAM이나 SSD/HDD까지 가서 데이터를 가져와야 하는 상황

&lt;참고: 사이클&gt;
1. 물리적 의미: 전기적 &#39;박동&#39; (The Pulse)
   CPU 내부에는 수정 진동자(Quartz Crystal)가 일정하게 전기 신호를 보냄. 
   이 신호가 0(Low)에서 1(High)로 올라갔다가 다시 0으로 내려오는 한 번의 파동이 바로 1 사이클임.
   이 박동이 없으면 CPU 내부의 데이터는 이동하거나 변할 수 없음. 즉, 모든 동작의 &#39;메트로놈&#39; 역할
2. 행동 단위로서의 의미: 명령어의 &#39;한 걸음&#39;
   CPU가 명령어를 처리하려면 보통 4단계의 과정을 거침(가져오기(Fetch) → 해석(Decode) → 실행(Execute) → 저장(Writeback))
   각 단계는 최소 1 사이클이 필요. 1 사이클 동안 &quot;레지스터에 있는 숫자 A를 가져와라&quot;라는 아주 작은 단위의 행동 하나를 수행
3. 접근 시간(Latency)에서의 의미: &quot;CPU가 데이터를 기다리며 허비하는 박동 수&quot;
   L1 캐시(4 사이클)은 CPU가 &quot;데이터 줘!&quot;라고 외친 뒤, 심장이 4번 뛸 동안 아무것도 못 하고 기다려야 데이터가 도착한다는 의미
* 현대 CPU는 이 기다리는 시간이 아까워서, 데이터를 기다리는 동안(사이클이 낭비되는 동안) 
   순서와 상관없이 할 수 있는 다른 일부터 먼저 처리하는 &#39;비순차적 실행(Out-of-Order Execution)&#39; 기술을 사용</code></pre><h3 id="지역성의-원리">지역성의 원리</h3>
<p><strong>지역성(Locality)이란?</strong></p>
<p><strong>지역성</strong>은 프로그램이 특정 시간에 메모리의 특정 부분만 집중적으로 접근하는 경향을 말합니다.</p>
<pre><code>프로그램의 메모리 접근 패턴:

무작위 접근 (지역성 없음):
메모리: [A] [B] [C] [D] [E] [F] [G] [H]
접근:    ↑       ↑   ↑       ↑   ↑
        A → D → B → F → C
→ 메모리 전체를 골고루 접근

지역적 접근 (지역성 있음):
메모리: [A] [B] [C] [D] [E] [F] [G] [H]
접근:    ↑   ↑   ↑
        A → B → C → B → A
→ 특정 영역만 집중 접근</code></pre><p><strong>왜 지역성이 중요한가?</strong></p>
<pre><code>캐시의 작동 원리:
1. 자주 쓰는 데이터를 빠른 메모리(캐시)에 복사
2. 다음 접근 시 캐시에서 가져옴 (빠름!)
3. 지역성이 높으면 → 캐시 히트 많음 → 빠름
4. 지역성이 낮으면 → 캐시 미스 많음 → 느림

캐시 히트: 0.5ns (매우 빠름)
캐시 미스: 100ns (200배 느림!)</code></pre><p><strong>지역성의 두 가지 종류</strong>:</p>
<p>프로그램의 지역성은 크게 <strong>시간적 지역성</strong>과 <strong>공간적 지역성</strong>으로 나뉩니다.</p>
<p><strong>1. 시간적 지역성(Temporal Locality)</strong></p>
<p><strong>정의</strong>: 최근에 접근한 데이터를 가까운 미래에 다시 접근하는 경향</p>
<pre><code>예:
for i in range(n):
    sum += arr[i]  # arr[i]는 한 번만 접근

for i in range(n):
    for j in range(n):
        sum += matrix[i]  # matrix[i]를 n번 재사용!
                          # 캐시에 유리</code></pre><p><strong>2. 공간적 지역성(Spatial Locality)</strong></p>
<p><strong>정의</strong>: 접근한 데이터 근처의 데이터를 곧 접근하는 경향</p>
<pre><code>예:
# 좋은 예: 순차 접근
for i in range(n):
    sum += arr[i]  # 연속된 메모리 접근
                   # 캐시 라인 활용

# 나쁜 예: 임의 접근
for i in random_indices:
    sum += arr[i]  # 불연속 접근
                   # 캐시 미스 많음</code></pre><h3 id="캐시-친화적-코드">캐시 친화적 코드</h3>
<p><strong>예시 1: 행렬 순회</strong></p>
<pre><code class="language-python">import numpy as np
import time

# 나쁜 예: 열 우선 순회 (Python)
def column_major(matrix):
    &quot;&quot;&quot;
    캐시 비친화적

    Python/C는 행 우선 저장 → 열 순회는 캐시 미스 많음
    &quot;&quot;&quot;
    n = len(matrix)
    total = 0

    for col in range(n):
        for row in range(n):
            total += matrix[row][col]  # 불연속 접근!

    return total

# 좋은 예: 행 우선 순회
def row_major(matrix):
    &quot;&quot;&quot;
    캐시 친화적

    연속된 메모리 접근 → 캐시 히트 많음
    &quot;&quot;&quot;
    n = len(matrix)
    total = 0

    for row in range(n):
        for col in range(n):
            total += matrix[row][col]  # 연속 접근!

    return total

# 벤치마크
n = 5000
matrix = [[i + j for j in range(n)] for i in range(n)]

start = time.time()
column_major(matrix)
col_time = time.time() - start

start = time.time()
row_major(matrix)
row_time = time.time() - start

print(f&quot;열 우선: {col_time:.4f}s&quot;)
print(f&quot;행 우선: {row_time:.4f}s&quot;)
print(f&quot;차이: {col_time / row_time:.2f}배&quot;)

# 예상 출력:
# 열 우선: 3.2456s
# 행 우선: 1.8234s
# 차이: 1.78배  ← 같은 O(n²)인데 78% 느림!</code></pre>
<p><strong>예시 2: 데이터 구조 선택</strong></p>
<pre><code class="language-python"># 배열 vs 연결 리스트
class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

# 연결 리스트: 캐시 비친화적
def sum_linked_list(head):
    &quot;&quot;&quot;
    노드들이 메모리에 흩어져 있음 → 캐시 미스 많음
    &quot;&quot;&quot;
    total = 0
    current = head
    while current:
        total += current.data  # 포인터 따라가기
        current = current.next  # 불연속 접근
    return total

# 배열: 캐시 친화적
def sum_array(arr):
    &quot;&quot;&quot;
    연속된 메모리 → 캐시 히트 많음
    &quot;&quot;&quot;
    total = 0
    for x in arr:  # 연속 접근
        total += x
    return total

# 벤치마크 (n = 1,000,000)
# 연결 리스트: 0.15초
# 배열: 0.05초
# → 3배 차이! (둘 다 O(n))</code></pre>
<hr>
<h2 id="🔧-하드웨어-특성">🔧 하드웨어 특성</h2>
<h3 id="cpu-파이프라인과-분기-예측">CPU 파이프라인과 분기 예측</h3>
<p><strong>분기 예측(Branch Prediction)</strong>:</p>
<p>*CPU가 if-else 같은 조건문(분기)의 결과(참/거짓)를 실제로 계산하기 전에 미리 추측하여 명령어를 파이프라인에 미리 실행하는 고속화 기술</p>
<pre><code class="language-python">import random
import time

# 예측 가능한 분기
def predictable_branch(arr):
    &quot;&quot;&quot;
    정렬된 배열: 분기 예측 성공
    &quot;&quot;&quot;
    count = 0
    for x in arr:
        if x &lt; 50:  # 처음엔 항상 True, x가 50 이후에는 항상 False
            count += 1
    return count

# 예측 불가능한 분기
def unpredictable_branch(arr):
    &quot;&quot;&quot;
    무작위 배열: 분기 예측 실패
    &quot;&quot;&quot;
    count = 0
    for x in arr:
        if x &lt; 50:  # True/False 무작위
            count += 1
    return count

# 벤치마크
n = 10_000_000

# 정렬된 배열
sorted_arr = list(range(100))
start = time.time()
predictable_branch(sorted_arr * (n // 100))
sorted_time = time.time() - start

# 무작위 배열
random_arr = [random.randint(0, 99) for _ in range(n)]
start = time.time()
unpredictable_branch(random_arr)
random_time = time.time() - start

print(f&quot;정렬됨: {sorted_time:.4f}s&quot;)
print(f&quot;무작위: {random_time:.4f}s&quot;)
print(f&quot;차이: {random_time / sorted_time:.2f}배&quot;)

# 예상 출력:
# 정렬됨: 0.3245s
# 무작위: 0.8912s
# 차이: 2.75배  ← 같은 연산인데!</code></pre>
<h3 id="simd-single-instruction-multiple-data">SIMD (Single Instruction Multiple Data)</h3>
<p>SIMD(단일 명령어, 다중 데이터)는 병렬 컴퓨팅 아키텍처의 한 유형으로 하나의 명령어로 여러 개의 데이터를 동시에 처리하는 기법</p>
<pre><code class="language-python">import numpy as np
import time

# 일반 반복문
def sum_python(arr):
    &quot;&quot;&quot;
    한 번에 하나씩 처리
    &quot;&quot;&quot;
    total = 0
    for x in arr:
        total += x
    return total

# NumPy (SIMD 활용)
def sum_numpy(arr):
    &quot;&quot;&quot;
    한 번에 여러 개 처리 (벡터화)

    CPU가 한 명령으로 4-8개 동시 처리
    &quot;&quot;&quot;
    return np.sum(arr)

# 벤치마크
n = 10_000_000
arr_python = list(range(n))
arr_numpy = np.arange(n)

start = time.time()
sum_python(arr_python)
python_time = time.time() - start

start = time.time()
sum_numpy(arr_numpy)
numpy_time = time.time() - start

print(f&quot;Python: {python_time:.4f}s&quot;)
print(f&quot;NumPy: {numpy_time:.4f}s&quot;)
print(f&quot;차이: {python_time / numpy_time:.2f}배&quot;)

# 예상 출력:
# Python: 0.8234s
# NumPy: 0.0145s
# 차이: 56.78배  ← 둘 다 O(n)!</code></pre>
<hr>
<h2 id="📊-실제-벤치마크">📊 실제 벤치마크</h2>
<h3 id="정렬-알고리즘-실측">정렬 알고리즘 실측</h3>
<pre><code class="language-python">import time
import random

def benchmark_sorts(n):
    &quot;&quot;&quot;
    다양한 정렬 알고리즘 벤치마크
    &quot;&quot;&quot;
    # 테스트 데이터
    data = [random.randint(0, 1000) for _ in range(n)]

    algorithms = {
        &#39;Bubble&#39;: bubble_sort,
        &#39;Insertion&#39;: insertion_sort,
        &#39;Merge&#39;: merge_sort,
        &#39;Quick&#39;: quick_sort,
        &#39;Python&#39;: sorted  # 내장 함수 (Timsort)
    }

    results = {}

    for name, func in algorithms.items():
        test_data = data.copy()
        start = time.perf_counter()
        func(test_data)
        elapsed = time.perf_counter() - start
        results[name] = elapsed

    return results

# 다양한 크기로 테스트
for n in [100, 500, 1000, 5000]:
    print(f&quot;\nn = {n}:&quot;)
    results = benchmark_sorts(n)

    for name, time_val in sorted(results.items(), key=lambda x: x[1]):
        print(f&quot;  {name:12s}: {time_val:.6f}s&quot;)

# 예상 출력:
# n = 100:
#   Python      : 0.000015s  ← C로 구현, 최적화
#   Quick       : 0.000123s
#   Merge       : 0.000189s
#   Insertion   : 0.000234s  ← n이 작아서 괜찮음
#   Bubble      : 0.000456s

# n = 1000:
#   Python      : 0.000234s
#   Quick       : 0.001567s
#   Merge       : 0.002134s
#   Insertion   : 0.021345s  ← 급격히 느려짐
#   Bubble      : 0.043212s

# n = 5000:
#   Python      : 0.001345s
#   Quick       : 0.009876s
#   Merge       : 0.013456s
#   Insertion   : 0.523451s  ← 매우 느림
#   Bubble      : 1.234567s  ← 극도로 느림</code></pre>
<h3 id="입력-패턴별-성능">입력 패턴별 성능</h3>
<pre><code class="language-python">def benchmark_patterns(sort_func):
    &quot;&quot;&quot;
    다양한 입력 패턴에서 성능 측정
    &quot;&quot;&quot;
    n = 10000

    patterns = {
        &#39;무작위&#39;: [random.randint(0, 1000) for _ in range(n)],
        &#39;정렬됨&#39;: list(range(n)),
        &#39;역순&#39;: list(range(n, 0, -1)),
        &#39;거의 정렬&#39;: list(range(n)),
        &#39;중복 많음&#39;: [i % 10 for i in range(n)]
    }

    # 거의 정렬된 패턴 생성
    patterns[&#39;거의 정렬&#39;][::100] = [random.randint(0, n) for _ in range(n // 100)]

    print(f&quot;\n{sort_func.__name__}:&quot;)
    for pattern_name, data in patterns.items():
        test_data = data.copy()
        start = time.perf_counter()
        sort_func(test_data)
        elapsed = time.perf_counter() - start
        print(f&quot;  {pattern_name:12s}: {elapsed:.6f}s&quot;)

# 테스트
benchmark_patterns(insertion_sort)
benchmark_patterns(quick_sort)
benchmark_patterns(merge_sort)

# 예상 출력:
# insertion_sort:
#   무작위      : 0.523456s
#   정렬됨      : 0.001234s  ← 최선의 경우!
#   역순        : 1.045678s  ← 최악의 경우
#   거의 정렬   : 0.012345s  ← 매우 빠름!
#   중복 많음   : 0.423456s

# quick_sort:
#   무작위      : 0.012345s
#   정렬됨      : 0.234567s  ← 최악의 경우 (피벗 선택)
#   역순        : 0.245678s
#   거의 정렬   : 0.213456s
#   중복 많음   : 0.013456s

# merge_sort:
#   무작위      : 0.015678s
#   정렬됨      : 0.015234s  ← 항상 일정!
#   역순        : 0.015987s
#   거의 정렬   : 0.015456s
#   중복 많음   : 0.015678s</code></pre>
<hr>
<h2 id="🎯-실무-최적화-전략">🎯 실무 최적화 전략</h2>
<h3 id="알고리즘-선택-가이드">알고리즘 선택 가이드</h3>
<pre><code class="language-python">def choose_sort_algorithm(data):
    &quot;&quot;&quot;
    상황에 맞는 정렬 알고리즘 선택
    &quot;&quot;&quot;
    n = len(data)

    # 1. 크기별 선택
    if n &lt; 50:
        return insertion_sort(data)  # 작은 배열: 오버헤드 작음

    # 2. 패턴 감지
    if is_nearly_sorted(data):
        return insertion_sort(data)  # 거의 정렬: O(n)

    # 3. 안정성 필요 여부
    if stability_required:
        return merge_sort(data)  # 안정 정렬

    # 4. 메모리 제한
    if memory_limited:
        return heap_sort(data)  # 제자리 O(n log n)

    # 5. 일반적인 경우
    return quick_sort(data)  # 평균 빠름

def is_nearly_sorted(data, threshold=0.05):
    &quot;&quot;&quot;
    거의 정렬되어 있는지 확인
    &quot;&quot;&quot;
    inversions = 0
    n = len(data)

    # 샘플링으로 확인 (전체 확인은 비용 큼)
    sample_size = min(1000, n)
    for i in range(sample_size - 1):
        if data[i] &gt; data[i + 1]:
            inversions += 1

    return inversions / sample_size &lt; threshold</code></pre>
<h3 id="마이크로-최적화">마이크로 최적화</h3>
<pre><code class="language-python"># 1. 함수 호출 오버헤드 줄이기
def slow_sum(arr):
    &quot;&quot;&quot;
    함수 호출 많음
    &quot;&quot;&quot;
    total = 0
    for x in arr:
        total = add(total, x)  # 함수 호출 오버헤드
    return total

def add(a, b):
    return a + b

def fast_sum(arr):
    &quot;&quot;&quot;
    인라인 연산
    &quot;&quot;&quot;
    total = 0
    for x in arr:
        total += x  # 직접 연산
    return total

# 2. 불필요한 작업 제거
def slow_filter(arr):
    &quot;&quot;&quot;
    중복 계산
    &quot;&quot;&quot;
    result = []
    for x in arr:
        if is_prime(x):  # 매번 계산
            result.append(x)
    return result

def fast_filter(arr):
    &quot;&quot;&quot;
    캐싱
    &quot;&quot;&quot;
    prime_cache = {}
    result = []

    for x in arr:
        if x not in prime_cache:
            prime_cache[x] = is_prime(x)

        if prime_cache[x]:
            result.append(x)

    return result

# 3. 메모리 할당 줄이기
def slow_concat(strings):
    &quot;&quot;&quot;
    매번 새 문자열 생성
    &quot;&quot;&quot;
    result = &quot;&quot;
    for s in strings:
        result += s  # O(n²) 시간!
    return result

def fast_concat(strings):
    &quot;&quot;&quot;
    한 번에 결합
    &quot;&quot;&quot;
    return &quot;&quot;.join(strings)  # O(n) 시간</code></pre>
<h3 id="프로파일링">프로파일링</h3>
<p>프로그램의 시간 복잡도 및 공간(메모리), 특정 명령어 이용, 함수 호출의 주기와 빈도 등을 측정하는 동적 프로그램 분석의 한 형태</p>
<pre><code class="language-python">import cProfile  # Python 내장 프로파일러
import pstats    # 프로파일링 결과 분석 도구

def profile_function(func, *args):
    &quot;&quot;&quot;
    함수의 실행 시간과 호출 통계를 측정하는 프로파일러

    func: 측정할 함수
    *args: 함수에 전달할 인자들

    Returns: 함수의 실행 결과
    &quot;&quot;&quot;
    # 1. 프로파일러 생성 및 시작
    profiler = cProfile.Profile()
    profiler.enable()  # 측정 시작

    # 2. 함수 실행 (이 구간이 측정됨)
    result = func(*args)

    # 3. 측정 종료
    profiler.disable()

    # 4. 결과 분석 및 출력
    stats = pstats.Stats(profiler)
    stats.sort_stats(&#39;cumulative&#39;)  # 누적 시간 순으로 정렬
    stats.print_stats(10)  # 상위 10개 함수만 출력

    return result

# 사용 예시
def slow_algorithm(n):
    &quot;&quot;&quot;
    성능 측정을 위한 느린 알고리즘

    O(n²) 시간복잡도
    &quot;&quot;&quot;
    result = []
    for i in range(n):
        for j in range(n):
            result.append(i * j)  # n² 번 호출
    return result

# 프로파일링 실행
profile_function(slow_algorithm, 1000)

# 출력 결과 해석:
&quot;&quot;&quot;
   ncalls  tottime  percall  cumtime  percall  filename:lineno(function)
        1    0.234    0.234    0.567    0.567  script.py:45(slow_algorithm)
  1000000    0.156    0.000    0.156    0.000  {method &#39;append&#39;}
        1    0.089    0.089    0.089    0.089  {range}

컬럼 의미:
---------------------------------------------------------------------------
ncalls:   함수가 호출된 횟수. 예: slow_algorithm은 1번, append는 1,000,000번 호출됨
tottime:  이 함수 자체에서 소요된 시간 (하위 함수 호출시간 제외). 예: slow_algorithm 자체 로직은 0.234초       
percall:  호출 1회 당 평균시간 (tottime / ncalls). 예: append 1회 당 0.000000156초 (매우 빠름)          
cumtime:  이 함수와 하위 함수 호출을 모두 포함한 총 시간. 예: slow_algorithm 전체는 0.567초          
percall:  호출 1회 당 누적 평균시간 (cumtime / ncalls)
filename:lineno(function): 함수의 위치. 예: script.py 파일 45번째 줄의 slow_algorithm 함수

분석:
-----
1. slow_algorithm이 0.567초 소요 (전체 시간)
2. append가 1,000,000번 호출되어 0.156초 소요 → 병목! append 호출을 줄이면 빨라짐
3. 개선 방법: 리스트 컴프리헨션 사용
   result = [i * j for i in range(n) for j in range(n)]
&quot;&quot;&quot;</code></pre>
<hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<p><strong>조기 최적화의 함정</strong></p>
<pre><code class="language-python"># 잘못된 접근
def premature_optimization():
    &quot;&quot;&quot;
    &quot;이 부분이 느릴 것 같으니 미리 최적화하자&quot;

    문제:
    - 실제로 느린지 모름
    - 복잡도만 증가
    - 유지보수 어려움
    &quot;&quot;&quot;
    # 복잡한 최적화 코드...
    pass

# 올바른 접근
def measure_then_optimize():
    &quot;&quot;&quot;
    1. 먼저 간단하게 구현
    2. 프로파일링으로 병목 찾기
    3. 병목만 최적화
    &quot;&quot;&quot;
    # 1. 간단한 구현
    def simple_version():
        pass

    # 2. 측정
    # ... 프로파일링 ...

    # 3. 필요한 부분만 최적화
    def optimized_critical_path():
        pass</code></pre>
<p><strong>실측의 중요성</strong></p>
<pre><code class="language-python"># 항상 실제 측정하기
import timeit   # timeit: 소규모 코드 조각이나 함수의 실행시간을 정확하게 측정, 성능을 비교 분석하는 데 사용

def compare_implementations():
    &quot;&quot;&quot;
    여러 구현 비교
    &quot;&quot;&quot;
    # 방법 1
    time1 = timeit.timeit(
        &#39;sum(range(1000))&#39;,
        number=10000
    )

    # 방법 2
    time2 = timeit.timeit(
        &#39;total = 0\nfor i in range(1000): total += i&#39;,
        number=10000
    )

    print(f&quot;방법 1: {time1:.4f}s&quot;)
    print(f&quot;방법 2: {time2:.4f}s&quot;)

    # 예상과 다를 수 있음!
    # 내장 함수가 최적화되어 있을 수도</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>이론 vs 실제</strong></p>
<pre><code>Big-O의 한계:
- 상수 계수 무시
- 낮은 차수 항 무시
- 하드웨어 특성 무시

실제 성능 요인:
- 상수 계수 (10 ~ 100배 차이)
- 캐시 효율 (2 ~ 10배 차이)
- 하드웨어 특성 (2 ~ 100배 차이)
- 구현 품질</code></pre><p><strong>상수 계수의 중요성</strong></p>
<pre><code>같은 O(n)도:
- 구현에 따라 10배 차이
- 언어에 따라 100배 차이
- 최적화에 따라 1000배 차이

교차점:
- 삽입 vs 병합: n ≈ 50
- 선형 vs 이진: n ≈ 100</code></pre><p><strong>캐시와 메모리</strong></p>
<pre><code>지역성 원리:
- 시간적: 최근 접근 재사용
- 공간적: 인접 데이터 접근

캐시 친화적:
- 순차 접근
- 배열 우선
- 행 우선 순회</code></pre><p><strong>실무 가이드</strong></p>
<pre><code>최적화 순서:
1. 올바른 알고리즘 선택
2. 프로파일링으로 병목 찾기
3. 병목만 최적화
4. 측정하여 검증

&quot;조기 최적화는 만악의 근원&quot;
- Donald Knuth</code></pre><p><strong>벤치마크 필수</strong></p>
<pre><code>이론으로 예측 X
실제로 측정 O

도구:
- timeit (Python)
- cProfile (프로파일링)
- perf (Linux)</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[07-06] 상각 분석 (Amortized Analysis)</strong></p>
<ul>
<li>상각 분석의 개념: 연속된 연산의 평균 비용 계산</li>
<li>집계 방법: 전체 비용을 연산 횟수로 나누기</li>
<li>회계 방법: 크레딧 개념을 이용한 분석</li>
<li>포텐셜 방법: 잠재 함수를 이용한 엄밀한 증명</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[07-04] 최선/평균/최악 경우 분석</a><br><strong>다음 글</strong>: <a href="#">[07-06] 상각 분석</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [07-04] 최선/평균/최악 경우 분석]]></title>
            <link>https://velog.io/@road_to_ai/07-04-%EC%B5%9C%EC%84%A0%ED%8F%89%EA%B7%A0%EC%B5%9C%EC%95%85-%EA%B2%BD%EC%9A%B0-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@road_to_ai/07-04-%EC%B5%9C%EC%84%A0%ED%8F%89%EA%B7%A0%EC%B5%9C%EC%95%85-%EA%B2%BD%EC%9A%B0-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Thu, 05 Mar 2026 14:36:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>같은 알고리즘도 입력에 따라 성능이 달라지므로, 최선/평균/최악의 경우를 각각 분석하여 알고리즘의 전체적인 성능을 이해해야 합니다.</p>
</blockquote>
<hr>
<h2 id="🎯-경우별-분석이란-무엇인가">🎯 경우별 분석이란 무엇인가</h2>
<h3 id="왜-입력에-따라-성능이-다른가">왜 입력에 따라 성능이 다른가?</h3>
<p><strong>같은 알고리즘도 입력이 다르면 성능이 크게 달라집니다.</strong></p>
<p><strong>실생활 비유</strong>:</p>
<pre><code>책에서 특정 페이지 찾기:

최선의 경우:
- 첫 페이지가 목표 페이지
- 1번 만에 찾음

평균적인 경우:
- 중간쯤에 있음
- 절반 정도 확인

최악의 경우:
- 마지막 페이지가 목표
- 전체를 다 확인</code></pre><p><strong>프로그래밍 예시</strong>:</p>
<pre><code class="language-python">def linear_search(arr, target):
    &quot;&quot;&quot;
    선형 탐색

    입력에 따라 성능이 다름!
    &quot;&quot;&quot;
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1

# 최선: arr = [5, 2, 8], target = 5
# → 첫 번째에 있음, 1번 비교

# 평균: arr = [1, 2, 3, 4, 5], target = 3
# → 중간에 있음, n/2번 비교

# 최악: arr = [1, 2, 3, 4, 5], target = 5
# → 마지막에 있음, n번 비교

# 또는 arr = [1, 2, 3], target = 10
# → 없음, n번 비교</code></pre>
<h3 id="세-가지-경우의-정의">세 가지 경우의 정의</h3>
<pre><code>최선의 경우 (Best Case):
- 가장 유리한 입력
- 가장 빠른 실행
- Ω(g(n))으로 표현

평균의 경우 (Average Case):
- 전형적인 입력
- 기댓값 계산
- Θ(g(n))으로 표현 (많은 경우)

최악의 경우 (Worst Case):
- 가장 불리한 입력
- 가장 느린 실행
- O(g(n))으로 표현</code></pre><hr>
<h2 id="🎲-최선의-경우-분석">🎲 최선의 경우 분석</h2>
<h3 id="최선의-경우란">최선의 경우란?</h3>
<p><strong>최선의 경우(Best Case)</strong>는 알고리즘이 가장 빠르게 실행되는 입력 조건입니다.</p>
<p><strong>특징</strong>:</p>
<pre><code>장점:
- 이론적 하한 제시
- 최적화 가능성 파악

단점:
- 실무에서 거의 의미 없음
- 운이 좋아야 발생
- 성능 보장 안 됨

표현:
- Ω(g(n)) 사용</code></pre><h3 id="최선의-경우-예시">최선의 경우 예시</h3>
<p><strong>예시 1: 선형 탐색</strong></p>
<pre><code class="language-python">def linear_search(arr, target):
    &quot;&quot;&quot;
    최선: Ω(1)

    첫 번째 원소가 target
    &quot;&quot;&quot;
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1

# 최선의 입력:
arr = [100, 2, 3, 4, 5]
target = 100
# → 1번 비교로 종료
# → Ω(1)</code></pre>
<p><strong>예시 2: 삽입 정렬</strong></p>
<pre><code class="language-python">def insertion_sort(arr):
    &quot;&quot;&quot;
    최선: Ω(n)

    이미 정렬된 배열
    &quot;&quot;&quot;
    n = len(arr)

    for i in range(1, n):
        key = arr[i]
        j = i - 1

        # 이미 정렬되어 있으면 while 진입 안 함
        while j &gt;= 0 and arr[j] &gt; key:
            arr[j + 1] = arr[j]
            j -= 1

        arr[j + 1] = key

    return arr

# 최선의 입력:
arr = [1, 2, 3, 4, 5]  # 이미 정렬됨
# → 각 i마다 1번 비교만
# → 총 n번 비교
# → Ω(n)</code></pre>
<p><strong>예시 3: 퀵 정렬</strong></p>
<pre><code class="language-python">def quick_sort(arr):
    &quot;&quot;&quot;
    최선: Ω(n log n)

    피벗이 항상 중간값
    &quot;&quot;&quot;
    if len(arr) &lt;= 1:
        return arr

    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x &lt; pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x &gt; pivot]

    return quick_sort(left) + middle + quick_sort(right)

# 최선의 입력:
# 피벗이 항상 정확히 중간에 위치
# → 균등 분할
# → Ω(n log n)</code></pre>
<hr>
<h2 id="📊-평균의-경우-분석">📊 평균의 경우 분석</h2>
<h3 id="평균의-경우란">평균의 경우란?</h3>
<p><strong>평균의 경우(Average Case)</strong>는 모든 가능한 입력에 대한 성능의 기댓값입니다.</p>
<p><strong>특징</strong>:</p>
<pre><code>장점:
- 실제 성능 반영
- 실무에서 유용

단점:
- 계산이 어려움
- 입력 분포 가정 필요

표현:
- Θ(g(n)) 또는 O(g(n))</code></pre><h3 id="평균-경우-계산-방법">평균 경우 계산 방법</h3>
<p><strong>확률적 분석 과정</strong>:</p>
<pre><code>1. 가능한 모든 입력 나열
2. 각 입력의 확률 계산
3. 각 입력의 실행 시간 계산
4. 기댓값 계산

E[T(n)] = Σ P(입력_i) × T(입력_i)

여기서:
- E[T(n)]: 기댓값
- P(입력_i): 입력_i의 확률
- T(입력_i): 입력_i에서의 실행 시간</code></pre><h3 id="평균-경우-예시">평균 경우 예시</h3>
<p><strong>예시 1: 선형 탐색 (성공)</strong></p>
<pre><code class="language-python">def linear_search(arr, target):
    &quot;&quot;&quot;
    평균 경우 분석

    가정: target이 배열에 있음
         각 위치에 있을 확률은 같음 (1/n)
    &quot;&quot;&quot;
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1

# 분석:
# n개 원소, target이 i번째에 있을 확률 = 1/n
# i번째에 있으면 i번 비교

# 기댓값:
# E[비교 횟수] = Σ(i=1 to n) (1/n) × i
#              = (1/n) × [1 + 2 + 3 + ... + n]
#              = (1/n) × n(n+1)/2
#              = (n+1)/2
#              ≈ n/2

# 평균: Θ(n)</code></pre>
<p><strong>상세 계산</strong>:</p>
<pre><code>배열: [a₁, a₂, a₃, ..., aₙ]
target이 각 위치에 있을 확률: 1/n

위치    비교 횟수    확률     기여도
--------------------------------------
1       1          1/n      1/n
2       2          1/n      2/n
3       3          1/n      3/n
...
n       n          1/n      n/n

총 기댓값:
E = (1 + 2 + 3 + ... + n) / n
  = [n(n+1)/2] / n
  = (n+1)/2
  = n/2 + 1/2
  ≈ n/2 (n이 클 때)

∴ 평균: Θ(n)</code></pre><p><strong>예시 2: 선형 탐색 (실패 포함)</strong></p>
<pre><code class="language-python"># 더 현실적인 분석:
# target이 있을 확률 = p
# target이 없을 확률 = 1-p

# 있는 경우 평균: (n+1)/2번
# 없는 경우: n번

# 전체 평균:
# E = p × (n+1)/2 + (1-p) × n
#   = p(n+1)/2 + n - pn
#   = pn/2 + p/2 + n - pn
#   = n - pn/2 + p/2
#   = n(1 - p/2) + p/2

# p = 0.5 (50% 확률로 있음):
# E = n(1 - 0.25) + 0.25
#   = 0.75n + 0.25
#   ≈ 3n/4

# 여전히 Θ(n)</code></pre>
<p><strong>예시 3: 퀵 정렬</strong></p>
<pre><code class="language-python">def quick_sort_average():
    &quot;&quot;&quot;
    평균 경우: Θ(n log n)

    가정: 피벗이 무작위 선택
         모든 순열이 동일 확률

    분석:
    T(n) = T(k) + T(n-k-1) + Θ(n)

    k: 피벗의 순위 (0부터 n-1까지 균등)

    평균:
    E[T(n)] = E[T(k) + T(n-k-1)] + Θ(n)
            = (1/n) Σ(k=0 to n-1) [T(k) + T(n-k-1)] + Θ(n)

    이를 풀면:
    E[T(n)] = Θ(n log n)
    &quot;&quot;&quot;
    pass

# 직관적 이해:
# - 피벗이 평균적으로 &quot;중간쯤&quot; 선택됨
# - 균형 잡힌 분할 → log n 레벨
# - 각 레벨 O(n) 작업
# → 총 Θ(n log n)</code></pre>
<hr>
<h2 id="💥-최악의-경우-분석">💥 최악의 경우 분석</h2>
<h3 id="최악의-경우란">최악의 경우란?</h3>
<p><strong>최악의 경우(Worst Case)</strong>는 알고리즘이 가장 느리게 실행되는 입력 조건입니다.</p>
<p><strong>특징</strong>:</p>
<pre><code>장점:
- 성능 보장 제공
- 안전한 설계 가능
- 가장 중요!

단점:
- 실제보다 비관적일 수 있음

표현:
- O(g(n)) 사용</code></pre><h3 id="왜-최악의-경우가-중요한가">왜 최악의 경우가 중요한가?</h3>
<p><strong>실무에서 최악의 경우를 고려해야 하는 이유</strong>:</p>
<pre><code>1. 성능 보장:
   &quot;절대 n² 시간을 넘지 않습니다&quot;
   → 신뢰성

2. 최악이 자주 발생:
   정렬된 데이터에 삽입 정렬
   → 매번 최악!

3. 안전 중요 시스템:
   의료 기기, 항공
   → 최악도 허용 범위 내

4. 실시간 시스템:
   게임, 동영상
   → 최대 지연 시간 보장</code></pre><h3 id="최악의-경우-예시">최악의 경우 예시</h3>
<p><strong>예시 1: 선형 탐색</strong></p>
<pre><code class="language-python">def linear_search(arr, target):
    &quot;&quot;&quot;
    최악: O(n)

    1) target이 마지막에 있음
    2) target이 없음
    &quot;&quot;&quot;
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1

# 최악의 입력 1:
arr = [1, 2, 3, 4, 5]
target = 5
# → n번 비교

# 최악의 입력 2:
arr = [1, 2, 3, 4, 5]
target = 10  # 없음
# → n번 비교

# 최악: O(n)</code></pre>
<p><strong>예시 2: 버블 정렬</strong></p>
<pre><code class="language-python">def bubble_sort(arr):
    &quot;&quot;&quot;
    최악: O(n²)

    역순 정렬된 배열
    &quot;&quot;&quot;
    n = len(arr)

    for i in range(n - 1):
        for j in range(n - 1 - i):
            if arr[j] &gt; arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]

    return arr

# 최악의 입력:
arr = [5, 4, 3, 2, 1]  # 역순
# → 모든 쌍을 교환
# → n(n-1)/2번 교환
# → O(n²)</code></pre>
<p><strong>예시 3: 퀵 정렬</strong></p>
<pre><code class="language-python">def quick_sort_worst(arr):
    &quot;&quot;&quot;
    최악: O(n²)

    피벗이 항상 최솟값 또는 최댓값
    &quot;&quot;&quot;
    if len(arr) &lt;= 1:
        return arr

    # 나쁜 피벗 선택: 항상 첫 원소
    pivot = arr[0]
    left = [x for x in arr[1:] if x &lt; pivot]
    middle = [pivot]
    right = [x for x in arr[1:] if x &gt;= pivot]

    return quick_sort_worst(left) + middle + quick_sort_worst(right)

# 최악의 입력:
arr = [1, 2, 3, 4, 5]  # 이미 정렬됨
# 피벗이 항상 최솟값
# → 불균형 분할
# → n + (n-1) + (n-2) + ... + 1
# → O(n²)

# 해결: 무작위 피벗 또는 중간값 선택</code></pre>
<hr>
<h2 id="📈-알고리즘별-경우-분석">📈 알고리즘별 경우 분석</h2>
<h3 id="정렬-알고리즘-비교">정렬 알고리즘 비교</h3>
<pre><code>알고리즘       최선          평균          최악
--------------------------------------------------
버블 정렬     O(n)         O(n²)         O(n²)
선택 정렬     O(n²)        O(n²)         O(n²)
삽입 정렬     O(n)         O(n²)         O(n²)
병합 정렬     Θ(n log n)   Θ(n log n)    Θ(n log n)
퀵 정렬       Ω(n log n)   Θ(n log n)    O(n²)
힙 정렬       Ω(n log n)   Θ(n log n)    O(n log n)</code></pre><p><strong>분석</strong>:</p>
<pre><code class="language-python"># 버블 정렬
def bubble_sort(arr):
    &quot;&quot;&quot;
    최선: O(n) - 이미 정렬됨 (최적화 버전)
    평균: O(n²)
    최악: O(n²) - 역순
    &quot;&quot;&quot;
    n = len(arr)
    for i in range(n - 1):
        swapped = False
        for j in range(n - 1 - i):
            if arr[j] &gt; arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                swapped = True
        if not swapped:  # 최선의 경우 조기 종료
            break

# 삽입 정렬
def insertion_sort(arr):
    &quot;&quot;&quot;
    최선: O(n) - 이미 정렬됨
    평균: O(n²)
    최악: O(n²) - 역순

    특징: 거의 정렬된 데이터에 매우 빠름!
    &quot;&quot;&quot;
    n = len(arr)
    for i in range(1, n):
        key = arr[i]
        j = i - 1
        # 이미 정렬되어 있으면 while 안 들어감
        while j &gt;= 0 and arr[j] &gt; key:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key

# 병합 정렬
def merge_sort(arr):
    &quot;&quot;&quot;
    최선: Θ(n log n)
    평균: Θ(n log n)
    최악: Θ(n log n)

    특징: 항상 일정! 안정적!
    &quot;&quot;&quot;
    if len(arr) &lt;= 1:
        return arr

    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])

    return merge(left, right)
    # 입력과 무관하게 항상 같은 분할</code></pre>
<h3 id="탐색-알고리즘-비교">탐색 알고리즘 비교</h3>
<pre><code>알고리즘       최선          평균          최악
--------------------------------------------------
선형 탐색     O(1)         O(n)          O(n)
이진 탐색     O(1)         O(log n)      O(log n)
해시 탐색     O(1)         O(1)          O(n)</code></pre><p><strong>분석</strong>:</p>
<pre><code class="language-python"># 이진 탐색
def binary_search(arr, target):
    &quot;&quot;&quot;
    최선: O(1) - 중간에 있음
    평균: O(log n)
    최악: O(log n) - 없음

    전제: 정렬된 배열
    &quot;&quot;&quot;
    left, right = 0, len(arr) - 1

    while left &lt;= right:
        mid = (left + right) // 2

        if arr[mid] == target:
            return mid  # 최선: 첫 비교에 찾음
        elif arr[mid] &lt; target:
            left = mid + 1
        else:
            right = mid - 1

    return -1  # 최악: log n번 비교 후 실패

# 해시 테이블
class HashTable:
    &quot;&quot;&quot;
    최선: O(1) - 충돌 없음
    평균: O(1) - 충돌 적음
    최악: O(n) - 모든 원소가 같은 버킷

    최악의 경우:
    - 나쁜 해시 함수
    - 모든 키가 같은 인덱스로
    &quot;&quot;&quot;
    pass</code></pre>
<hr>
<h2 id="🎯-실무-의사결정">🎯 실무 의사결정</h2>
<h3 id="어떤-경우를-고려해야-하는가">어떤 경우를 고려해야 하는가?</h3>
<p><strong>시나리오별 선택</strong>:</p>
<pre><code>1. 성능 보장 중요 (실시간, 안전) → 최악의 경우 최우선
   예: 병합 정렬 (Θ(n log n) 보장)

2. 일반적인 웹 서비스  → 평균 경우 중요
   예: 퀵 정렬 (평균 빠름)

3. 특수한 입력 패턴 → 최선의 경우 활용
   예: 거의 정렬된 데이터 → 삽입 정렬</code></pre><h3 id="하이브리드-접근">하이브리드 접근</h3>
<pre><code class="language-python">def tim_sort_strategy(arr):
    &quot;&quot;&quot;
    Python의 기본 정렬 (Timsort)

    전략: 입력에 따라 다른 알고리즘
    &quot;&quot;&quot;
    n = len(arr)

    # 작은 배열: 삽입 정렬
    if n &lt; 64:
        return insertion_sort(arr)

    # 큰 배열: 병합 정렬
    # 단, 거의 정렬된 부분은 삽입 정렬
    return adaptive_merge_sort(arr)

def intro_sort_strategy(arr):
    &quot;&quot;&quot;
    C++ STL sort (Introsort)

    전략: 퀵 정렬 + 힙 정렬
    &quot;&quot;&quot;
    # 일반: 퀵 정렬 (평균 빠름)
    # 재귀 깊이 초과 시: 힙 정렬 (최악 회피)

    max_depth = 2 * log(len(arr))
    return adaptive_quick_sort(arr, max_depth)</code></pre>
<h3 id="실무-예시">실무 예시</h3>
<p><strong>예시 1: 데이터베이스 인덱스</strong></p>
<pre><code>선택: B-Tree vs 해시 인덱스

B-Tree:
- 최악: O(log n)
- 범위 검색 가능
- 안정적

해시:
- 평균: O(1)
- 최악: O(n) (충돌 시)
- 범위 검색 불가

결정:
→ 범위 검색 필요 → B-Tree
→ 단일 키 검색만 → 해시</code></pre><p><strong>예시 2: 웹 서버 응답 시간</strong></p>
<pre><code>요구사항:
- 평균 응답 시간: 100ms 이하
- 최대 응답 시간: 1000ms 이하

선택:
평균이 중요하지만
최악도 허용 범위 내여야 함

→ 최악의 경우도 반드시 확인!</code></pre><hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<p><strong>최악의 경우 회피</strong></p>
<pre><code class="language-python"># 나쁜 예: 퀵 정렬 (피벗 고정)
def quick_sort_bad(arr):
    if len(arr) &lt;= 1:
        return arr

    pivot = arr[0]  # 항상 첫 원소
    # 정렬된 배열에서 O(n²)!
    # ...

# 좋은 예: 무작위 피벗
import random

def quick_sort_good(arr):
    if len(arr) &lt;= 1:
        return arr

    # 무작위 피벗 선택
    pivot_idx = random.randint(0, len(arr) - 1)
    pivot = arr[pivot_idx]

    # 최악의 경우 확률적으로 회피
    # 평균 O(n log n) 보장</code></pre>
<p><strong>입력 검증</strong></p>
<pre><code class="language-python">def process_data(data):
    &quot;&quot;&quot;
    최악의 입력 패턴 사전 차단
    &quot;&quot;&quot;
    # 1. 크기 제한
    if len(data) &gt; MAX_SIZE:
        raise ValueError(&quot;입력이 너무 큼&quot;)

    # 2. 패턴 감지
    if is_adversarial_input(data):
        # 다른 알고리즘 사용
        return robust_algorithm(data)

    # 3. 일반 알고리즘
    return fast_algorithm(data)</code></pre>
<p><strong>성능 모니터링</strong></p>
<pre><code class="language-python">import time

def monitored_sort(arr):
    &quot;&quot;&quot;
    실행 시간 측정
    &quot;&quot;&quot;
    start = time.time()
    result = sort_algorithm(arr)
    elapsed = time.time() - start

    # 경고: 최악의 경우 감지
    if elapsed &gt; THRESHOLD:
        log_warning(f&quot;느린 정렬: {elapsed}초, 크기: {len(arr)}&quot;)

    return result</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>세 가지 경우</strong></p>
<pre><code>최선의 경우:
- 가장 유리한 입력
- 이론적 하한
- Ω(g(n))

평균의 경우:
- 전형적인 입력
- 기댓값 계산
- 실무 중요

최악의 경우:
- 가장 불리한 입력
- 성능 보장
- 가장 중요!</code></pre><p><strong>알고리즘 선택 기준</strong></p>
<pre><code>정렬 선택:
작은 배열 (&lt; 50):       삽입 정렬
거의 정렬된 경우:        삽입 정렬
성능 보장 필요:          병합 정렬
평균적으로 빠르게:        퀵 정렬</code></pre><p><strong>복잡도 표현</strong></p>
<pre><code>최선: Ω(g(n)) 또는 Θ(g(n))
평균: Θ(g(n)) 또는 O(g(n))
최악: O(g(n))

보통 최악으로 표현:
&quot;이 알고리즘은 O(n log n)입니다&quot;
= &quot;최악의 경우 O(n log n)&quot;</code></pre><p><strong>실무 지침</strong></p>
<pre><code>성능 중요 시스템:
→ 최악의 경우 우선
→ 병합 정렬, 힙 정렬

일반 시스템:
→ 평균의 경우 고려
→ 퀵 정렬, 해시

특수 상황:
→ 입력 특성 활용
→ 삽입 정렬 (거의 정렬됨)</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[07-05] 상수 계수와 실제 성능</strong></p>
<ul>
<li>이론 vs 실제: Big-O로는 알 수 없는 실제 성능 차이</li>
<li>상수 계수의 영향: 같은 O(n)도 10배 차이 가능</li>
<li>캐시와 메모리: 하드웨어가 성능에 미치는 영향</li>
<li>실무 벤치마크: 실제 측정과 프로파일링 방법</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[07-03] 점근적 표기법</a><br><strong>다음 글</strong>: <a href="#">[07-05] 상수 계수와 실제 성능</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [07-03] 점근적 표기법 (Asymptotic Notation)]]></title>
            <link>https://velog.io/@road_to_ai/07-03-%EC%A0%90%EA%B7%BC%EC%A0%81-%ED%91%9C%EA%B8%B0%EB%B2%95-Asymptotic-Notation</link>
            <guid>https://velog.io/@road_to_ai/07-03-%EC%A0%90%EA%B7%BC%EC%A0%81-%ED%91%9C%EA%B8%B0%EB%B2%95-Asymptotic-Notation</guid>
            <pubDate>Sun, 01 Mar 2026 02:24:55 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>점근적 표기법은 알고리즘의 성능을 수학적으로 엄밀하게 표현하는 방법으로, Big-O, Big-Ω, Big-Θ 등이 있습니다.</p>
</blockquote>
<hr>
<h2 id="🎯-점근적-표기법이란-무엇인가">🎯 점근적 표기법이란 무엇인가</h2>
<h3 id="점근적-표기법의-기본-개념">점근적 표기법의 기본 개념</h3>
<p>지금까지 &quot;O(n)&quot;, &quot;O(n²)&quot; 같은 표기를 사용했습니다. 이제 이것의 <strong>정확한 수학적 의미</strong>를 알아봅시다.</p>
<p><strong>점근적(Asymptotic)</strong>의 의미:</p>
<pre><code>점근(漸近): 점점 가까워진다

수학적 의미:
n이 무한대로 갈 때 (n → ∞)
함수의 증가율이 어떻게 되는가?

예:
f(n) = 3n² + 2n + 1

n이 클 때:
- n² 항이 지배적
- 2n, 1은 무시 가능
- 계수 3도 무시

→ &quot;점근적으로 n²에 비례한다&quot;</code></pre><p><strong>실생활 비유</strong>:</p>
<pre><code>두 사람의 재산 증가:

A: 매년 100만원씩 증가 (선형)
B: 매년 전년도의 2배 증가 (지수)

초기 (1년차):
A: 100만원
B: 100만원
→ 비슷함

나중 (20년차):
A: 2,000만원
B: 52,428,800만원 (약 5천억!)
→ B가 압도적

점근적으로 지수 증가가 훨씬 빠름!</code></pre><h3 id="왜-점근적-표기법이-필요한가">왜 점근적 표기법이 필요한가?</h3>
<p><strong>1. 하드웨어 독립적</strong></p>
<pre><code>실제 실행 시간:
- 컴퓨터마다 다름
- 언어마다 다름
- 컴파일러마다 다름

점근적 표기법:
- 하드웨어 무관
- 언어 무관
- 본질적 성능만 표현</code></pre><p><strong>2. 큰 입력에서의 행동</strong></p>
<pre><code>작은 입력 (n=10):
O(n): 10번
O(n²): 100번
→ 10배 차이

큰 입력 (n=10000):
O(n): 10,000번
O(n²): 100,000,000번
→ 10,000배 차이!

점근적 표기법은 큰 입력에서의 차이를 보여줌</code></pre><p><strong>3. 알고리즘 비교</strong></p>
<pre><code>알고리즘 A: 5n² + 100n + 500
알고리즘 B: 0.01n³ + n

어느 것이 빠른가?

작은 n: A가 느릴 수도
큰 n: B가 훨씬 느림 (n³ 때문)

점근적으로 A가 우수!</code></pre><hr>
<h2 id="📐-big-o-표기법-상한">📐 Big-O 표기법 (상한)</h2>
<h3 id="big-o-빅-오의-정의">Big-O (빅 오)의 정의</h3>
<p><strong>수학적 정의</strong>:</p>
<pre><code>f(n) = O(g(n))

의미:
n이 충분히 클 때, f(n)은 g(n)의 상수 배를 넘지 않는다

엄밀한 정의:
f(n) = O(g(n)) ⟺
∃ c &gt; 0, ∃ n₀ &gt; 0 such that
∀ n ≥ n₀, f(n) ≤ c · g(n)

읽는 법:
&quot;양의 상수 c와 n₀가 존재하여,
모든 n ≥ n₀에 대해
f(n) ≤ c · g(n)이 성립한다&quot;</code></pre><p><strong>직관적 이해</strong>:</p>
<pre><code>f(n) = O(g(n))

= &quot;f(n)은 최악의 경우 g(n)에 비례한다&quot;
= &quot;f(n)은 g(n)보다 빠르거나 같다&quot;
= &quot;g(n)은 f(n)의 상한(upper bound)&quot;

예:
f(n) = 3n² + 2n + 1 = O(n²)

의미:
n이 충분히 크면
3n² + 2n + 1 ≤ c · n² (적당한 c에 대해)</code></pre><h3 id="big-o-증명-예시">Big-O 증명 예시</h3>
<p><strong>예제 1</strong>: f(n) = 3n + 5는 O(n)임을 증명하라.</p>
<pre><code>증명:
f(n) = 3n + 5

목표: f(n) ≤ c · n을 만족하는 c, n₀ 찾기

시도:
3n + 5 ≤ c · n
5 ≤ (c - 3)n
5/n ≤ c - 3

n ≥ 1일 때, 5/n ≤ 5
따라서 c = 8, n₀ = 1이면 성립

검증:
n ≥ 1일 때
3n + 5 ≤ 8n
5 ≤ 5n ✓

∴ f(n) = O(n) (증명 완료)</code></pre><p><strong>예제 2</strong>: f(n) = n² + n은 O(n²)임을 증명하라.</p>
<pre><code>증명:
f(n) = n² + n

목표: f(n) ≤ c · n²을 만족하는 c, n₀ 찾기

시도:
n² + n ≤ c · n²
n ≤ (c - 1)n²
1/n ≤ c - 1

n ≥ 1일 때, 1/n ≤ 1
따라서 c = 2, n₀ = 1이면 성립

검증:
n ≥ 1일 때
n² + n ≤ 2n²
n ≤ n² ✓ (n ≥ 1이면 성립)

∴ f(n) = O(n²) (증명 완료)</code></pre><p><strong>예제 3</strong>: f(n) = 2ⁿ은 O(n²)이 <strong>아님</strong>을 증명하라.</p>
<pre><code>귀류법으로 증명:

가정: f(n) = O(n²)
즉, ∃ c, n₀ such that 2ⁿ ≤ c · n²

하지만:
n = 100일 때
2¹⁰⁰ ≈ 1.27 × 10³⁰
c · 100² = c · 10,000

아무리 큰 c를 선택해도
2ⁿ은 결국 c · n²을 넘어섬

이유: 지수 함수는 다항 함수보다 빠르게 증가

∴ 2ⁿ ≠ O(n²) (증명 완료)</code></pre><h3 id="big-o의-특성">Big-O의 특성</h3>
<p><strong>1. 상수 계수 무시</strong></p>
<pre><code>f(n) = 5n → O(n)
f(n) = 100n → O(n)
f(n) = 0.001n → O(n)

모두 같음!</code></pre><p><strong>2. 낮은 차수 항 무시</strong></p>
<pre><code>f(n) = n² + n → O(n²)
f(n) = n² + 100n + 1000 → O(n²)

n²이 지배적이므로 나머지 무시</code></pre><p><strong>3. 덧셈 규칙</strong></p>
<pre><code>f(n) = O(f₁(n))
g(n) = O(g₁(n))

→ f(n) + g(n) = O(max(f₁(n), g₁(n)))

예:
O(n) + O(n²) = O(n²)
O(n log n) + O(n) = O(n log n)</code></pre><p><strong>4. 곱셈 규칙</strong></p>
<pre><code>f(n) = O(f₁(n))
g(n) = O(g₁(n))

→ f(n) · g(n) = O(f₁(n) · g₁(n))

예:
O(n) · O(n) = O(n²)
O(log n) · O(n) = O(n log n)</code></pre><hr>
<h2 id="📉-big-ω-표기법-하한">📉 Big-Ω 표기법 (하한)</h2>
<h3 id="big-ω-빅-오메가의-정의">Big-Ω (빅 오메가)의 정의</h3>
<p><strong>수학적 정의</strong>:</p>
<pre><code>f(n) = Ω(g(n))

의미:
n이 충분히 클 때, f(n)은 g(n)의 상수 배 이상이다

엄밀한 정의:
f(n) = Ω(g(n)) ⟺
∃ c &gt; 0, ∃ n₀ &gt; 0 such that
∀ n ≥ n₀, f(n) ≥ c · g(n)

읽는 법:
&quot;양의 상수 c와 n₀가 존재하여,
모든 n ≥ n₀에 대해
f(n) ≥ c · g(n)이 성립한다&quot;</code></pre><p><strong>직관적 이해</strong>:</p>
<pre><code>f(n) = Ω(g(n))

= &quot;f(n)은 최선의 경우에도 g(n)에 비례한다&quot;
= &quot;f(n)은 g(n)보다 느리거나 같다&quot;
= &quot;g(n)은 f(n)의 하한(lower bound)&quot;

예:
f(n) = 3n² + 2n + 1 = Ω(n²)

의미:
n이 충분히 크면
3n² + 2n + 1 ≥ c · n² (적당한 c에 대해)</code></pre><h3 id="big-ω-증명-예시">Big-Ω 증명 예시</h3>
<p><strong>예제</strong>: f(n) = 3n² + 2n은 Ω(n²)임을 증명하라.</p>
<pre><code>증명:
f(n) = 3n² + 2n

목표: f(n) ≥ c · n²을 만족하는 c, n₀ 찾기

시도:
3n² + 2n ≥ c · n²
2n ≥ (c - 3)n²

n이 크면 2n보다 (c-3)n²이 더 큼 (c &lt; 3이면)
따라서 c = 3, n₀ = 1이면 성립

검증:
n ≥ 1일 때
3n² + 2n ≥ 3n²
2n ≥ 0 ✓

∴ f(n) = Ω(n²) (증명 완료)</code></pre><h3 id="big-ω의-활용">Big-Ω의 활용</h3>
<p><strong>최선의 경우 분석</strong>:</p>
<pre><code class="language-python">def linear_search(arr, target):
    &quot;&quot;&quot;
    최선: Ω(1) - 첫 번째에 있음
    최악: O(n) - 마지막 또는 없음
    &quot;&quot;&quot;
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1

# 최선의 경우: arr[0] == target
# → 1번 비교 → Ω(1)</code></pre>
<p><strong>알고리즘의 하한</strong>:</p>
<pre><code>비교 기반 정렬:
&quot;어떤 비교 기반 정렬도 최소 Ω(n log n)&quot;

증명 (결정 트리):
- n개 원소의 순열: n! 가지
- 비교 결과로 구분: 2^h ≥ n!
- 트리 높이: h ≥ log₂(n!)
- Stirling 근사: log₂(n!) ≈ n log₂ n
- ∴ 최소 Ω(n log n) 비교 필요

→ 병합 정렬, 퀵 정렬이 최적!</code></pre><hr>
<h2 id="🎯-big-θ-표기법-정확한-차수">🎯 Big-Θ 표기법 (정확한 차수)</h2>
<h3 id="big-θ-빅-세타의-정의">Big-Θ (빅 세타)의 정의</h3>
<p><strong>수학적 정의</strong>:</p>
<pre><code>f(n) = Θ(g(n))

의미:
f(n) = O(g(n)) AND f(n) = Ω(g(n))  즉, 상한과 하한이 같음

엄밀한 정의:
f(n) = Θ(g(n)) ⟺
∃ c₁, c₂ &gt; 0, ∃ n₀ &gt; 0 such that
∀ n ≥ n₀, c₁ · g(n) ≤ f(n) ≤ c₂ · g(n)

읽는 법:
&quot;양의 상수 c₁, c₂와 n₀가 존재하여,
모든 n ≥ n₀에 대해
c₁ · g(n) ≤ f(n) ≤ c₂ · g(n)이 성립한다&quot;</code></pre><p><strong>직관적 이해</strong>:</p>
<pre><code>f(n) = Θ(g(n))

= &quot;f(n)은 정확히 g(n)에 비례한다&quot;
= &quot;f(n)과 g(n)은 같은 증가율&quot;
= &quot;최선/최악 모두 g(n)&quot;

시각화:
      c₂ · g(n) ← 상한
         ↑
      f(n) ← 실제 함수
         ↓
      c₁ · g(n) ← 하한

f(n)이 두 경계 사이에 끼임</code></pre><h3 id="big-θ-증명-예시">Big-Θ 증명 예시</h3>
<p><strong>예제</strong>: f(n) = 3n² + 2n은 Θ(n²)임을 증명하라.</p>
<pre><code>증명:
f(n) = 3n² + 2n

방법 1: O와 Ω를 각각 증명

1) f(n) = O(n²) 증명:
   3n² + 2n ≤ 5n² (n ≥ 1일 때)
   → c₂ = 5, n₀ = 1

2) f(n) = Ω(n²) 증명:
   3n² + 2n ≥ 3n² (항상 성립)
   → c₁ = 3, n₀ = 1

∴ 3n² ≤ 3n² + 2n ≤ 5n² (n ≥ 1)
∴ f(n) = Θ(n²) (증명 완료)


방법 2: 직접 증명

목표: c₁ · n² ≤ 3n² + 2n ≤ c₂ · n²

하한:
3n² + 2n ≥ 3n² (항상 성립)
→ c₁ = 3

상한:
3n² + 2n ≤ 3n² + 2n²  (n ≥ 1일 때)
         = 5n²
→ c₂ = 5

∴ c₁ = 3, c₂ = 5, n₀ = 1로
   3n² ≤ 3n² + 2n ≤ 5n² (n ≥ 1)

∴ f(n) = Θ(n²) (증명 완료)</code></pre><h3 id="big-θ의-활용">Big-Θ의 활용</h3>
<p><strong>정확한 복잡도 표현</strong>:</p>
<pre><code class="language-python">def sum_array(arr):
    &quot;&quot;&quot;
    Θ(n) - 최선/평균/최악 모두 n번
    &quot;&quot;&quot;
    total = 0
    for x in arr:  # 정확히 n번
        total += x
    return total

def bubble_sort(arr):
    &quot;&quot;&quot;
    최선: Ω(n) - 이미 정렬됨
    최악: O(n²) - 역순
    평균: Θ(n²)

    Big-Θ로 표현 불가 (최선≠최악)
    &quot;&quot;&quot;
    # ...

def merge_sort(arr):
    &quot;&quot;&quot;
    Θ(n log n) - 항상 같은 복잡도
    &quot;&quot;&quot;
    # ...</code></pre>
<hr>
<h2 id="📊-표기법-비교">📊 표기법 비교</h2>
<h3 id="관계-정리">관계 정리</h3>
<pre><code>관계:
f(n) = Θ(g(n)) ⟺ f(n) = O(g(n)) AND f(n) = Ω(g(n))

벤 다이어그램:
        O(g(n))
    ┌─────────────┐
    │            │
    │   Θ(g(n))  │ ← 교집합
    │            │
    └─────────────┘
        Ω(g(n))</code></pre><p><strong>예시</strong>:</p>
<pre><code>f(n) = 3n² + 2n

Big-O:
f(n) = O(n²) ✓
f(n) = O(n³) ✓ (더 느슨한 상한)
f(n) = O(n⁴) ✓
f(n) = O(n) ✗ (틀림)

Big-Ω:
f(n) = Ω(n²) ✓
f(n) = Ω(n) ✓ (더 느슨한 하한)
f(n) = Ω(log n) ✓
f(n) = Ω(n³) ✗ (틀림)

Big-Θ:
f(n) = Θ(n²) ✓ (정확!)
f(n) = Θ(n) ✗
f(n) = Θ(n³) ✗</code></pre><h3 id="언제-어떤-표기법">언제 어떤 표기법?</h3>
<pre><code>Big-O (가장 많이 사용):
- 최악의 경우 성능
- 알고리즘 비교
- 성능 보장

예: &quot;이 알고리즘은 O(n log n)입니다&quot;

Big-Ω:
- 최선의 경우
- 문제의 하한
- 알고리즘 최적성 증명

예: &quot;비교 정렬은 최소 Ω(n log n)입니다&quot;

Big-Θ:
- 정확한 복잡도
- 모든 경우가 같을 때
- 이론적 분석

예: &quot;병합 정렬은 Θ(n log n)입니다&quot;</code></pre><hr>
<h2 id="📐-기타-표기법">📐 기타 표기법</h2>
<h3 id="little-o-작은-오">Little-o (작은 오)</h3>
<pre><code>f(n) = o(g(n))

의미:
f(n)은 g(n)보다 &quot;엄격히&quot; 느리게 증가

정의:
lim(n→∞) f(n)/g(n) = 0

예:
n = o(n²) ✓
n² = o(n²) ✗ (같으므로)
n log n = o(n²) ✓

차이:
Big-O: f(n) ≤ c · g(n) (이하)
little-o: f(n) &lt; g(n) (미만, 극한에서)</code></pre><h3 id="little-ω-작은-오메가">Little-ω (작은 오메가)</h3>
<pre><code>f(n) = ω(g(n))

의미:
f(n)은 g(n)보다 &quot;엄격히&quot; 빠르게 증가

정의:
lim(n→∞) f(n)/g(n) = ∞

예:
n² = ω(n) ✓
n² = ω(n²) ✗ (같으므로)
2ⁿ = ω(n²) ✓

차이:
Big-Ω: f(n) ≥ c · g(n) (이상)
little-ω: f(n) &gt; g(n) (초과, 극한에서)</code></pre><hr>
<h2 id="🔍-복잡도-함수-순서">🔍 복잡도 함수 순서</h2>
<h3 id="주요-복잡도-순서">주요 복잡도 순서</h3>
<pre><code>느림 ←──────────────────────────────→ 빠름

O(n!) &gt; O(2ⁿ) &gt; O(n³) &gt; O(n²) &gt; O(n log n) &gt; O(n) &gt; O(log n) &gt; O(1)

더 정확히:
O(1)
  &lt; O(log log n)
  &lt; O(log n)
  &lt; O((log n)²)
  &lt; O(√n)
  &lt; O(n)
  &lt; O(n log n)
  &lt; O(n²)
  &lt; O(n³)
  &lt; O(2ⁿ)
  &lt; O(n!)
  &lt; O(nⁿ)</code></pre><h3 id="극한을-이용한-비교">극한을 이용한 비교</h3>
<pre><code>f(n)과 g(n)의 비교:

lim(n→∞) f(n)/g(n) = L이면

L = 0:    f(n) = o(g(n))      f가 훨씬 느림
0 &lt; L &lt; ∞: f(n) = Θ(g(n))     같은 차수
L = ∞:    f(n) = ω(g(n))      f가 훨씬 빠름

예:
lim(n→∞) n/n² = lim 1/n = 0
→ n = o(n²)

lim(n→∞) 3n²/n² = 3
→ 3n² = Θ(n²)

lim(n→∞) 2ⁿ/n² = ∞
→ 2ⁿ = ω(n²)</code></pre><hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<p><strong>표기법 남용 피하기</strong></p>
<pre><code class="language-python"># 잘못된 표현
def algorithm(n):
    # &quot;이 알고리즘은 O(n)이다&quot; ✗
    # O(n)은 집합이지 값이 아님
    pass

# 올바른 표현
# &quot;이 알고리즘의 시간복잡도는 O(n)이다&quot; ✓
# &quot;이 알고리즘은 O(n) 시간에 실행된다&quot; ✓</code></pre>
<p><strong>정확한 분석</strong></p>
<pre><code class="language-python">def example(n):
    # 잘못된 분석
    # &quot;O(n) + O(n) = O(2n) = O(n)&quot; ✗
    # 중간 단계 불필요

    # 올바른 분석
    # &quot;O(n) + O(n) = O(n)&quot; ✓

    for i in range(n):
        pass  # O(n)

    for i in range(n):
        pass  # O(n)

    # 총: O(n)</code></pre>
<p><strong>최선/평균/최악 명확히</strong></p>
<pre><code class="language-python">def binary_search(arr, target):
    &quot;&quot;&quot;
    최선: Θ(1) - 중간에 있음
    평균: Θ(log n)
    최악: Θ(log n) - 없음

    보통 최악으로 표현: O(log n)
    &quot;&quot;&quot;
    # ...</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>점근적 표기법의 본질</strong></p>
<ul>
<li>입력이 클 때의 증가율</li>
<li>하드웨어 독립적</li>
<li>알고리즘의 본질적 효율성</li>
</ul>
<p><strong>세 가지 주요 표기법</strong></p>
<pre><code>표기법    의미                 용도
--------------------------------------------------
Big-O    상한 (≤)            최악의 경우
Big-Ω    하한 (≥)            최선의 경우
Big-Θ    정확한 차수 (=)    모든 경우 같을 때</code></pre><p><strong>수학적 정의</strong></p>
<pre><code>Big-O:
f(n) = O(g(n)) ⟺
∃ c, n₀ : ∀n ≥ n₀, f(n) ≤ c·g(n)

Big-Ω:
f(n) = Ω(g(n)) ⟺
∃ c, n₀ : ∀n ≥ n₀, f(n) ≥ c·g(n)

Big-Θ:
f(n) = Θ(g(n)) ⟺
f(n) = O(g(n)) AND f(n) = Ω(g(n))</code></pre><p><strong>복잡도 순서</strong></p>
<pre><code>O(1) &lt; O(log n) &lt; O(n) &lt; O(n log n) &lt; O(n²) &lt; O(2ⁿ) &lt; O(n!)</code></pre><p><strong>실무 지침</strong></p>
<pre><code>일반적으로 Big-O 사용:
- 최악의 경우 보장
- 가장 보수적

Big-Θ 사용:
- 정확한 분석 필요 시
- 이론적 연구</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[07-04] 최선/평균/최악 경우 분석</strong></p>
<ul>
<li>입력에 따른 성능 차이: 같은 알고리즘도 입력에 따라 다른 성능</li>
<li>최선의 경우 분석: 가장 좋은 입력 패턴과 성능</li>
<li>평균 경우 분석: 확률적 분석과 기댓값 계산</li>
<li>최악의 경우 분석: 성능 보장과 실무에서의 중요성</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[07-02] 공간 복잡도</a><br><strong>다음 글</strong>: <a href="#">[07-04] 최선/평균/최악 경우 분석</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [07-02] 공간 복잡도 (Space Complexity)]]></title>
            <link>https://velog.io/@road_to_ai/07-02-%EA%B3%B5%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%84-Space-Complexity</link>
            <guid>https://velog.io/@road_to_ai/07-02-%EA%B3%B5%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%84-Space-Complexity</guid>
            <pubDate>Fri, 27 Feb 2026 14:39:44 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>공간 복잡도는 알고리즘이 실행되는 동안 사용하는 메모리 공간의 양을 나타내는 척도입니다.</p>
</blockquote>
<hr>
<h2 id="🎯-공간-복잡도란-무엇인가">🎯 공간 복잡도란 무엇인가</h2>
<h3 id="공간-복잡도의-기본-개념">공간 복잡도의 기본 개념</h3>
<p>시간 복잡도가 &quot;얼마나 빠른가&quot;를 측정한다면, <strong>공간 복잡도(Space Complexity)</strong>는 &quot;얼마나 많은 메모리를 사용하는가&quot;를 측정합니다.</p>
<p><strong>실생활 비유</strong>:</p>
<pre><code>여행 짐 싸기:

빠른 방법 (시간 중시):
- 옷을 모두 펼쳐서 보관
- 빨리 찾지만 공간 많이 차지

효율적인 방법 (공간 중시):
- 옷을 돌돌 말아서 보관
- 찾는데 시간 걸리지만 공간 절약

→ 시간 vs 공간 트레이드오프!</code></pre><p><strong>또 다른 예시</strong>:</p>
<pre><code>책상 위 작업:

큰 책상 (메모리 많음):
- 모든 자료를 펼쳐 놓고 작업
- 빠르지만 공간 많이 필요

작은 책상 (메모리 적음):
- 필요한 것만 꺼내서 작업
- 느리지만 공간 절약</code></pre><h3 id="왜-공간-복잡도가-중요한가">왜 공간 복잡도가 중요한가?</h3>
<p><strong>1. 메모리 제한</strong></p>
<pre><code>임베디드 시스템:
- 스마트워치: 512MB RAM
- IoT 센서: 64KB RAM
→ 메모리 효율이 생명!

모바일 앱:
- 메모리 많이 쓰면 강제 종료
- 배터리 소모 증가

서버:
- 동시 접속자 1만 명
- 각자 메모리 사용
→ 공간 복잡도가 비용!</code></pre><p><strong>2. 실행 가능여부 판단</strong></p>
<pre><code>문제: 10억 개 데이터 처리

O(1) 공간: 가능!
O(n) 공간: 4GB 필요 (가능)
O(n²) 공간: 4,000,000GB 필요 (불가능!)

→ 시간은 되지만 공간이 안 되는 경우!</code></pre><p><strong>3. 성능 영향</strong></p>
<pre><code>캐시 미스:
- 메모리 많이 쓰면 캐시 효율 떨어짐
- 실제 속도 저하

스와핑:
- 메모리 부족 시 디스크 사용
- 속도 100배 이상 느려짐</code></pre><h3 id="공간-복잡도의-구성-요소">공간 복잡도의 구성 요소</h3>
<p>알고리즘이 사용하는 총 메모리는 다음과 같이 나뉩니다:</p>
<pre><code>총 공간 = 고정 공간 + 가변 공간

1. 고정 공간 (Fixed Space):
   - 입력 크기와 무관한 공간
   - 코드, 상수, 변수 등

2. 가변 공간 (Variable Space):
   - 입력 크기에 따라 변하는 공간
   - 동적 할당, 재귀 스택 등

공간 복잡도 = 주로 가변 공간만 측정</code></pre><hr>
<h2 id="📊-공간-복잡도-분석">📊 공간 복잡도 분석</h2>
<h3 id="o1---상수-공간">O(1) - 상수 공간</h3>
<p><strong>정의</strong>: 입력 크기와 무관하게 일정한 메모리 사용</p>
<pre><code class="language-python">def sum_array(arr):
    &quot;&quot;&quot;
    O(1) 공간

    사용 메모리:
    - total: 하나의 정수 (4바이트)
    - i: 인덱스 (4바이트)

    입력 크기와 무관!
    &quot;&quot;&quot;
    total = 0  # 상수 공간
    for i in range(len(arr)):
        total += arr[i]
    return total

# arr 크기가 100이든 1,000,000이든
# 추가 메모리 사용량은 같음! = 입력이 커져도 메모리를 더 쓰지 않는다!</code></pre>
<p><strong>특징</strong>:</p>
<pre><code>장점:
- 메모리 효율 최고
- 대용량 데이터 처리 가능

예시:
- 반복문으로 최댓값 찾기
- 제자리 정렬 (버블, 삽입)
- 이진 탐색 (반복 버전)</code></pre><h3 id="olog-n---로그-공간">O(log n) - 로그 공간</h3>
<p><strong>정의</strong>: 입력이 절반씩 줄어들 때 필요한 공간</p>
<pre><code class="language-python">def binary_search_recursive(arr, target, left, right):
    &quot;&quot;&quot;
    O(log n) 공간 - 재귀 호출 스택

    재귀 깊이: log n
    각 호출마다 스택 프레임 생성

    총 공간: O(log n)
    &quot;&quot;&quot;
    if left &gt; right:
        return -1

    mid = (left + right) // 2

    if arr[mid] == target:
        return mid
    elif arr[mid] &lt; target:
        # 재귀 호출 → 스택 사용
        return binary_search_recursive(arr, target, mid + 1, right)
    else:
        return binary_search_recursive(arr, target, left, mid - 1)

# 호출 스택:
# binary_search(0, 1000)
#   → binary_search(500, 1000)
#     → binary_search(750, 1000)
#       → ...
# 최대 log₂(1000) ≈ 10번 중첩</code></pre>
<p><strong>특징</strong>:</p>
<pre><code>발생 상황:
- 재귀적 이진 탐색
- 분할 정복 (병합 정렬의 재귀)
- 균형 이진 트리 탐색

주의:
- 반복문으로 바꾸면 O(1) 가능!</code></pre><h3 id="on---선형-공간">O(n) - 선형 공간</h3>
<p><strong>정의</strong>: 입력 크기에 비례하는 메모리 사용</p>
<pre><code class="language-python">def copy_array(arr):
    &quot;&quot;&quot;
    O(n) 공간

    새 배열 생성: n개 원소
    &quot;&quot;&quot;
    new_arr = []
    for item in arr:
        new_arr.append(item)
    return new_arr

def fibonacci_memo(n):
    &quot;&quot;&quot;
    O(n) 공간 - 메모이제이션

    memo 배열: n+1개 원소 저장
    &quot;&quot;&quot;
    memo = [0] * (n + 1)
    memo[1] = 1

    for i in range(2, n + 1):
        memo[i] = memo[i-1] + memo[i-2]

    return memo[n]

def merge_sort(arr):
    &quot;&quot;&quot;
    O(n) 공간 - 병합 과정에서 임시 배열

    분할: 공간 사용 없음
    병합: 임시 배열 필요
    &quot;&quot;&quot;
    if len(arr) &lt;= 1:
        return arr

    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])

    # 병합 시 새 배열 생성 (O(n))
    return merge(left, right)</code></pre>
<p><strong>특징</strong>:</p>
<pre><code>발생 상황:
- 배열 복사
- 동적 계획법 (DP 테이블)
- 해시 테이블
- 그래프 인접 리스트

일반적:
- 가장 흔한 공간 복잡도
- 대부분 허용 가능</code></pre><h3 id="on²---제곱-공간">O(n²) - 제곱 공간</h3>
<p><strong>정의</strong>: 입력 크기의 제곱에 비례</p>
<pre><code class="language-python">def adjacency_matrix(n):
    &quot;&quot;&quot;
    O(n²) 공간 - 인접 행렬

    n×n 2차원 배열
    &quot;&quot;&quot;
    matrix = [[0] * n for _ in range(n)]
    return matrix

def floyd_warshall(graph):  # floyd_warshall 은 그래프에서 &#39;모든 정점 쌍 사이의 최단 경로&#39;를 구하는 알고리즘
    &quot;&quot;&quot;
    O(n²) 공간 - 모든 쌍 최단 경로

    dist[i][j]: i에서 j로의 최단 거리
    n×n 배열
    &quot;&quot;&quot;
    n = len(graph)
    dist = [[float(&#39;inf&#39;)] * n for _ in range(n)]

    # 초기화 및 계산...

    return dist

def all_pairs_distance(points): # all_pairs_distance 는 주어진 점(points)들 사이의 모든 거리 조합을 계산하는 함수
    &quot;&quot;&quot;
    O(n²) 공간 - 모든 점 쌍의 거리

    n개 점 → n(n-1)/2 쌍
    &quot;&quot;&quot;
    n = len(points)
    distances = [[0] * n for _ in range(n)]

    for i in range(n):
        for j in range(n):
            distances[i][j] = distance(points[i], points[j])

    return distances</code></pre>
<p><strong>특징</strong>:</p>
<pre><code>발생 상황:
- 2차원 DP 테이블
- 그래프 인접 행렬
- 모든 쌍 문제

문제:
- 메모리 많이 사용
- n=10,000이면 400MB
- n=100,000이면 40GB (불가능!)</code></pre><hr>
<h2 id="⚖️-시간-공간-트레이드오프">⚖️ 시간-공간 트레이드오프</h2>
<h3 id="트레이드오프란">트레이드오프란?</h3>
<p><strong>시간을 줄이려면 공간이 필요하고, 공간을 줄이려면 시간이 필요합니다.</strong></p>
<p><strong>예시 1: 피보나치 수열</strong></p>
<pre><code class="language-python"># 방법 1: 순수 재귀 (공간 절약, 시간 낭비)
def fib_recursive(n):
    &quot;&quot;&quot;
    시간: O(2^n) - 매우 느림!
    공간: O(n) - 재귀 스택만
    &quot;&quot;&quot;
    if n &lt;= 1:
        return n
    return fib_recursive(n-1) + fib_recursive(n-2)

# 방법 2: 메모이제이션 (시간 절약, 공간 사용)
def fib_memo(n, memo={}):
    &quot;&quot;&quot;
    시간: O(n) - 빠름!
    공간: O(n) - memo 딕셔너리
    &quot;&quot;&quot;
    if n in memo:
        return memo[n]
    if n &lt;= 1:
        return n

    memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
    return memo[n]

# 방법 3: 반복 (시간 절약, 공간 최소)
def fib_iterative(n):
    &quot;&quot;&quot;
    시간: O(n) - 빠름!
    공간: O(1) - 변수 2개만

    최적!
    &quot;&quot;&quot;
    if n &lt;= 1:
        return n

    prev2, prev1 = 0, 1
    for _ in range(2, n + 1):
        current = prev1 + prev2
        prev2 = prev1
        prev1 = current

    return prev1

# 비교:
# n=40일 때
# 재귀: 수십 초, 40번 재귀
# 메모: 0.001초, 40개 저장
# 반복: 0.001초, 2개 변수만</code></pre>
<p><strong>예시 2: 최단 경로</strong></p>
<pre><code class="language-python"># 방법 1: 모든 경로 저장 (공간 많이)
def all_paths_bfs(graph, start):
    &quot;&quot;&quot;
    시간: O(V + E)
    공간: O(V²) - 모든 경로 저장
    &quot;&quot;&quot;
    paths = {node: [] for node in graph}
    # 모든 경로 기록...

# 방법 2: 거리만 저장 (공간 절약)
def shortest_distance_bfs(graph, start):
    &quot;&quot;&quot;
    시간: O(V + E)
    공간: O(V) - 거리만 저장

    경로는 필요 시 재구성
    &quot;&quot;&quot;
    distance = {node: float(&#39;inf&#39;) for node in graph}
    # 거리만 기록...</code></pre>
<h3 id="트레이드오프-선택-전략">트레이드오프 선택 전략</h3>
<pre><code>상황별 선택:

메모리가 제한적:
→ 시간 희생, 공간 절약
→ 재계산, 스트리밍 처리

시간이 중요:
→ 메모리 사용, 시간 절약
→ 캐싱, 메모이제이션

균형 잡힌 접근:
→ 최적의 알고리즘 선택
→ 피보나치 반복 버전</code></pre><hr>
<h2 id="🔄-재귀의-공간-복잡도">🔄 재귀의 공간 복잡도</h2>
<h3 id="재귀-호출-스택">재귀 호출 스택</h3>
<p>재귀 함수는 <strong>호출 스택</strong>에 메모리를 사용합니다.</p>
<pre><code class="language-python">def factorial_recursive(n):
    &quot;&quot;&quot;
    공간 복잡도: O(n)

    호출 스택:
    factorial(5)
      → factorial(4)
        → factorial(3)
          → factorial(2)
            → factorial(1)

    최대 n개 호출이 스택에 쌓임
    &quot;&quot;&quot;
    if n &lt;= 1:
        return 1
    return n * factorial_recursive(n - 1)

def factorial_iterative(n):
    &quot;&quot;&quot;
    공간 복잡도: O(1)

    반복문 사용 → 스택 사용 없음
    &quot;&quot;&quot;
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

# 공간 비교:
# n=10000일 때
# 재귀: 10000번 스택 프레임 (스택 오버플로우(Stack Overflow)!)
# 반복: 변수 2개만</code></pre>
<h3 id="재귀-깊이-문제">재귀 깊이 문제</h3>
<pre><code class="language-python">import sys

# Python 기본 재귀 호출의 횟수(깊이) 제한(Recursion Limit): 약 1000
print(sys.getrecursionlimit())  # 1000

# 깊은 재귀 시도
def deep_recursion(n):
    if n == 0:
        return 0
    return deep_recursion(n - 1)

try:
    deep_recursion(2000)  # RecursionError!
except RecursionError:
    print(&quot;스택 오버플로우!&quot;)

# 해결 방법 1: 재귀 깊이 증가 (비추천)
sys.setrecursionlimit(10000)

# 해결 방법 2: 반복문 사용 (추천)
def iterative_solution(n):
    result = 0
    for i in range(n):
        result += i
    return result</code></pre>
<h3 id="꼬리-재귀-최적화-tail-call-optimization-tco">꼬리 재귀 최적화 (Tail Call Optimization, TCO)</h3>
<pre><code class="language-python"># 일반 재귀 (O(n) 공간)
def sum_recursive(n):
    &quot;&quot;&quot;
    호출 스택에 n개 쌓임
    &quot;&quot;&quot;
    if n == 0:
        return 0
    return n + sum_recursive(n - 1)

# 꼬리 재귀 (O(n) 공간, 이론상 O(1) 가능)
def sum_tail_recursive(n, acc=0):
    &quot;&quot;&quot;
    마지막에 재귀 호출만   * 메모지를 계속 새로 쓰는 게 아니라, 기존 메모지의 내용을 지우고 새로 쓰는 것과 같음

    Python은 꼬리 재귀 최적화 X
    → 여전히 O(n) 공간

    하지만 다른 언어(Scala, Scheme)에서는
    O(1) 공간으로 최적화됨
    &quot;&quot;&quot;
    if n == 0:
        return acc
    return sum_tail_recursive(n - 1, acc + n)

# Python에서는 그냥 반복문 쓰기!
def sum_iterative(n):
    &quot;&quot;&quot;
    O(1) 공간
    &quot;&quot;&quot;
    acc = 0
    for i in range(1, n + 1):
        acc += i
    return acc</code></pre>
<hr>
<h2 id="🎛️-제자리-알고리즘">🎛️ 제자리 알고리즘</h2>
<h3 id="제자리-알고리즘이란">제자리 알고리즘이란?</h3>
<p><strong>In-Place Algorithm</strong>: 입력 외에 추가 공간을 거의 사용하지 않는 알고리즘</p>
<pre><code class="language-python"># 제자리 정렬 (O(1) 공간)
def bubble_sort_inplace(arr):
    &quot;&quot;&quot;
    추가 배열 없이 원본 수정

    공간: O(1)
    &quot;&quot;&quot;
    n = len(arr)
    for i in range(n - 1):
        for j in range(n - 1 - i):
            if arr[j] &gt; arr[j + 1]:
                # 제자리 교환
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

# 제자리가 아닌 정렬 (O(n) 공간)
def merge_sort(arr):
    &quot;&quot;&quot;
    병합 시 새 배열 생성

    공간: O(n)
    &quot;&quot;&quot;
    if len(arr) &lt;= 1:
        return arr

    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])

    # 새 배열 생성!
    return merge(left, right)</code></pre>
<h3 id="제자리-알고리즘-예시">제자리 알고리즘 예시</h3>
<pre><code class="language-python"># 1. 배열 뒤집기
def reverse_inplace(arr):
    &quot;&quot;&quot;
    O(1) 공간 - 양 끝에서 교환
    &quot;&quot;&quot;
    left, right = 0, len(arr) - 1
    while left &lt; right:
        arr[left], arr[right] = arr[right], arr[left]
        left += 1
        right -= 1

# 2. 중복 제거 (정렬된 배열)
def remove_duplicates_inplace(arr):
    &quot;&quot;&quot;
    O(1) 공간 - 투 포인터
    &quot;&quot;&quot;
    if not arr:
        return 0

    write = 1
    for read in range(1, len(arr)):
        if arr[read] != arr[read - 1]:
            arr[write] = arr[read]
            write += 1

    return write  # 새 길이

# 3. 0을 뒤로 이동
def move_zeros_inplace(arr):
    &quot;&quot;&quot;
    O(1) 공간
    &quot;&quot;&quot;
    write = 0
    for read in range(len(arr)):
        if arr[read] != 0:
            arr[write] = arr[read]
            write += 1

    # 나머지를 0으로
    while write &lt; len(arr):
        arr[write] = 0
        write += 1</code></pre>
<hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<p><strong>공간 복잡도 줄이기</strong></p>
<pre><code class="language-python"># 나쁜 예: 불필요한 복사
def process_data_bad(data):
    # 매번 복사 (O(n) 공간)
    copied = data.copy()
    result = []
    for item in copied:
        result.append(item * 2)
    return result

# 좋은 예: 제자리 처리
def process_data_good(data):
    # 제자리 수정 (O(1) 공간)
    for i in range(len(data)):
        data[i] *= 2
    return data

# 또는 제너레이터 사용 (O(1) 공간)
def process_data_generator(data):
    for item in data:
        yield item * 2  # yield : 값을 반환하고 그 자리에 일시정지함
                        #         주로 대용량 텍스트 파일을 한 줄씩 읽거나, 끝이 없는 무한한 데이터를 다룰 때 필수적으로 사용</code></pre>
<p><strong>메모리 프로파일링(Memory Profiling)</strong>: 프로그램이 실행되는 동안 메모리를 어디서, 얼마나, 왜 사용하는지 정밀하게 분석하는 과정</p>
<pre><code class="language-python">import tracemalloc

# 메모리 사용량 측정
tracemalloc.start()

# 알고리즘 실행
data = [i for i in range(1000000)]

current, peak = tracemalloc.get_traced_memory()
print(f&quot;현재: {current / 10**6:.2f}MB&quot;)
print(f&quot;최대: {peak / 10**6:.2f}MB&quot;)

tracemalloc.stop()</code></pre>
<p><strong>큰 파일 처리</strong></p>
<pre><code class="language-python"># 나쁜 예: 전체 파일 메모리에 로드
def process_file_bad(filename):
    # O(파일 크기) 공간
    with open(filename) as f:
        data = f.read()  # 전체 로드!
        # 처리...

# 좋은 예: 스트리밍 처리
def process_file_good(filename):
    # O(1) 공간
    with open(filename) as f:
        for line in f:  # 한 줄씩
            # 처리...
            pass</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>공간 복잡도의 본질</strong></p>
<ul>
<li>알고리즘이 사용하는 메모리 양</li>
<li>입력 크기에 따른 메모리 증가율</li>
<li>시간 복잡도와 함께 고려</li>
</ul>
<p><strong>주요 공간 복잡도</strong></p>
<pre><code>복잡도      예시
--------------------------------------------------
O(1)       변수 몇 개, 제자리 정렬
O(log n)   재귀 이진 탐색, 재귀 병합 정렬
O(n)       배열 복사, DP 테이블, 해시 테이블
O(n²)      2차원 배열, 인접 행렬</code></pre><p><strong>시간-공간 트레이드오프</strong></p>
<pre><code>피보나치:
- 순수 재귀: 시간 O(2^n), 공간 O(n)
- 메모이제이션: 시간 O(n), 공간 O(n)
- 반복: 시간 O(n), 공간 O(1) ← 최적!</code></pre><p><strong>재귀와 공간</strong></p>
<pre><code>재귀 호출:
- 호출 스택에 메모리 사용
- 깊이 n이면 O(n) 공간
- Python 최대 깊이: 약 1000

해결:
- 반복문으로 변환
- 꼬리 재귀 (Python 지원 X)</code></pre><p><strong>제자리 알고리즘</strong></p>
<pre><code>In-Place:
- 입력 외 추가 공간 최소
- O(1) 공간
- 버블/삽입 정렬, 배열 뒤집기</code></pre><p><strong>실무 선택 기준</strong></p>
<pre><code>메모리 제한적 (임베디드, IoT):
→ 공간 복잡도 우선

성능 중요 (서버, 앱):
→ 시간 복잡도 우선, 캐싱 활용

균형:
→ 최적 알고리즘 선택</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[07-03] 점근적 표기법 (Asymptotic Notation)</strong></p>
<ul>
<li>Big-O의 수학적 정의: 상한을 나타내는 엄밀한 수학적 의미</li>
<li>Big-Ω (오메가): 알고리즘의 하한, 최선의 경우</li>
<li>Big-Θ (세타): 상한과 하한이 같을 때의 정확한 복잡도</li>
<li>증명 방법: 점근적 표기법의 수학적 증명 기법</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[07-01] 시간 복잡도</a><br><strong>다음 글</strong>: <a href="#">[07-03] 점근적 표기법</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [07-01] 시간 복잡도 (Time Complexity)]]></title>
            <link>https://velog.io/@road_to_ai/07-01-%EC%8B%9C%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%84-Time-Complexity</link>
            <guid>https://velog.io/@road_to_ai/07-01-%EC%8B%9C%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%84-Time-Complexity</guid>
            <pubDate>Fri, 27 Feb 2026 07:32:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>시간 복잡도는 알고리즘의 효율성을 수학적으로 표현하는 방법으로, 입력 크기에 따른 실행 시간의 증가율을 나타냅니다.</p>
</blockquote>
<hr>
<p>시간 복잡도를 알아보기 전에 알고리즘 분석에 대해 먼저 살펴보겠습니다.</p>
<h1 id="알고리즘-분석-algorithm-analysis">알고리즘 분석 (Algorithm Analysis)</h1>
<blockquote>
<p>알고리즘 분석은 알고리즘의 효율성을 수학적으로 평가하고 비교하는 방법입니다.</p>
</blockquote>
<blockquote>
<p>좋은 알고리즘을 만드는 것도 중요하지만, 그것이 얼마나 효율적인지 측정하고 개선하는 것도 똑같이 중요합니다.</p>
</blockquote>
<hr>
<h2 id="🎯-알고리즘-분석이란">🎯 알고리즘 분석이란?</h2>
<h3 id="왜-알고리즘을-분석하는가">왜 알고리즘을 분석하는가?</h3>
<p>지금까지 다양한 알고리즘 설계 기법을 배웠습니다. 이제는 만든 알고리즘이 <strong>얼마나 좋은지</strong> 평가할 차례입니다.</p>
<p><strong>실생활 비유</strong>:</p>
<pre><code>자동차를 만들었다면:
- 빠르기는? (성능)
- 연료는 얼마나? (효율성)
- 공간은 얼마나? (크기)

알고리즘도 마찬가지:
- 얼마나 빠른가? → 시간 복잡도
- 메모리는 얼마나? → 공간 복잡도
- 최악의 경우는? → 최악/평균/최선 분석</code></pre><p><strong>두 가지 핵심 질문</strong>:</p>
<pre><code>1. 이 알고리즘은 얼마나 빠른가?  →  시간 복잡도 (Time Complexity)

2. 이 알고리즘은 메모리를 얼마나 쓰는가?  → 공간 복잡도 (Space Complexity)</code></pre><h3 id="알고리즘-분석의-목적">알고리즘 분석의 목적</h3>
<p><strong>1. 알고리즘 비교</strong></p>
<pre><code>문제: 정렬
방법 A: 버블 정렬
방법 B: 병합 정렬

어느 것이 더 빠른가? → 시간 복잡도 비교

A: O(n²)
B: O(n log n)

→ B가 더 빠름!</code></pre><p><strong>2. 성능 예측</strong></p>
<pre><code>현재: 100개 데이터 처리 (1초)
미래: 10,000개 데이터 처리 (?)

알고리즘이 O(n²)이면:
→ (10,000/100)² = 10,000배 → 약 2.7시간 예상

알고리즘이 O(n)이면:
→ 10,000/100 = 100배  → 약 100초 예상</code></pre><p><strong>3. 최적화 방향 결정</strong></p>
<pre><code>병목 지점 발견:
- 어느 부분이 느린가?
- 개선 가능한가?
- 트레이드오프는?

예: 시간 vs 공간  →  메모리를 더 써서 시간을 줄일 수 있나?</code></pre><h3 id="분석의-종류">분석의 종류</h3>
<p><strong>1. 점근적 분석 (Asymptotic Analysis)</strong></p>
<pre><code>입력 크기(n)가 충분히 클 때의 성능

왜 &quot;충분히 클 때&quot;?
- 작은 입력에서는 차이 미미
- 큰 입력에서 차이 극명

예:
n=10: O(n)과 O(n²) 차이 작음
n=10000: O(n)과 O(n²) 차이 엄청남</code></pre><p><strong>2. 최선/평균/최악 분석</strong></p>
<pre><code>같은 알고리즘도 입력에 따라 다름

퀵 정렬:
- 최선: O(n log n) (피벗이 항상 중간)
- 평균: O(n log n)
- 최악: O(n²) (피벗이 항상 끝)

어느 것으로 평가?
→ 보통 최악의 경우 (안전)</code></pre><p><strong>3. 상각 분석 (Amortized Analysis)</strong></p>
<pre><code>여러 연산의 평균 비용

예: 동적 배열
대부분: O(1) 삽입
가끔: O(n) 확장

평균적으로: O(1)</code></pre><h3 id="표기법">표기법</h3>
<p>알고리즘 성능을 수학적으로 표현하는 방법:</p>
<p><strong>Big-O (상한)</strong>: &quot;빅 오&quot;로 읽음, 알고리즘의 실행 시간이 &quot;아무리 느려도 이보다는 빨라&quot;</p>
<pre><code>O(g(n)): &quot;최악의 경우 g(n)보다 느리지 않다&quot;

가장 많이 사용
예: 이진 탐색은 O(log n)</code></pre><p><strong>Big-Ω (하한)</strong>: &quot;빅 오메가&quot;로 읽음, 알고리즘의 실행 시간이 &quot;아무리 빨라도 이보다는 느려&quot;</p>
<pre><code>Ω(g(n)): &quot;최선의 경우 g(n)보다 빠르지 않다&quot;

최소 성능 보장
예: 비교 기반 정렬은 최소 Ω(n log n)</code></pre><p><strong>Big-Θ (상한 = 하한)</strong>: &quot;빅 세타&quot;로 읽음, 알고리즘의 실행 시간이 &quot;평균적으로 딱 이만큼 걸려&quot;</p>
<pre><code>Θ(g(n)): &quot;항상 g(n)에 비례&quot;

정확한 복잡도
예: 배열 전체 순회는 Θ(n)</code></pre><h3 id="이-파트에서-배울-내용">이 파트에서 배울 내용</h3>
<pre><code>1. 시간 복잡도
   - Big-O 표기법
   - 주요 복잡도 (O(1), O(n), O(n²), ...)
   - 복잡도 계산 방법

2. 공간 복잡도
   - 메모리 사용량
   - 시간-공간 트레이드오프
   - 재귀의 공간 복잡도

3. 점근적 표기법
   - Big-O, Big-Ω, Big-Θ
   - 수학적 정의
   - 증명 방법

4. 최선/평균/최악 분석
   - 입력에 따른 성능 차이
   - 평균 케이스 분석
   - 확률적 분석

5. 상수 계수와 실제 성능
   - 이론 vs 실제
   - 캐시, 메모리 접근
   - 실무 고려사항

6. 상각 분석
   - 평균 비용 계산
   - 동적 배열, 스택
   - 잠재 함수 방법

7. 복잡도 클래스
   - P, NP, NP-Complete
   - 계산 복잡도 이론
   - 다루기 어려운 문제들</code></pre><h3 id="실무에서의-중요성">실무에서의 중요성</h3>
<p><strong>왜 분석이 중요한가?</strong></p>
<pre><code>작은 데이터 (n &lt; 100):
- 어떤 알고리즘이든 빠름
- 코드 간결성이 더 중요

큰 데이터 (n &gt; 100,000):
- 알고리즘 선택이 결정적
- O(n²)는 불가능
- O(n log n) 필수

예:
n = 100,000
O(n log n): 1초
O(n²): 2.7시간!</code></pre><p><strong>실무 의사결정</strong>:</p>
<pre><code>상황 1: 실시간 검색 → O(log n) 필수 (이진 탐색, 트리)

상황 2: 배치 처리 → O(n log n) 허용 (정렬 + 처리)

상황 3: 메모리 제한 → 공간 복잡도 우선 (스트리밍)

상황 4: 한 번만 실행 → 구현 간단한 것 (O(n²)도 OK)</code></pre><hr>
<p>이제 알고리즘 분석의 첫 번째 주제인 <strong>시간 복잡도</strong>를 자세히 알아봅시다.</p>
<hr>
<h2 id="🎯-시간-복잡도란-무엇인가">🎯 시간 복잡도란 무엇인가</h2>
<h3 id="알고리즘-성능의-척도">알고리즘 성능의 척도</h3>
<p>알고리즘을 만들었다면, 그것이 <strong>얼마나 빠른지</strong> 평가해야 합니다.</p>
<p><strong>왜 시간 복잡도가 필요한가?</strong></p>
<pre><code>두 정렬 알고리즘:
A: 10개 데이터를 1초에 정렬
B: 10개 데이터를 2초에 정렬

A가 더 빠르다?

100만 개 데이터라면?
A: 100,000초 (약 28시간)
B: 200초 (약 3분)

B가 훨씬 빠름!

→ 절대 시간이 아닌 &quot;증가율&quot;이 중요</code></pre><p><strong>시간 복잡도의 의미</strong>:</p>
<pre><code>알고리즘의 실행 시간이 입력 크기(n)에 따라 어떻게 증가하는가?

예:
- 선형 증가 (n배)
- 제곱 증가 (n²배)
- 로그 증가 (log n배)</code></pre><h3 id="실생활-비유">실생활 비유</h3>
<pre><code>책에서 이름 찾기:

방법 1: 처음부터 한 장씩
→ 책이 두 배 두꺼우면 시간도 두 배
→ 선형 증가 (O(n))

방법 2: 이진 탐색
→ 책이 두 배 두꺼워도 1번만 더
→ 로그 증가 (O(log n))

방법 3: 모든 쌍 비교
→ 책이 두 배 두꺼우면 시간은 네 배
→ 제곱 증가 (O(n²))</code></pre><h3 id="측정-방법">측정 방법</h3>
<p><strong>1. 실제 측정 (X)</strong></p>
<pre><code class="language-python">import time

start = time.time()
sort_algorithm(data)
end = time.time()
print(f&quot;실행 시간: {end - start}초&quot;)</code></pre>
<p><strong>문제점</strong>:</p>
<ul>
<li>컴퓨터 성능에 따라 다름</li>
<li>입력 데이터에 따라 다름</li>
<li>일반화 어려움</li>
</ul>
<p><strong>2. 연산 횟수 계산 (O)</strong></p>
<pre><code class="language-python">def linear_search(arr, target):
    comparisons = 0
    for item in arr:
        comparisons += 1  # 비교 연산
        if item == target:
            return comparisons
    return comparisons

# 연산 횟수 = n번</code></pre>
<p><strong>장점</strong>:</p>
<ul>
<li>하드웨어 독립적</li>
<li>일반적 성능 파악</li>
<li>알고리즘 비교 가능</li>
</ul>
<hr>
<h2 id="📊-big-o-표기법">📊 Big-O 표기법</h2>
<h3 id="big-o란">Big-O란?</h3>
<p><strong>Big-O 표기법</strong>은 알고리즘의 시간 복잡도를 표현하는 수학적 표기법입니다.</p>
<p><strong>정의</strong>:</p>
<pre><code>f(n) = O(g(n))

의미:
&quot;n이 충분히 클 때, f(n)은 g(n)에 비례하여 증가한다&quot;

예:
f(n) = 3n² + 2n + 1
→ O(n²)

큰 항만 남기고, 계수 제거</code></pre><p><strong>왜 이렇게?</strong></p>
<pre><code>f(n) = 3n² + 2n + 1

n = 10:     3×100 + 2×10 + 1 = 321
n = 100:    3×10000 + 2×100 + 1 = 30201
n = 1000:   3×1000000 + 2×1000 + 1 = 3002001

n이 클수록:
- n² 항이 지배적
- 2n, 1은 무시 가능
- 계수 3도 무시 (비율만 중요)

따라서: O(n²)</code></pre><h3 id="주요-시간-복잡도">주요 시간 복잡도</h3>
<p><strong>복잡도 계층</strong>:</p>
<pre><code>빠름 ←──────────────────────────────→ 느림

O(1) &lt; O(log n) &lt; O(n) &lt; O(n log n) &lt; O(n²) &lt; O(2ⁿ) &lt; O(n!)

상수      로그     선형       선형로그      제곱       지수     팩토리얼</code></pre><p><strong>증가율 비교</strong>
| 복잡도 | n=10 | n=100 | n=1,000 | n=10,000 |
|--------|------|-------|---------|----------|
| O(1) | 1 | 1 | 1 | 1 |
| O(log n) | 3 | 7 | 10 | 13 |
| O(n) | 10 | 100 | 1,000 | 10,000 |
| O(n log n) | 30 | 700 | 10,000 | 130,000 |
| O(n²) | 100 | 10,000 | 1,000,000 | 100,000,000 |
| O(2ⁿ) | 1,024 | 1.3×10³⁰ | - | - |
| O(n!) | 3,628,800 | - | - | - |
:</p>
<hr>
<h2 id="🔍-복잡도별-예시">🔍 복잡도별 예시</h2>
<h3 id="o1---상수-시간">O(1) - 상수 시간</h3>
<p><strong>정의</strong>: 입력 크기와 무관하게 일정한 시간</p>
<pre><code class="language-python">def get_first_element(arr):
    &quot;&quot;&quot;
    O(1) - 상수 시간

    배열 크기와 무관
    &quot;&quot;&quot;
    return arr[0]  # 한 번의 접근

def hash_lookup(hash_table, key):
    &quot;&quot;&quot;
    O(1) - 해시 테이블 조회
    &quot;&quot;&quot;
    return hash_table[key]

# 예시
arr = [1, 2, 3, 4, 5, ... , 1000000]
first = get_first_element(arr)  # 항상 같은 시간</code></pre>
<p><strong>특징</strong>:</p>
<ul>
<li>가장 빠름</li>
<li>입력 크기 증가해도 시간 동일</li>
</ul>
<h3 id="olog-n---로그-시간">O(log n) - 로그 시간</h3>
<p><strong>정의</strong>: 입력이 절반씩 줄어들 때</p>
<pre><code class="language-python">def binary_search(arr, target):
    &quot;&quot;&quot;
    O(log n) - 이진 탐색

    매번 절반씩 줄어듦
    &quot;&quot;&quot;
    left, right = 0, len(arr) - 1

    while left &lt;= right:
        mid = (left + right) // 2

        if arr[mid] == target:
            return mid
        elif arr[mid] &lt; target:
            left = mid + 1  # 오른쪽 절반만
        else:
            right = mid - 1  # 왼쪽 절반만

    return -1

# 분석:
# n = 1000 → 10번
# n = 1000000 → 20번
# n이 1000배 증가해도 10번만 더!</code></pre>
<p><strong>특징</strong>:</p>
<ul>
<li>매우 효율적</li>
<li>분할 정복 알고리즘에서 흔함</li>
</ul>
<h3 id="on---선형-시간">O(n) - 선형 시간</h3>
<p><strong>정의</strong>: 입력 크기에 비례</p>
<pre><code class="language-python">def linear_search(arr, target):
    &quot;&quot;&quot;
    O(n) - 선형 탐색

    모든 원소 확인
    &quot;&quot;&quot;
    for i, item in enumerate(arr):  # enumerate(arr): 리스트 arr에서 &#39;인덱스(i)&#39;와 &#39;내용(item)&#39;을 동시에 꺼내주는 함수
        if item == target:
            return i
    return -1       # 리스트 arr 전체에서 target를 못 찾았다면, &quot;없음&quot;을 뜻하는 약속된 신호인 -1을 반환

def find_max(arr):
    &quot;&quot;&quot;
    O(n) - 최댓값 찾기
    &quot;&quot;&quot;
    max_val = arr[0]
    for num in arr:  # n번 반복
        if num &gt; max_val:
            max_val = num
    return max_val

# 분석:
# n = 100 → 100번
# n = 1000 → 1000번
# n이 10배 → 시간도 10배</code></pre>
<p><strong>특징</strong>:</p>
<ul>
<li>가장 흔한 복잡도</li>
<li>모든 데이터를 봐야 할 때</li>
</ul>
<h3 id="on-log-n---선형로그-시간">O(n log n) - 선형로그 시간</h3>
<p><strong>정의</strong>: 분할 정복 + 합치기</p>
<pre><code class="language-python">def merge_sort(arr):
    &quot;&quot;&quot;
    O(n log n) - 병합 정렬

    분할: O(log n)
    합치기: O(n)
    &quot;&quot;&quot;
    if len(arr) &lt;= 1:
        return arr

    # 분할 (log n 레벨)
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])

    # 합치기 (n번)
    return merge(left, right)

def merge(left, right):
    &quot;&quot;&quot;두 정렬된 배열 합치기 - O(n)&quot;&quot;&quot;
    result = []
    i = j = 0

    while i &lt; len(left) and j &lt; len(right):
        if left[i] &lt; right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1

    result.extend(left[i:])
    result.extend(right[j:])
    return result

# 분석:
# 레벨: log n
# 각 레벨에서 n번 작업
# 총: n × log n</code></pre>
<p><strong>특징</strong>:</p>
<ul>
<li>효율적인 정렬 알고리즘</li>
<li>O(n)보다 느리지만 O(n²)보다 빠름</li>
</ul>
<h3 id="on²---제곱-시간">O(n²) - 제곱 시간</h3>
<p><strong>정의</strong>: 이중 반복문</p>
<pre><code class="language-python">def bubble_sort(arr):
    &quot;&quot;&quot;
    O(n²) - 버블 정렬

    이중 반복문
    &quot;&quot;&quot;
    n = len(arr)

    for i in range(n):          # n번
        for j in range(n - 1):  # n번
            if arr[j] &gt; arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]

    return arr

def find_duplicates(arr):
    &quot;&quot;&quot;
    O(n²) - 모든 쌍 비교
    &quot;&quot;&quot;
    duplicates = []

    for i in range(len(arr)):        # n번
        for j in range(i + 1, len(arr)):  # n번
            if arr[i] == arr[j]:
                duplicates.append(arr[i])

    return duplicates

# 분석:
# n = 10 → 100번
# n = 100 → 10,000번
# n = 1000 → 1,000,000번</code></pre>
<p><strong>특징</strong>:</p>
<ul>
<li>작은 데이터에는 OK</li>
<li>큰 데이터에는 느림</li>
</ul>
<h3 id="o2ⁿ---지수-시간">O(2ⁿ) - 지수 시간</h3>
<p><strong>정의</strong>: 입력마다 선택지가 2배</p>
<pre><code class="language-python">def fibonacci_recursive(n):
    &quot;&quot;&quot;
    O(2ⁿ) - 순수 재귀 피보나치

    매번 2개로 분기
    &quot;&quot;&quot;
    if n &lt;= 1:
        return n

    # 2개로 분기
    return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)

# 분석:
# fib(5):
#           fib(5)
#          /      \
#      fib(4)    fib(3)
#      /    \    /    \
#   fib(3) fib(2) ...
#
# 호출 횟수: 2^5 = 32번

# n = 10 → 1,024번
# n = 20 → 1,048,576번
# n = 30 → 1,073,741,824번!</code></pre>
<p><strong>특징</strong>:</p>
<ul>
<li>매우 느림</li>
<li>작은 n도 불가능</li>
<li>동적 계획법으로 개선 필요</li>
</ul>
<h3 id="on---팩토리얼-시간">O(n!) - 팩토리얼 시간</h3>
<p><strong>정의</strong>: 모든 순열</p>
<pre><code class="language-python">def permutations_naive(arr):
    &quot;&quot;&quot;
    O(n!) - 모든 순열 생성

    n개 원소의 순열: n!개
    &quot;&quot;&quot;
    if len(arr) &lt;= 1:
        return [arr]

    result = []
    for i in range(len(arr)):
        # 나머지 원소의 순열
        rest = arr[:i] + arr[i+1:]
        for perm in permutations_naive(rest):
            result.append([arr[i]] + perm)

    return result

# 분석:
# n = 3 → 6개 (3!)
# n = 5 → 120개 (5!)
# n = 10 → 3,628,800개 (10!)</code></pre>
<p><strong>특징</strong>:</p>
<ul>
<li>가장 느림</li>
<li>n &gt; 15면 실용적으로 불가능</li>
</ul>
<hr>
<h2 id="🔢-복잡도-계산-연습">🔢 복잡도 계산 연습</h2>
<h3 id="기본-규칙">기본 규칙</h3>
<p><strong>1. 순차 실행: 더하기</strong></p>
<pre><code class="language-python">def example1(arr):
    # O(n)
    for x in arr:
        print(x)

    # O(n)
    for x in arr:
        print(x * 2)

    # 총: O(n) + O(n) = O(2n) = O(n)
    # 계수 제거!</code></pre>
<p><strong>2. 중첩 실행: 곱하기</strong></p>
<pre><code class="language-python">def example2(arr):
    # 외부: O(n)
    for i in arr:
        # 내부: O(n)
        for j in arr:
            print(i, j)

    # 총: O(n) × O(n) = O(n²)</code></pre>
<p><strong>3. 큰 항만 남기기</strong></p>
<pre><code class="language-python">def example3(arr):
    # O(n)
    for x in arr:
        print(x)

    # O(n²)
    for i in arr:
        for j in arr:
            print(i, j)

    # 총: O(n) + O(n²) = O(n²)
    # 큰 항만!</code></pre>
<h3 id="예제-분석">예제 분석</h3>
<p><strong>예제 1</strong>:</p>
<pre><code class="language-python">def example(arr):
    # 1번 연산
    result = arr[0]  # O(1)

    # n번 반복
    for x in arr:  # O(n)
        result += x

    return result

# 총: O(1) + O(n) = O(n)</code></pre>
<p><strong>예제 2</strong>:</p>
<pre><code class="language-python">def example(arr):
    count = 0

    # n번 반복
    for i in range(len(arr)):
        # n번 반복
        for j in range(i):
            count += 1

    return count

# 분석:
# i=0: 0번
# i=1: 1번
# i=2: 2번
# ...
# i=n-1: n-1번
# 총: 0+1+2+...+(n-1) = n(n-1)/2 = O(n²)</code></pre>
<p><strong>예제 3</strong>:</p>
<pre><code class="language-python">def example(arr):
    # log n번 반복 (절반씩)
    i = 1
    while i &lt; len(arr):
        # n번 반복
        for j in range(len(arr)):
            print(i, j)
        i *= 2

    return

# 총: O(log n) × O(n) = O(n log n)</code></pre>
<p><strong>예제 4</strong>:</p>
<pre><code class="language-python">def example(n):
    # n번
    for i in range(n):
        print(i)

    # n²번 (이중 반복)
    for i in range(n):
        for j in range(n):
            print(i, j)

    # log n번 (절반씩)
    i = 1
    while i &lt; n:
        print(i)
        i *= 2

    return

# 총: O(n) + O(n²) + O(log n) = O(n²)
# 가장 큰 항만!</code></pre>
<hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<h2 id="복잡도별-특성">복잡도별 특성</h2>
<h3 id="🟢-효율적-실용적으로-사용-가능">🟢 효율적 (실용적으로 사용 가능)</h3>
<h4 id="o1---상수-시간-1"><strong>O(1) - 상수 시간</strong></h4>
<ul>
<li>📊 성장률: 입력 크기와 무관</li>
<li>🔍 예시: 해시 테이블 조회, 배열 인덱스 접근</li>
<li>⏱️ n=1,000,000 → 1번의 연산</li>
</ul>
<h4 id="olog-n---로그-시간-1"><strong>O(log n) - 로그 시간</strong></h4>
<ul>
<li>📊 성장률: 매우 완만</li>
<li>🔍 예시: 이진 탐색, 균형 이진 트리 탐색</li>
<li>⏱️ n=1,000,000 → 약 20번의 연산</li>
</ul>
<h4 id="on---선형-시간-1"><strong>O(n) - 선형 시간</strong></h4>
<ul>
<li>📊 성장률: 선형 증가</li>
<li>🔍 예시: 배열 순회, 단순 반복문</li>
<li>⏱️ n=1,000,000 → 1,000,000번의 연산</li>
</ul>
<h3 id="🟡-주의-필요-중간-효율">🟡 주의 필요 (중간 효율)</h3>
<h4 id="on-log-n---선형-로그-시간"><strong>O(n log n) - 선형 로그 시간</strong></h4>
<ul>
<li>📊 성장률: 선형보다 약간 가파름</li>
<li>🔍 예시: 병합 정렬, 퀵 정렬(평균), 힙 정렬</li>
<li>⏱️ n=1,000,000 → 약 20,000,000번의 연산</li>
</ul>
<h3 id="🔴-비효율적-작은-데이터만-가능">🔴 비효율적 (작은 데이터만 가능)</h3>
<h4 id="on²---이차-시간"><strong>O(n²) - 이차 시간</strong></h4>
<ul>
<li>📊 성장률: 급격한 증가</li>
<li>🔍 예시: 버블 정렬, 삽입 정렬, 이중 반복문</li>
<li>⏱️ n=1,000 → 1,000,000번의 연산</li>
<li>⚠️ n &gt; 10,000이면 실용적이지 않음</li>
</ul>
<h4 id="o2ⁿ---지수-시간-1"><strong>O(2ⁿ) - 지수 시간</strong></h4>
<ul>
<li>📊 성장률: 폭발적 증가</li>
<li>🔍 예시: 피보나치(재귀), 부분집합 생성</li>
<li>⏱️ n=30 → 1,073,741,824번의 연산</li>
<li>⚠️ n &gt; 20이면 거의 불가능</li>
</ul>
<h4 id="on---팩토리얼-시간-1"><strong>O(n!) - 팩토리얼 시간</strong></h4>
<ul>
<li>📊 성장률: 최악의 증가율</li>
<li>🔍 예시: 순열 생성, 외판원 문제(브루트 포스)</li>
<li>⏱️ n=10 → 3,628,800번의 연산</li>
<li>⚠️ n &gt; 12이면 실질적으로 불가능</li>
</ul>
<h2 id="실용적-가이드라인">실용적 가이드라인</h2>
<ul>
<li>✅ <strong>O(1), O(log n), O(n)</strong>: 대부분의 실용적인 알고리즘</li>
<li>⚠️ <strong>O(n log n)</strong>: 효율적인 정렬 알고리즘의 한계</li>
<li>⚠️ <strong>O(n²)</strong>: 작은 데이터셋에서만 사용 가능</li>
<li>❌ <strong>O(2ⁿ), O(n!)</strong>: n이 20을 넘으면 실용적으로 사용 불가능</li>
</ul>
<hr>
<h2 id="📊-데이터-크기별-허용-시간-복잡도">📊 데이터 크기별 허용 시간 복잡도</h2>
<table>
<thead>
<tr>
<th>데이터 크기 (n)</th>
<th>O(log n)</th>
<th>O(n)</th>
<th>O(n log n)</th>
<th>O(n²)</th>
<th>O(n³)</th>
<th>O(2ⁿ)</th>
<th>O(n!)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>≤ 10</strong></td>
<td>✅ 최적</td>
<td>✅ 최적</td>
<td>✅ 최적</td>
<td>✅ 가능</td>
<td>✅ 가능</td>
<td>✅ 가능</td>
<td>✅ 가능</td>
</tr>
<tr>
<td><strong>≤ 20</strong></td>
<td>✅ 최적</td>
<td>✅ 최적</td>
<td>✅ 최적</td>
<td>✅ 가능</td>
<td>✅ 가능</td>
<td>✅ 가능</td>
<td>❌ 불가</td>
</tr>
<tr>
<td><strong>≤ 100</strong></td>
<td>✅ 최적</td>
<td>✅ 최적</td>
<td>✅ 최적</td>
<td>✅ 가능</td>
<td>✅ 가능</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
</tr>
<tr>
<td><strong>≤ 1,000</strong></td>
<td>✅ 최적</td>
<td>✅ 최적</td>
<td>✅ 최적</td>
<td>✅ 가능</td>
<td>⚠️ 한계</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
</tr>
<tr>
<td><strong>≤ 10,000</strong></td>
<td>✅ 최적</td>
<td>✅ 최적</td>
<td>✅ 권장</td>
<td>⚠️ 한계</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
</tr>
<tr>
<td><strong>≤ 100,000</strong></td>
<td>✅ 최적</td>
<td>✅ 최적</td>
<td>✅ 권장</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
</tr>
<tr>
<td><strong>≤ 1,000,000</strong></td>
<td>✅ 최적</td>
<td>✅ 필수</td>
<td>✅ 권장</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
</tr>
<tr>
<td><strong>&gt; 1,000,000</strong></td>
<td>✅ 최적</td>
<td>✅ 필수</td>
<td>⚠️ 한계</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
<td>❌ 불가</td>
</tr>
</tbody></table>
<h3 id="범례">범례</h3>
<ul>
<li>✅ <strong>최적</strong>: 가장 효율적인 선택</li>
<li>✅ <strong>권장</strong>: 실용적으로 충분히 빠름</li>
<li>✅ <strong>필수</strong>: 이보다 느린 알고리즘은 시간 초과</li>
<li>✅ <strong>가능</strong>: 실행 가능하나 더 빠른 방법 권장</li>
<li>⚠️ <strong>한계</strong>: 시간 초과 위험이 있음</li>
<li>❌ <strong>불가</strong>: 실행 시간이 너무 오래 걸림</li>
</ul>
<h3 id="최적화-전략">최적화 전략</h3>
<p><strong>1. 불필요한 반복 제거</strong></p>
<pre><code class="language-python"># 나쁜 예: O(n²)
def has_duplicate_bad(arr):
    for i in range(len(arr)):
        for j in range(len(arr)):
            if i != j and arr[i] == arr[j]:
                return True
    return False

# 좋은 예: O(n)
def has_duplicate_good(arr):
    seen = set()
    for x in arr:
        if x in seen:
            return True
        seen.add(x)
    return False</code></pre>
<p><strong>2. 적절한 자료구조</strong></p>
<pre><code class="language-python"># 나쁜 예: O(n) 검색
def search_in_list(lst, target):
    return target in lst  # O(n)

# 좋은 예: O(1) 검색
def search_in_set(s, target):
    return target in s  # O(1)</code></pre>
<p><strong>3. 사전 계산</strong></p>
<pre><code class="language-python"># 나쁜 예: 매번 계산
def sum_queries_bad(arr, queries):
    results = []
    for l, r in queries:
        # 매 쿼리마다 O(r-l)
        results.append(sum(arr[l:r]))
    return results

# 좋은 예: 누적합
def sum_queries_good(arr, queries):
    # 사전 계산: O(n)
    prefix = [0]
    for x in arr:
        prefix.append(prefix[-1] + x)

    # 각 쿼리: O(1)
    results = []
    for l, r in queries:
        results.append(prefix[r] - prefix[l])
    return results</code></pre>
<h3 id="성능-측정">성능 측정</h3>
<pre><code class="language-python">import time

def measure_time(func, *args): # func: 측정하고 싶은 함수
                               # *args: &quot;가변 인자&quot;로 func가 실행될 때 필요한 재료(배열, 타겟 값 등)를 몇 개든 유연하게 받음
    &quot;&quot;&quot;함수 실행 시간 측정&quot;&quot;&quot;
    start = time.time()
    result = func(*args)
    end = time.time()
    print(f&quot;{func.__name__}: {end - start:.6f}초&quot;)  # func.__name__: 실행된 함수의 이름을 자동으로 출력
    return result

# 사용
arr = list(range(10000))
measure_time(bubble_sort, arr.copy())  # O(n²)
measure_time(merge_sort, arr.copy())   # O(n log n)</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>Big-O 표기법</strong></p>
<pre><code>f(n) = O(g(n))

의미: n이 클 때 f(n)은 g(n)에 비례

특징:
- 큰 항만
- 계수 제거
- 최악의 경우</code></pre><p><strong>주요 복잡도</strong></p>
<pre><code>O(1)        상수       가장 빠름
O(log n)    로그       매우 빠름
O(n)        선형       빠름
O(n log n)  선형로그   괜찮음
O(n²)       제곱       느림
O(2ⁿ)       지수       매우 느림
O(n!)       팩토리얼   극도로 느림</code></pre><p><strong>계산 규칙</strong></p>
<pre><code>순차: O(f) + O(g) = O(max(f, g))
중첩: O(f) × O(g) = O(f × g)</code></pre><p><strong>실무 지침</strong></p>
<pre><code>작은 데이터 (n &lt; 100):
- 복잡도 덜 중요
- 코드 간결성 우선

큰 데이터 (n &gt; 10,000):
- 복잡도 매우 중요
- O(n log n) 이하 필수</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[07-02] 공간 복잡도 (Space Complexity)</strong></p>
<ul>
<li>공간 복잡도의 개념: 알고리즘이 사용하는 메모리 양</li>
<li>시간-공간 트레이드오프: 메모리를 더 써서 시간 절약하기</li>
<li>재귀의 공간 복잡도: 호출 스택의 깊이 이해</li>
<li>제자리 알고리즘: 추가 공간 없이 동작하는 효율적 방법</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[06-09] 문자열 알고리즘</a><br><strong>다음 글</strong>: <a href="#">[07-02] 공간 복잡도</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [06-09] 문자열 알고리즘]]></title>
            <link>https://velog.io/@road_to_ai/06-09-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@road_to_ai/06-09-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Thu, 19 Feb 2026 11:40:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>문자열 알고리즘은 텍스트 검색, 패턴 매칭, 압축 등 문자열 처리에 특화된 효율적인 알고리즘들입니다.</p>
</blockquote>
<hr>
<h2 id="🎯-문자열-알고리즘이란">🎯 문자열 알고리즘이란</h2>
<h3 id="문자열-문제의-중요성">문자열 문제의 중요성</h3>
<p>문자열 처리는 컴퓨터 과학에서 가장 기본적이면서도 중요한 분야입니다.</p>
<p><strong>실생활 응용</strong>:</p>
<pre><code>텍스트 검색:
- 구글, 네이버 검색
- Ctrl+F (문서 내 검색)
- 코드 에디터의 찾기

DNA 분석:
- 유전자 서열 매칭
- 유사한 패턴 찾기

텍스트 편집:
- 맞춤법 검사
- 자동 완성
- 표절 탐지

데이터 압축:
- ZIP, RAR
- 중복 패턴 제거</code></pre><p><strong>기본 문제: 패턴 매칭</strong></p>
<pre><code>텍스트: &quot;ABCABCDABCDE&quot;
패턴: &quot;ABCD&quot;

목표: 패턴이 텍스트의 어디에 있는가?
답: 인덱스 3 (0부터 시작)

      0123456789...
텍스트: ABCABCDABCDE
패턴:      ABCD
          찾음! (인덱스 3)</code></pre><h3 id="이번-글에서-다룰-알고리즘">이번 글에서 다룰 알고리즘</h3>
<pre><code>1. 순진한 방법 (Naive)
   - 기본 접근
   - 비교 대상

2. KMP 알고리즘
   - 실패 함수
   - 불필요한 비교 건너뛰기

3. 라빈-카프 알고리즘
   - 해싱 이용
   - 다중 패턴 매칭

4. 트라이 (Trie)
   - 문자열 집합 저장
   - 빠른 검색</code></pre><hr>
<h2 id="🔍-순진한-문자열-매칭">🔍 순진한 문자열 매칭</h2>
<h3 id="순진한-방법이란">순진한 방법이란?</h3>
<p><strong>순진한 방법(Naive/Brute Force)</strong>은 가장 직관적인 패턴 매칭 방법입니다.</p>
<p><strong>핵심 아이디어</strong>:</p>
<pre><code>텍스트의 모든 위치에서 패턴과 비교

텍스트: ABCABCDABCDE
패턴:   ABCD

위치 0: ABCA vs ABCD → 불일치 (마지막)
위치 1: BCAB vs ABCD → 불일치 (첫 글자)
위치 2: CABC vs ABCD → 불일치 (첫 글자)
위치 3: ABCD vs ABCD → 일치! ✓
...</code></pre><p><strong>특징</strong>:</p>
<pre><code>장점:
- 구현 간단
- 이해 쉬움

단점:
- 비효율적
- 불필요한 비교 많음</code></pre><h3 id="순진한-방법-구현">순진한 방법 구현</h3>
<pre><code class="language-python">def naive_search(text, pattern):
    &quot;&quot;&quot;
    순진한 문자열 매칭

    text: 검색 대상 텍스트
    pattern: 찾을 패턴

    Returns: 패턴이 나타나는 모든 위치 리스트

    시간복잡도: O(nm)
    - n: 텍스트 길이
    - m: 패턴 길이
    - 최악의 경우 모든 위치에서 m번 비교
    &quot;&quot;&quot;
    n = len(text)
    m = len(pattern)
    positions = []

    # 텍스트의 각 위치에서 시도
    for i in range(n - m + 1):
        # 패턴의 각 문자와 비교
        j = 0
        while j &lt; m and text[i + j] == pattern[j]:
            j += 1

        # 모든 문자가 일치하면
        if j == m:
            positions.append(i)

    return positions

# 사용 예시
text = &quot;ABCABCDABCDE&quot;
pattern = &quot;ABCD&quot;

result = naive_search(text, pattern)
print(f&quot;텍스트: {text}&quot;)
print(f&quot;패턴: {pattern}&quot;)
print(f&quot;발견 위치: {result}&quot;)

# 비교 횟수 세기
def naive_search_count(text, pattern):
    &quot;&quot;&quot;비교 횟수를 세는 버전&quot;&quot;&quot;
    n = len(text)
    m = len(pattern)
    positions = []
    comparisons = 0

    for i in range(n - m + 1):
        j = 0
        while j &lt; m and text[i + j] == pattern[j]:
            comparisons += 1
            j += 1

        # 불일치한 경우도 비교 횟수 추가
        if j &lt; m:
            comparisons += 1

        if j == m:
            positions.append(i)

    return positions, comparisons

result, count = naive_search_count(text, pattern)
print(f&quot;\n총 비교 횟수: {count}&quot;)</code></pre>
<p><strong>최악의 경우</strong>:</p>
<pre><code>텍스트: AAAAAAAAAA (n개)
패턴:   AAAAB      (m개)

매 위치에서 m-1개는 일치, 마지막만 불일치
→ (n-m+1) × m 번 비교
→ O(nm)

예: n=10, m=5
AAAAAAAAAA
AAAAB      (5번 비교)
 AAAAB     (5번 비교)
  AAAAB    (5번 비교)
   ...
6개 위치 × 5번 = 30번 비교</code></pre><hr>
<h2 id="🔄-kmp-knuth-morris-pratt-알고리즘">🔄 KMP (Knuth-Morris-Pratt) 알고리즘</h2>
<h3 id="kmp란">KMP란?</h3>
<p><strong>KMP 알고리즘</strong>은 불필요한 비교를 건너뛰는 효율적인 패턴 매칭 알고리즘입니다.</p>
<p><strong>핵심 통찰</strong>:</p>
<pre><code>순진한 방법의 문제:

텍스트: ABCABCDABCDE
패턴:   ABCD
       |||X
       ABC는 일치, D만 불일치

다음 위치에서 다시 처음부터?
        ABCD
         X

낭비!
이미 &quot;ABC&quot;가 일치한다는 정보를 알고 있는데
처음부터 다시 비교할 필요 없음!</code></pre><p><strong>KMP의 아이디어</strong>:</p>
<pre><code>패턴 자체에서 정보 추출

패턴: ABCDABC

&quot;ABC&quot;로 시작하고 &quot;ABC&quot;로 끝남!
→ 불일치 시 3칸 건너뛸 수 있음

이 정보를 &quot;실패 함수&quot;로 미리 계산</code></pre><h3 id="실패-함수-failure-function">실패 함수 (Failure Function)</h3>
<p><strong>실패 함수</strong>는 패턴의 각 위치에서 접두사와 접미사의 최대 일치 길이를 저장합니다.</p>
<p><strong>개념</strong>:</p>
<pre><code>패턴: ABCDABC

각 위치에서:
A       접두사=접미사 = 없음 → 0
AB      접두사=접미사 = 없음 → 0
ABC     접두사=접미사 = 없음 → 0
ABCD    접두사=접미사 = 없음 → 0
ABCDA   접두사=접미사 = A → 1
ABCDAB  접두사=접미사 = AB → 2
ABCDABC 접두사=접미사 = ABC → 3

실패 함수: [0, 0, 0, 0, 1, 2, 3]</code></pre><p><strong>의미</strong>:</p>
<pre><code>실패 함수[i] = k 의미:
&quot;pattern[0:k] == pattern[i-k+1:i+1]&quot;

즉, i 위치까지의 부분 문자열에서
길이 k인 접두사와 접미사가 같음

예: ABCDABC (i=6)
실패 함수[6] = 3
→ ABC(접두사) == ABC(접미사)</code></pre><h3 id="실패-함수-계산">실패 함수 계산</h3>
<pre><code class="language-python">def compute_failure_function(pattern):
    &quot;&quot;&quot;
    KMP 실패 함수 계산

    pattern: 패턴 문자열

    Returns: 실패 함수 배열

    시간복잡도: O(m)
    - m: 패턴 길이

    원리:
    - 동적 계획법
    - 이전 정보를 활용하여 다음 값 계산
    &quot;&quot;&quot;
    m = len(pattern)
    failure = [0] * m

    # j: 현재 접두사 길이
    # i: 현재 확인 중인 위치
    j = 0

    # i=1부터 시작 (i=0은 항상 0)
    for i in range(1, m):
        # 불일치 시 j를 줄여가며 확인
        while j &gt; 0 and pattern[i] != pattern[j]:
            j = failure[j - 1]

        # 일치하면
        if pattern[i] == pattern[j]:
            j += 1
            failure[i] = j
        # 불일치면 failure[i] = 0 (이미 초기화됨)

    return failure

# 사용 예시
pattern = &quot;ABCDABC&quot;
failure = compute_failure_function(pattern)

print(f&quot;패턴: {pattern}&quot;)
print(&quot;위치:  &quot;, &quot; &quot;.join(str(i) for i in range(len(pattern))))
print(&quot;실패:  &quot;, &quot; &quot;.join(str(f) for f in failure))

# 여러 예시
patterns = [&quot;AAAA&quot;, &quot;ABAB&quot;, &quot;ABABC&quot;, &quot;ABCDABCA&quot;]
print(&quot;\n다양한 패턴의 실패 함수:&quot;)
for p in patterns:
    f = compute_failure_function(p)
    print(f&quot;{p:12s} → {f}&quot;)</code></pre>
<p><strong>실패 함수 계산 과정</strong>:</p>
<pre><code>패턴: ABCDABC

i=0: failure[0] = 0 (초기값)

i=1: pattern[1]=&#39;B&#39;, pattern[0]=&#39;A&#39;
     불일치 → failure[1] = 0

i=2: pattern[2]=&#39;C&#39;, pattern[0]=&#39;A&#39;
     불일치 → failure[2] = 0

i=3: pattern[3]=&#39;D&#39;, pattern[0]=&#39;A&#39;
     불일치 → failure[3] = 0

i=4: pattern[4]=&#39;A&#39;, pattern[0]=&#39;A&#39;
     일치! j=1 → failure[4] = 1

i=5: pattern[5]=&#39;B&#39;, pattern[1]=&#39;B&#39;
     일치! j=2 → failure[5] = 2

i=6: pattern[6]=&#39;C&#39;, pattern[2]=&#39;C&#39;
     일치! j=3 → failure[6] = 3

결과: [0, 0, 0, 0, 1, 2, 3]</code></pre><h3 id="kmp-매칭">KMP 매칭</h3>
<pre><code class="language-python">def kmp_search(text, pattern):
    &quot;&quot;&quot;
    KMP 문자열 매칭

    text: 검색 대상 텍스트
    pattern: 찾을 패턴

    Returns: 패턴이 나타나는 모든 위치 리스트

    시간복잡도: O(n + m)
    - n: 텍스트 길이
    - m: 패턴 길이
    - 실패 함수 계산: O(m)
    - 매칭: O(n)
    &quot;&quot;&quot;
    n = len(text)
    m = len(pattern)

    # 1. 실패 함수 계산
    failure = compute_failure_function(pattern)

    positions = []
    j = 0  # 패턴에서 현재 비교 중인 위치

    # 2. 텍스트 순회
    for i in range(n):
        # 불일치 시 실패 함수 활용
        while j &gt; 0 and text[i] != pattern[j]:
            j = failure[j - 1]  # 건너뛰기!

        # 일치하면
        if text[i] == pattern[j]:
            j += 1

            # 패턴 전체 일치
            if j == m:
                positions.append(i - m + 1)
                j = failure[j - 1]  # 다음 매칭을 위해   
    return positions

# 사용 예시
text = &quot;ABCABCDABCDE&quot;
pattern = &quot;ABCD&quot;

result = kmp_search(text, pattern)
print(f&quot;텍스트: {text}&quot;)
print(f&quot;패턴: {pattern}&quot;)
print(f&quot;발견 위치: {result}&quot;)

# 비교 횟수 세기
def kmp_search_count(text, pattern):
    &quot;&quot;&quot;비교 횟수를 세는 버전&quot;&quot;&quot;
    n = len(text)
    m = len(pattern)

    failure = compute_failure_function(pattern)

    positions = []
    comparisons = 0
    j = 0

    for i in range(n):
        while j &gt; 0 and text[i] != pattern[j]:
            j = failure[j - 1]
            comparisons += 1
        comparisons += 1  # 현재 비교

        if text[i] == pattern[j]:
            j += 1            
            if j == m:
                positions.append(i - m + 1)
                j = failure[j - 1]

    return positions, comparisons

result, count = kmp_search_count(text, pattern)
print(f&quot;\n총 비교 횟수: {count}&quot;)
print(f&quot;순진한 방법과 비교: {count} vs 더 많음&quot;)</code></pre>
<p><strong>KMP 실행 과정</strong>:</p>
<pre><code>텍스트: ABCABCDABCDE
패턴:   ABCD
실패:   [0, 0, 0, 0]

i=0: text[0]=&#39;A&#39;, pattern[0]=&#39;A&#39;
     일치, j=1

i=1: text[1]=&#39;B&#39;, pattern[1]=&#39;B&#39;
     일치, j=2

i=2: text[2]=&#39;C&#39;, pattern[2]=&#39;C&#39;
     일치, j=3

i=3: text[3]=&#39;A&#39;, pattern[3]=&#39;D&#39;
     불일치! j=failure[2]=0

     text[3]=&#39;A&#39;, pattern[0]=&#39;A&#39;
     일치, j=1

i=4: text[4]=&#39;B&#39;, pattern[1]=&#39;B&#39;
     일치, j=2

i=5: text[5]=&#39;C&#39;, pattern[2]=&#39;C&#39;
     일치, j=3

i=6: text[6]=&#39;D&#39;, pattern[3]=&#39;D&#39;
     일치, j=4
     매칭 발견! 위치=3

...

핵심: 불일치 시 실패 함수로 건너뛰기!</code></pre><hr>
<h2 id="🔢-라빈-카프-rabin-karp-알고리즘">🔢 라빈-카프 (Rabin-Karp) 알고리즘</h2>
<h3 id="라빈-카프란">라빈-카프란?</h3>
<p><strong>라빈-카프 알고리즘</strong>은 해싱을 이용한 패턴 매칭 알고리즘입니다.</p>
<p><strong>핵심 아이디어</strong>:</p>
<pre><code>문자열을 숫자로 변환하여 비교

패턴: &quot;ABC&quot;
해시: hash(&quot;ABC&quot;) = 123

텍스트를 순회하며:
- 각 부분 문자열의 해시 계산
- 해시가 같으면 실제 문자열 비교

장점:
- 여러 패턴을 동시에 검색 가능
- 평균적으로 빠름</code></pre><p><strong>해시 함수</strong>:</p>
<pre><code>문자열을 숫자로:

&quot;ABC&quot; → 1×100 + 2×10 + 3×1 = 123

일반적으로:
hash = s[0]×d^(m-1) + s[1]×d^(m-2) + ... + s[m-1]×d^0

d: 진법 (보통 256, 문자 종류 수)
m: 패턴 길이</code></pre><h3 id="롤링-해시-rolling-hash">롤링 해시 (Rolling Hash)</h3>
<p><strong>롤링 해시</strong>는 이전 해시값을 재사용하여 다음 해시를 빠르게 계산하는 기법입니다.</p>
<p><strong>원리</strong>:</p>
<pre><code>텍스트: &quot;ABCDE&quot;
패턴 길이: 3

hash(&quot;ABC&quot;) = 1×100 + 2×10 + 3×1 = 123

hash(&quot;BCD&quot;)를 처음부터 계산?
= 2×100 + 3×10 + 4×1 = 234

롤링 해시:
hash(&quot;BCD&quot;) = (hash(&quot;ABC&quot;) - 1×100) × 10 + 4
            = (123 - 100) × 10 + 4
            = 234

이전 값 재사용!</code></pre><p><strong>공식</strong>:</p>
<pre><code>hash_new = (hash_old - text[old_pos]×d^(m-1)) × d + text[new_pos]

old_pos: 이전 시작 위치
new_pos: 새로운 끝 위치</code></pre><h3 id="라빈-카프-구현">라빈-카프 구현</h3>
<pre><code class="language-python">def rabin_karp(text, pattern, d=256, q=101):
    &quot;&quot;&quot;
    라빈-카프 문자열 매칭

    text: 검색 대상 텍스트
    pattern: 찾을 패턴
    d: 진법 (문자 종류 수)
    q: 소수 (해시 충돌 감소용)

    Returns: 패턴이 나타나는 모든 위치 리스트

    시간복잡도:
    - 평균: O(n + m)
    - 최악: O(nm) (해시 충돌 많을 때)

    특징:
    - 해싱 이용
    - 다중 패턴 매칭에 유리
    - 롤링 해시로 최적화
    &quot;&quot;&quot;
    n = len(text)
    m = len(pattern)
    positions = []

    # d^(m-1) % q 미리 계산
    h = pow(d, m - 1, q)

    # 패턴과 텍스트 첫 윈도우의 해시 계산
    pattern_hash = 0
    text_hash = 0

    for i in range(m):       # ord(): 문자를 컴퓨터가 이해하는 숫자(아스키 코드)로 바꿈 (예: &#39;A&#39; -&gt; 65)
        pattern_hash = (d * pattern_hash + ord(pattern[i])) % q
        text_hash = (d * text_hash + ord(text[i])) % q

    # 텍스트 순회
    for i in range(n - m + 1):
        # 해시가 같으면
        if pattern_hash == text_hash:
            # 실제 문자열 비교 (해시 충돌 대비)
            if text[i:i+m] == pattern:
                positions.append(i)

        # 다음 윈도우의 해시 계산 (롤링)
        if i &lt; n - m:
            # 이전 문자 제거, 새 문자 추가
            text_hash = (d * (text_hash - ord(text[i]) * h) + 
                        ord(text[i + m])) % q

            # 음수 방지
            if text_hash &lt; 0:
                text_hash += q

    return positions

# 사용 예시
text = &quot;ABCABCDABCDE&quot;
pattern = &quot;ABCD&quot;

result = rabin_karp(text, pattern)
print(f&quot;텍스트: {text}&quot;)
print(f&quot;패턴: {pattern}&quot;)
print(f&quot;발견 위치: {result}&quot;)

# 다중 패턴 매칭
def rabin_karp_multiple(text, patterns, d=256, q=101):
    &quot;&quot;&quot;
    여러 패턴을 동시에 검색

    라빈-카프의 장점 활용!
    &quot;&quot;&quot;
    # 각 패턴의 해시 계산
    pattern_hashes = {}
    for pattern in patterns:
        h = 0
        for char in pattern:
            h = (d * h + ord(char)) % q
        pattern_hashes[h] = pattern

    results = {p: [] for p in patterns}

    # 패턴 길이 (모두 같다고 가정)
    m = len(patterns[0])
    n = len(text)

    # 텍스트 해시 계산 및 매칭
    h_multiplier = pow(d, m - 1, q)
    text_hash = 0

    for i in range(m):
        text_hash = (d * text_hash + ord(text[i])) % q

    for i in range(n - m + 1):
        # 해시가 어떤 패턴과 일치하는지 확인
        if text_hash in pattern_hashes:
            pattern = pattern_hashes[text_hash]
            if text[i:i+m] == pattern:
                results[pattern].append(i)

        # 롤링 해시
        if i &lt; n - m:
            text_hash = (d * (text_hash - ord(text[i]) * h_multiplier) + 
                        ord(text[i + m])) % q
            if text_hash &lt; 0:
                text_hash += q   
    return results

# 다중 패턴 예시
text = &quot;ABCABCDABCDE&quot;
patterns = [&quot;ABC&quot;, &quot;BCD&quot;, &quot;CDE&quot;]

results = rabin_karp_multiple(text, patterns)
print(&quot;\n\n다중 패턴 매칭:&quot;)
for pattern, positions in results.items():
    print(f&quot;{pattern}: {positions}&quot;)</code></pre>
<hr>
<h2 id="🌳-트라이-trie">🌳 트라이 (Trie)</h2>
<h3 id="트라이란">트라이란?</h3>
<p><strong>트라이(Trie)</strong>는 문자열 집합을 효율적으로 저장하고 검색하는 트리 자료구조입니다.</p>
<p><strong>핵심 아이디어</strong>:</p>
<pre><code>공통 접두사를 공유

문자열 집합: [&quot;cat&quot;, &quot;car&quot;, &quot;dog&quot;]

트리 구조:
        root
       /    \
      c      d
      |      |
      a      o
     / \     |
    t   r    g

&quot;ca&quot;를 공유 → 메모리 절약</code></pre><p><strong>특징</strong>:</p>
<pre><code>장점:
- 검색: O(m) (m: 문자열 길이)
- 접두사 검색 빠름
- 자동 완성에 유용

단점:
- 메모리 많이 사용
- 각 노드마다 자식 배열</code></pre><h3 id="트라이-구현">트라이 구현</h3>
<pre><code class="language-python">class TrieNode:
    &quot;&quot;&quot;
    트라이의 노드

    Attributes:
        children: 자식 노드 딕셔너리 {문자: TrieNode}
        is_end: 이 노드가 단어의 끝인가?
    &quot;&quot;&quot;    
    def __init__(self):
        self.children = {}
        self.is_end = False

class Trie:
    &quot;&quot;&quot;
    트라이 자료구조

    문자열 집합을 효율적으로 저장
    &quot;&quot;&quot;

    def __init__(self):
        &quot;&quot;&quot;루트 노드로 시작&quot;&quot;&quot;
        self.root = TrieNode()

    def insert(self, word):
        &quot;&quot;&quot;
        단어 삽입

        word: 삽입할 단어

        시간복잡도: O(m)
        - m: 단어 길이

        동작:
         1. 루트부터 시작
         2. 각 문자마다 자식 노드 확인
         3. 없으면 생성, 있으면 이동
         4. 마지막 노드에 is_end 표시
        &quot;&quot;&quot;
        node = self.root

        for char in word:
            # 자식 노드가 없으면 생성
            if char not in node.children:
                node.children[char] = TrieNode()

            # 다음 노드로 이동
            node = node.children[char]

        # 단어의 끝 표시
        node.is_end = True

    def search(self, word):
        &quot;&quot;&quot;
        단어 검색

        word: 검색할 단어

        Returns: bool: 단어가 존재하면 True

        시간복잡도: O(m)
        &quot;&quot;&quot;
        node = self.root

        for char in word:
            # 자식 노드가 없으면 단어 없음
            if char not in node.children:
                return False           
            node = node.children[char]

        # 단어의 끝이어야 함
        return node.is_end

    def starts_with(self, prefix):
        &quot;&quot;&quot;
        접두사로 시작하는 단어가 있는가?

        prefix: 접두사

        Returns: bool: 접두사를 가진 단어가 있으면 True

        시간복잡도: O(m)

        자동 완성에 유용!
        &quot;&quot;&quot;
        node = self.root

        for char in prefix:
            if char not in node.children:
                return False

            node = node.children[char]

        # 접두사까지 도달했으면 True (is_end 확인 불필요)
        return True

    def find_all_with_prefix(self, prefix):
        &quot;&quot;&quot;
        접두사로 시작하는 모든 단어 찾기

        prefix: 접두사

        Returns: 접두사를 가진 모든 단어 리스트

        자동 완성 기능!
        &quot;&quot;&quot;
        node = self.root

        # 접두사까지 이동
        for char in prefix:
            if char not in node.children:
                return []
            node = node.children[char]

        # 이 노드부터 DFS로 모든 단어 수집
        results = []
        self._collect_words(node, prefix, results)
        return results

    def _collect_words(self, node, current_word, results):
        &quot;&quot;&quot;
        DFS로 모든 단어 수집 (헬퍼 함수)
        &quot;&quot;&quot;
        # 단어의 끝이면 추가
        if node.is_end:
            results.append(current_word)

        # 모든 자식 탐색
        for char, child in node.children.items():
            self._collect_words(child, current_word + char, results)

# 사용 예시
trie = Trie()

# 단어 삽입
words = [&quot;cat&quot;, &quot;car&quot;, &quot;card&quot;, &quot;care&quot;, &quot;dog&quot;, &quot;dodge&quot;, &quot;door&quot;]
for word in words:
    trie.insert(word)

print(&quot;삽입된 단어:&quot;, words)

# 검색
print(&quot;\n검색:&quot;)
print(f&quot;&#39;car&#39; 존재? {trie.search(&#39;car&#39;)}&quot;)
print(f&quot;&#39;can&#39; 존재? {trie.search(&#39;can&#39;)}&quot;)

# 접두사 확인
print(&quot;\n접두사 확인:&quot;)
print(f&quot;&#39;ca&#39;로 시작? {trie.starts_with(&#39;ca&#39;)}&quot;)
print(f&quot;&#39;do&#39;로 시작? {trie.starts_with(&#39;do&#39;)}&quot;)
print(f&quot;&#39;da&#39;로 시작? {trie.starts_with(&#39;da&#39;)}&quot;)

# 자동 완성
print(&quot;\n자동 완성:&quot;)
prefix = &quot;car&quot;
suggestions = trie.find_all_with_prefix(prefix)
print(f&quot;&#39;{prefix}&#39;로 시작하는 단어: {suggestions}&quot;)

prefix = &quot;do&quot;
suggestions = trie.find_all_with_prefix(prefix)
print(f&quot;&#39;{prefix}&#39;로 시작하는 단어: {suggestions}&quot;)</code></pre>
<p><strong>트라이 시각화</strong>:</p>
<pre><code>단어: cat, car, card, care, dog, dodge, door

        root
       /    \
      c      d
      |      |
      a      o
      |     / \
      r    g   o
     /|\   |   |
    d e t  e   r
    |      |   |
 (card)(dodge)(door)

각 노드의 is_end:
- t: True (cat)
- r: True (car)
- d: True (card)
- e: True (care)
- g: True (dog)
- e: True (dodge)
- r: True (door)</code></pre><hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<p><strong>알고리즘 선택</strong></p>
<pre><code class="language-python"># 단순 패턴 매칭 → KMP
kmp_search(text, pattern)  # O(n+m)

# 여러 패턴 동시 검색 → 라빈-카프
rabin_karp_multiple(text, patterns)

# 사전, 자동 완성 → 트라이
trie = Trie()
trie.find_all_with_prefix(&quot;car&quot;)</code></pre>
<p><strong>성능 비교</strong></p>
<pre><code>알고리즘      시간        공간      특징
--------------------------------------------------
순진한       O(nm)       O(1)      간단
KMP         O(n+m)      O(m)      단일 패턴 최적
라빈-카프    O(n+m)평균   O(1)      다중 패턴
트라이       O(m)        O(총길이)  접두사 검색</code></pre><p><strong>실무 고려사항</strong></p>
<pre><code class="language-python"># 짧은 텍스트 → 순진한 방법도 OK
if len(text) &lt; 1000:
    naive_search(text, pattern)

# 긴 텍스트 → KMP
else:
    kmp_search(text, pattern)

# 여러 패턴 → 라빈-카프 또는 Aho-Corasick
rabin_karp_multiple(text, patterns)

# 자동 완성, 사전 → 트라이
trie = Trie()</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>패턴 매칭</strong></p>
<pre><code>순진한 방법:
- 모든 위치에서 비교
- O(nm)
- 간단하지만 느림

KMP:
- 실패 함수로 최적화
- O(n+m)
- 단일 패턴에 최적

라빈-카프:
- 해싱 이용
- O(n+m) 평균
- 다중 패턴에 유리</code></pre><p><strong>트라이</strong></p>
<pre><code>문자열 집합 저장:
- 공통 접두사 공유
- 검색: O(m)
- 자동 완성, 사전
- 메모리 많이 사용</code></pre><p><strong>시간복잡도 비교</strong></p>
<pre><code>연산            순진한    KMP     라빈-카프      트라이
--------------------------------------------------------
패턴 매칭      O(nm)    O(n+m)   O(n+m)평균     -
다중 패턴      O(nmk)   O(nmk)   O(n+mk)       O(n+m)
접두사 검색     -        -         -           O(m)
자동 완성       -        -         -           O(m+결과)</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[07-01] 시간 복잡도 (Time Complexity)</strong></p>
<ul>
<li>Big-O 표기법: 알고리즘 성능을 수학적으로 표현하기</li>
<li>점근적 분석: 입력 크기가 커질 때의 성능 이해</li>
<li>복잡도 계산: 다양한 알고리즘의 시간복잡도 분석</li>
<li>최선/평균/최악: 상황별 알고리즘 성능 비교</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[06-08] 그래프 알고리즘</a><br><strong>다음 글</strong>: <a href="#">[07-01] 시간 복잡도</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [06-08] 그래프 알고리즘]]></title>
            <link>https://velog.io/@road_to_ai/06-08-%EA%B7%B8%EB%9E%98%ED%94%84-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@road_to_ai/06-08-%EA%B7%B8%EB%9E%98%ED%94%84-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Thu, 19 Feb 2026 10:53:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>그래프 알고리즘은 최소 신장 트리, 최단 경로, 위상 정렬 등 그래프 구조에 특화된 문제를 효율적으로 해결하는 알고리즘들입니다.</p>
</blockquote>
<hr>
<h2 id="🎯-그래프-알고리즘이란">🎯 그래프 알고리즘이란</h2>
<h3 id="그래프-문제의-특징">그래프 문제의 특징</h3>
<p>그래프 자료구조를 배웠다면, 이제 그래프로 표현된 문제를 <strong>효율적으로 해결</strong>하는 알고리즘을 배울 차례입니다.</p>
<p><strong>자료구조 vs 알고리즘</strong>:</p>
<pre><code>자료구조 파트에서 배운 것:
- 그래프란? (정점, 간선, 용어)
- 표현 방법 (인접 행렬, 인접 리스트)
- 기본 탐색 (DFS, BFS)

알고리즘 파트에서 배울 것:
- 최소 비용 네트워크 (MST)
- 최단 경로 찾기
- 작업 순서 정하기 (위상 정렬)
- 연결성 분석</code></pre><p><strong>실생활 문제</strong>:</p>
<pre><code>통신망 구축:
- 모든 도시를 연결하되 비용 최소화
→ 최소 신장 트리 (MST)

내비게이션:
- 출발지에서 목적지까지 최단 경로
→ 최단 경로 알고리즘

프로젝트 일정:
- 선후 관계를 고려한 작업 순서
→ 위상 정렬</code></pre><hr>
<h2 id="🌳-최소-신장-트리-mst-minimum-spanning-tree">🌳 최소 신장 트리 (MST, Minimum Spanning Tree)</h2>
<h3 id="신장-트리란">신장 트리란?</h3>
<p><strong>신장 트리(Spanning Tree)</strong>와 <strong>최소 신장 트리(Minimum Spanning Tree, MST)</strong>를 이해해 봅시다.</p>
<p><strong>신장 트리</strong>:</p>
<pre><code>그래프의 모든 정점을 포함하면서 사이클이 없는 부분 그래프

특징:
- 모든 정점 포함
- 간선 개수 = 정점 개수 - 1
- 사이클 없음 (트리)

예:
원본 그래프:        신장 트리 중 하나:
  A─B─C              A─B─C
  │ │ │              │   
  D─E─F              D─E─F

간선 7개            간선 5개 (6개 정점 - 1)</code></pre><p><strong>최소 신장 트리</strong>:</p>
<pre><code>가중 그래프에서 총 가중치가 최소인 신장 트리

예:
    A ─2─ B
    │╲    │
    1  3  4
    │   ╲ │
    C ─5─ D

가능한 신장 트리:
1. A-C(1) + C-D(5) + A-B(2) = 8
2. A-C(1) + A-D(3) + A-B(2) = 6 ← 최소!
3. A-C(1) + A-D(3) + D-B(4) = 8

MST: A-C(1) + A-D(3) + A-B(2) = 6</code></pre><p><strong>실생활 응용</strong>:</p>
<pre><code>통신망 구축:
- 모든 도시를 연결
- 케이블 비용 최소화

전력망:
- 모든 건물에 전기
- 전선 길이 최소화

도로망:
- 모든 마을 연결
- 건설 비용 최소화</code></pre><h3 id="kruskal-알고리즘">Kruskal 알고리즘</h3>
<p><strong>Kruskal</strong>은 간선을 가중치 순으로 정렬하여 MST를 만드는 그리디 알고리즘입니다.</p>
<p><strong>핵심 아이디어</strong>:</p>
<pre><code>1. 모든 간선을 가중치 오름차순 정렬
2. 가장 작은 간선부터 선택
3. 사이클을 만들지 않으면 추가
4. V-1개 간선을 선택할 때까지 반복

&quot;욕심쟁이 방식&quot;
- 항상 가장 저렴한 간선 선택
- 사이클만 피하면 됨</code></pre><p><strong>사이클 판단</strong>: Union-Find 자료구조 사용</p>
<pre><code>Union-Find:
- 각 정점이 어느 집합에 속하는지 관리
- 같은 집합끼리 연결하면 사이클 발생

예:
간선 A-B 추가:
A 집합: {A}
B 집합: {B}
→ 다른 집합 → 연결 가능 → Union

간선 A-C 추가:
A 집합: {A, B}
C 집합: {C}
→ 다른 집합 → 연결 가능 → Union

간선 B-C 추가:
B 집합: {A, B, C}
C 집합: {A, B, C}
→ 같은 집합 → 사이클 발생! → 거부</code></pre><h3 id="kruskal-구현">Kruskal 구현</h3>
<pre><code class="language-python">class UnionFind:
    &quot;&quot;&quot;
    Union-Find 자료구조 (Disjoint Set)

    사이클 판단에 사용
    &quot;&quot;&quot;

    def __init__(self, n):
        &quot;&quot;&quot;
        n: 정점 개수
        &quot;&quot;&quot;
        # parent[i]: i의 부모 (초기: 자기 자신)
        self.parent = list(range(n))
        # rank[i]: i를 루트로 하는 트리의 높이
        self.rank = [0] * n

    def find(self, x):
        &quot;&quot;&quot;
        x가 속한 집합의 대표(루트) 찾기

        경로 압축(Path Compression) 최적화:
        - 찾는 과정에서 트리를 평평하게

        시간복잡도: O(α(n)) ≈ O(1)
        &quot;&quot;&quot;
        if self.parent[x] != x:
            # 재귀적으로 루트 찾기 + 경로 압축
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        &quot;&quot;&quot;
        x와 y가 속한 집합 합치기

        Union by Rank 최적화:
        - 높이가 낮은 트리를 높은 트리 아래 붙임

        Returns: bool: 합쳤으면 True, 이미 같은 집합이면 False
        &quot;&quot;&quot;
        root_x = self.find(x)
        root_y = self.find(y)

        # 이미 같은 집합
        if root_x == root_y:
            return False

        # Union by Rank
        if self.rank[root_x] &lt; self.rank[root_y]:
            self.parent[root_x] = root_y
        elif self.rank[root_x] &gt; self.rank[root_y]:
            self.parent[root_y] = root_x
        else:
            self.parent[root_y] = root_x
            self.rank[root_x] += 1

        return True

def kruskal(num_vertices, edges):
    &quot;&quot;&quot;
    Kruskal의 최소 신장 트리 알고리즘

    num_vertices: 정점 개수
    edges: [(u, v, weight), ...] 간선 리스트

    Returns: (최소 비용, MST 간선 리스트)

    시간복잡도: O(E log E)
    - 간선 정렬: O(E log E)
    - Union-Find: O(E × α(V)) ≈ O(E)

    알고리즘:
     1. 간선을 가중치 순 정렬
     2. 작은 것부터 선택
     3. 사이클 안 만들면 추가
    &quot;&quot;&quot;
    # 1. 간선을 가중치 오름차순 정렬
    edges.sort(key=lambda x: x[2])

    # Union-Find 초기화
    uf = UnionFind(num_vertices)

    mst_edges = []  # MST에 포함될 간선
    mst_cost = 0    # 총 비용

    # 2. 각 간선 확인
    for u, v, weight in edges:
        # 3. 사이클을 만들지 않으면 추가
        if uf.union(u, v):
            mst_edges.append((u, v, weight))
            mst_cost += weight

            # MST 완성 (V-1개 간선)
            if len(mst_edges) == num_vertices - 1:
                break

    return mst_cost, mst_edges

# 사용 예시
# 정점: 0, 1, 2, 3
# 간선: (u, v, weight)
edges = [
    (0, 1, 2),
    (0, 2, 1),
    (0, 3, 3),
    (1, 2, 3),
    (1, 3, 4),
    (2, 3, 5)
]

cost, mst = kruskal(4, edges)

print(f&quot;최소 신장 트리 비용: {cost}&quot;)
print(&quot;MST 간선:&quot;)
for u, v, w in mst:
    print(f&quot;  {u} - {v}: {w}&quot;)</code></pre>
<p><strong>실행 과정</strong>:</p>
<pre><code>간선 정렬:
(0,2,1), (0,1,2), (0,3,3), (1,2,3), (1,3,4), (2,3,5)

단계 1: (0,2,1) 선택
- find(0)=0, find(2)=2 (다른 집합)
- union(0,2) → MST에 추가
- 집합: {0,2}, {1}, {3}
- MST: [(0,2,1)], 비용: 1

단계 2: (0,1,2) 선택
- find(0)=0, find(1)=1 (다른 집합)
- union(0,1) → MST에 추가
- 집합: {0,1,2}, {3}
- MST: [(0,2,1), (0,1,2)], 비용: 3

단계 3: (0,3,3) 선택
- find(0)=0, find(3)=3 (다른 집합)
- union(0,3) → MST에 추가
- 집합: {0,1,2,3}
- MST: [(0,2,1), (0,1,2), (0,3,3)], 비용: 6

V-1=3개 간선 선택 완료!</code></pre><h3 id="prim-알고리즘">Prim 알고리즘</h3>
<p><strong>Prim</strong>은 정점을 하나씩 추가하며 MST를 만드는 그리디 알고리즘입니다.</p>
<p><strong>핵심 아이디어</strong>:</p>
<pre><code>1. 임의의 시작 정점 선택
2. MST에 포함된 정점들과 연결된 간선 중 최소 선택
3. 새 정점 추가
4. 모든 정점이 포함될 때까지 반복

&quot;점진적 확장&quot;
- 항상 현재 트리에서 가장 가까운 정점 추가</code></pre><p><strong>Kruskal vs Prim</strong>:</p>
<pre><code>Kruskal:
- 간선 중심
- 전체 간선 정렬
- Union-Find 사용
- 희소 그래프에 유리

Prim:
- 정점 중심
- 우선순위 큐 사용
- 시작점에서 확장
- 밀집 그래프에 유리</code></pre><h3 id="prim-구현">Prim 구현</h3>
<pre><code class="language-python">import heapq

def prim(num_vertices, adj_list, start=0):
    &quot;&quot;&quot;
    Prim의 최소 신장 트리 알고리즘

    num_vertices: 정점 개수
    adj_list: 인접 리스트 {v: [(neighbor, weight), ...]}
    start: 시작 정점

    Returns: (최소 비용, MST 간선 리스트)

    시간복잡도: O((V + E) log V)
    - 우선순위 큐 사용

    알고리즘:
    1. 시작 정점 선택
    2. 연결된 간선들을 우선순위 큐에 추가
    3. 최소 가중치 간선 선택
    4. 새 정점이면 MST에 추가
    &quot;&quot;&quot;
    visited = set()  # MST에 포함된 정점
    mst_edges = []
    mst_cost = 0

    # 우선순위 큐: (가중치, 정점1, 정점2)
    pq = []

    # 1. 시작 정점부터
    visited.add(start)

    # 시작 정점의 모든 간선을 큐에 추가
    for neighbor, weight in adj_list[start]:
        heapq.heappush(pq, (weight, start, neighbor))

    # 2. MST가 완성될 때까지
    while pq and len(visited) &lt; num_vertices:
        # 3. 최소 가중치 간선 선택
        weight, u, v = heapq.heappop(pq)

        # 이미 MST에 포함된 정점이면 건너뛰기
        if v in visited:
            continue

        # 4. MST에 추가
        visited.add(v)
        mst_edges.append((u, v, weight))
        mst_cost += weight

        # 새로 추가된 정점의 간선들을 큐에 추가
        for neighbor, w in adj_list[v]:
            if neighbor not in visited:
                heapq.heappush(pq, (w, v, neighbor))

    return mst_cost, mst_edges

# 사용 예시
# 인접 리스트로 표현
adj_list = {
    0: [(1, 2), (2, 1), (3, 3)],
    1: [(0, 2), (2, 3), (3, 4)],
    2: [(0, 1), (1, 3), (3, 5)],
    3: [(0, 3), (1, 4), (2, 5)]
}

cost, mst = prim(4, adj_list, start=0)

print(f&quot;최소 신장 트리 비용: {cost}&quot;)
print(&quot;MST 간선:&quot;)
for u, v, w in mst:
    print(f&quot;  {u} - {v}: {w}&quot;)</code></pre>
<hr>
<h2 id="🛣️-최단-경로-알고리즘">🛣️ 최단 경로 알고리즘</h2>
<h3 id="최단-경로-single-source-shortest-path-문제의-종류">최단 경로 (Single-Source Shortest Path) 문제의 종류</h3>
<p>최단 경로 문제는 여러 변형이 있습니다.</p>
<p><strong>1. 단일 출발점 최단 경로</strong></p>
<pre><code>한 정점에서 다른 모든 정점까지의 최단 경로

예: 내비게이션
출발: 서울
도착: 모든 도시

알고리즘:
- Dijkstra (양수 가중치)
- Bellman-Ford (음수 가중치 허용)</code></pre><p><strong>2. 모든 쌍 최단 경로 (All-Pairs Shortest Path)</strong></p>
<pre><code>모든 정점 쌍 사이의 최단 경로

예: 물류 네트워크
모든 도시 간 최단 거리 표

알고리즘:
- Floyd-Warshall</code></pre><h3 id="dijkstra-알고리즘-복습--확장">Dijkstra 알고리즘 (복습 + 확장)</h3>
<p>자료구조 파트에서 간단히 다뤘던 Dijkstra를 더 깊이 이해해 봅시다.</p>
<p><strong>제약 조건</strong>: 음수 가중치 불가</p>
<pre><code>왜 음수 가중치가 안 되나?

그래프:
  A ─2→ B
  ↓     ↓
 -5     1
  ↓     ↓
  C ─→  D

A에서 D로:
경로 1: A → B → D = 2 + 1 = 3
경로 2: A → C → D = -5 + ? (C→D 간선 필요)

문제:
Dijkstra는 &quot;확정된&quot; 정점을 다시 보지 않음
→ 음수 간선으로 더 짧아질 수 있음을 놓침</code></pre><h3 id="bellman-ford-알고리즘">Bellman-Ford 알고리즘</h3>
<p><strong>Bellman-Ford</strong>는 음수 가중치를 허용하는 최단 경로 알고리즘입니다.</p>
<p><strong>핵심 아이디어</strong>:</p>
<pre><code>모든 간선을 V-1번 반복하며 거리 갱신

왜 V-1번?
- 최단 경로는 최대 V-1개 간선
- V번째에도 갱신되면? → 음수 사이클 존재!

음수 사이클:
  A → B → C → A (총 비용: -1)

계속 돌면 무한히 짧아짐 → 최단 경로 없음</code></pre><h3 id="bellman-ford-구현">Bellman-Ford 구현</h3>
<pre><code class="language-python">def bellman_ford(num_vertices, edges, start):
    &quot;&quot;&quot;
    Bellman-Ford 최단 경로 알고리즘

    num_vertices: 정점 개수
    edges: [(u, v, weight), ...] 간선 리스트
    start: 시작 정점

    Returns: (거리 딕셔너리, 음수 사이클 존재 여부)

    시간복잡도: O(VE)
    - V-1번 반복
    - 각 반복마다 E개 간선 확인

    특징:
    - 음수 가중치 허용
    - 음수 사이클 탐지
    - Dijkstra보다 느림
    &quot;&quot;&quot;
    # 거리 초기화
    dist = {v: float(&#39;inf&#39;) for v in range(num_vertices)}
    dist[start] = 0

    # V-1번 반복
    for _ in range(num_vertices - 1):
        # 모든 간선에 대해 relaxation
        updated = False
        for u, v, weight in edges:
            # u를 거쳐 v로 가는 것이 더 짧으면
            if dist[u] != float(&#39;inf&#39;) and dist[u] + weight &lt; dist[v]:
                dist[v] = dist[u] + weight
                updated = True

        # 갱신이 없으면 조기 종료 (최적화)
        if not updated:
            break

    # 음수 사이클 확인 (V번째 반복)
    has_negative_cycle = False
    for u, v, weight in edges:
        if dist[u] != float(&#39;inf&#39;) and dist[u] + weight &lt; dist[v]:
            has_negative_cycle = True
            break

    return dist, has_negative_cycle

# 사용 예시
edges = [
    (0, 1, 4),
    (0, 2, 1),
    (2, 1, 2),
    (1, 3, 1),
    (2, 3, 5)
]

dist, has_cycle = bellman_ford(4, edges, start=0)

print(&quot;최단 거리:&quot;)
for v, d in sorted(dist.items()):
    print(f&quot;  0 → {v}: {d}&quot;)

if has_cycle:
    print(&quot;\n음수 사이클 존재!&quot;)
else:
    print(&quot;\n음수 사이클 없음&quot;)

# 음수 가중치 예제
edges_negative = [
    (0, 1, 1),
    (1, 2, -3),
    (2, 3, 1),
    (0, 3, 4)
]

print(&quot;\n\n음수 가중치 그래프:&quot;)
dist2, has_cycle2 = bellman_ford(4, edges_negative, start=0)

for v, d in sorted(dist2.items()):
    print(f&quot;  0 → {v}: {d}&quot;)</code></pre>
<h3 id="floyd-warshall-알고리즘">Floyd-Warshall 알고리즘</h3>
<p><strong>Floyd-Warshall</strong>은 모든 정점 쌍 사이의 최단 경로를 구하는 동적 계획법 알고리즘입니다.</p>
<p><strong>핵심 아이디어</strong>:</p>
<pre><code>3중 반복문으로 모든 경로 확인

for k in 정점들:  # 중간 경유지
    for i in 정점들:  # 출발
        for j in 정점들:  # 도착
            # i → k → j가 더 짧으면 갱신
            if dist[i][k] + dist[k][j] &lt; dist[i][j]:
                dist[i][j] = dist[i][k] + dist[k][j]

k를 거쳐가는 것이 더 빠른지 확인!</code></pre><h3 id="floyd-warshall-구현">Floyd-Warshall 구현</h3>
<pre><code class="language-python">def floyd_warshall(num_vertices, edges):
    &quot;&quot;&quot;
    Floyd-Warshall 모든 쌍 최단 경로

    num_vertices: 정점 개수
    edges: [(u, v, weight), ...] 간선 리스트

    Returns: 거리 행렬 (2차원 리스트)

    시간복잡도: O(V³)
    공간복잡도: O(V²)

    특징:
    - 모든 쌍 최단 경로
    - 동적 계획법
    - 음수 가중치 허용 (음수 사이클 제외)
    - 코드 간단
    &quot;&quot;&quot;
    # 거리 행렬 초기화
    INF = float(&#39;inf&#39;)
    dist = [[INF] * num_vertices for _ in range(num_vertices)]

    # 자기 자신까지의 거리는 0
    for i in range(num_vertices):
        dist[i][i] = 0

    # 간선 정보 입력
    for u, v, weight in edges:
        dist[u][v] = weight

    # Floyd-Warshall
    # k: 중간 경유지
    for k in range(num_vertices):
        # i: 출발점
        for i in range(num_vertices):
            # j: 도착점
            for j in range(num_vertices):
                # i → k → j가 더 짧으면 갱신
                if dist[i][k] + dist[k][j] &lt; dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]   
    return dist

# 사용 예시
edges = [
    (0, 1, 3),
    (0, 2, 8),
    (0, 3, -4),
    (1, 3, 1),
    (1, 4, 7),
    (2, 1, 4),
    (3, 2, -5),
    (3, 4, 6),
    (4, 0, 2)
]

dist_matrix = floyd_warshall(5, edges)

print(&quot;모든 쌍 최단 거리:&quot;)
print(&quot;     &quot;, end=&quot;&quot;)
for j in range(5):
    print(f&quot;{j:5d}&quot;, end=&quot;&quot;)
print()

for i in range(5):
    print(f&quot;{i}: &quot;, end=&quot;&quot;)
    for j in range(5):
        if dist_matrix[i][j] == float(&#39;inf&#39;):
            print(&quot;  INF&quot;, end=&quot;&quot;)
        else:
            print(f&quot;{dist_matrix[i][j]:5d}&quot;, end=&quot;&quot;)
    print()</code></pre>
<hr>
<h2 id="📋-위상-정렬-topological-sort">📋 위상 정렬 (Topological Sort)</h2>
<h3 id="위상-정렬이란">위상 정렬이란?</h3>
<p><strong>위상 정렬</strong>은 방향 그래프에서 정점들을 선후 관계를 만족하도록 일렬로 나열하는 것입니다.</p>
<p><strong>실생활 예시</strong>:</p>
<pre><code>대학 수강 신청:
- 자료구조를 듣기 전에 프로그래밍 기초
- 알고리즘을 듣기 전에 자료구조
- ...

프로그래밍 기초 → 자료구조 → 알고리즘

프로젝트 일정:
- 설계 완료 후 개발 시작
- 개발 완료 후 테스트 시작
- ...

설계 → 개발 → 테스트 → 배포</code></pre><p><strong>조건</strong>:</p>
<pre><code>1. 방향 그래프 (DAG: Directed Acyclic Graph)
2. 사이클 없음 (필수!)

사이클이 있으면?
A → B → C → A

A를 먼저? 그럼 C는?
C를 먼저? 그럼 B는?
B를 먼저? 그럼 A는?

→ 불가능!</code></pre><h3 id="위상-정렬-dfs-기반">위상 정렬: DFS 기반</h3>
<p><strong>핵심 아이디어</strong>:</p>
<pre><code>DFS로 탐색하되, 종료 시점에 스택에 추가
→ 스택을 뒤집으면 위상 정렬!

왜?
- DFS는 &quot;끝&quot;부터 완료
- 의존하는 것이 없는 것부터 완료
- 역순이 위상 정렬</code></pre><h3 id="dfs-기반-위상-정렬-구현">DFS 기반 위상 정렬 구현</h3>
<pre><code class="language-python">def topological_sort_dfs(num_vertices, adj_list):
    &quot;&quot;&quot;
    위상 정렬 - DFS 기반

    num_vertices: 정점 개수
    adj_list: 인접 리스트 {v: [neighbors]}

    Returns: - 위상 정렬 결과 (리스트)
             - 사이클이 있으면 None

    시간복잡도: O(V + E)
    &quot;&quot;&quot;
    visited = set()      # 방문한 정점
    rec_stack = set()    # 현재 재귀 스택 (사이클 탐지용)
    result = []          # 결과 (역순으로 추가됨)
    has_cycle = False

    def dfs(v):
        &quot;&quot;&quot;DFS + 사이클 탐지&quot;&quot;&quot;
        nonlocal has_cycle

        # 사이클 탐지
        if v in rec_stack:
            has_cycle = True
            return

        if v in visited:
            return

        # 방문 표시
        visited.add(v)
        rec_stack.add(v)

        # 인접 정점 탐색
        for neighbor in adj_list.get(v, []):
            dfs(neighbor)
            if has_cycle:
                return

        # 현재 정점 처리 완료
        rec_stack.remove(v)
        result.append(v)  # 종료 시 추가 (역순)

    # 모든 정점에 대해 DFS
    for v in range(num_vertices):
        if v not in visited:
            dfs(v)
            if has_cycle:
                return None  # 사이클 있음

    # 역순이 위상 정렬
    return result[::-1]

# 사용 예시
# 과목 선수 관계
# 0: 프로그래밍 기초
# 1: 자료구조
# 2: 알고리즘
# 3: 운영체제
# 4: 데이터베이스

adj_list = {
    0: [1],      # 프로그래밍 기초 → 자료구조
    1: [2],      # 자료구조 → 알고리즘
    0: [3],      # 프로그래밍 기초 → 운영체제
    1: [4],      # 자료구조 → 데이터베이스
}

# 제대로 표현 (중복 키 수정)
adj_list = {
    0: [1, 3],   # 프로그래밍 기초 → 자료구조, 운영체제
    1: [2, 4],   # 자료구조 → 알고리즘, 데이터베이스
    2: [],
    3: [],
    4: []
}

result = topological_sort_dfs(5, adj_list)

if result:
    print(&quot;수강 순서:&quot;)
    course_names = [&quot;프로그래밍 기초&quot;, &quot;자료구조&quot;, &quot;알고리즘&quot;, &quot;운영체제&quot;, &quot;데이터베이스&quot;]
    for v in result:
        print(f&quot;  {v}: {course_names[v]}&quot;)
else:
    print(&quot;사이클 존재! (선수 관계 순환)&quot;)</code></pre>
<h3 id="kahn-알고리즘-진입-차수-기반">Kahn 알고리즘 (진입 차수 기반)</h3>
<p>진입 차수(indegree)가 0인 노드부터 BFS(너비 우선 탐색) 방식으로 순차적으로 방문하여 </p>
<p>DAG(방향 비순환 그래프)의 위상 정렬을 수행하는 효율적인 알고리즘입니다. </p>
<p>진입 차수가 0인 노드를 큐에 넣고, 해당 노드와 연결된 간선을 제거하며 반복하는 방식입니다.</p>
<p><strong>핵심 아이디어</strong>:</p>
<pre><code>1. 진입 차수가 0인 정점을 큐에 추가
2. 큐에서 정점 꺼내기
3. 그 정점이 가리키는 정점들의 진입 차수 감소
4. 진입 차수가 0이 되면 큐에 추가
5. 반복

진입 차수 (Indegree):
- 들어오는 간선의 개수
- 선수 과목의 개수</code></pre><h3 id="kahn-알고리즘-구현">Kahn 알고리즘 구현</h3>
<pre><code class="language-python">from collections import deque

def topological_sort_kahn(num_vertices, edges):
    &quot;&quot;&quot;
    위상 정렬 - Kahn 알고리즘 (진입 차수 기반)

    num_vertices: 정점 개수
    edges: [(u, v), ...] 간선 리스트 (u → v)

    Returns: - 위상 정렬 결과
             - 사이클이 있으면 None

    시간복잡도: O(V + E)
    &quot;&quot;&quot;
    # 인접 리스트와 진입 차수 계산
    adj_list = {v: [] for v in range(num_vertices)}
    indegree = {v: 0 for v in range(num_vertices)}

    for u, v in edges:
        adj_list[u].append(v)
        indegree[v] += 1

    # 진입 차수가 0인 정점들로 시작
    queue = deque()
    for v in range(num_vertices):
        if indegree[v] == 0:
            queue.append(v) 
    result = []

    while queue:
        # 진입 차수 0인 정점 꺼내기
        v = queue.popleft()
        result.append(v)

        # 이 정점이 가리키는 정점들의 진입 차수 감소
        for neighbor in adj_list[v]:
            indegree[neighbor] -= 1

            # 진입 차수가 0이 되면 큐에 추가
            if indegree[neighbor] == 0:
                queue.append(neighbor)

    # 모든 정점을 방문했는가?
    if len(result) == num_vertices:
        return result
    else:
        return None  # 사이클 존재

# 사용 예시
edges = [
    (0, 1),  # 프로그래밍 기초 → 자료구조
    (0, 3),  # 프로그래밍 기초 → 운영체제
    (1, 2),  # 자료구조 → 알고리즘
    (1, 4),  # 자료구조 → 데이터베이스
]

result = topological_sort_kahn(5, edges)

if result:
    print(&quot;수강 순서 (Kahn):&quot;)
    course_names = [&quot;프로그래밍 기초&quot;, &quot;자료구조&quot;, &quot;알고리즘&quot;, &quot;운영체제&quot;, &quot;데이터베이스&quot;]
    for v in result:
        print(f&quot;  {v}: {course_names[v]}&quot;)
else:
    print(&quot;사이클 존재!&quot;)</code></pre>
<hr>
<h2 id="💡-실무-팁">💡 실무 팁</h2>
<p><strong>MST 알고리즘 선택</strong></p>
<pre><code class="language-python"># 희소 그래프 → Kruskal
# 간선이 적음
kruskal(vertices, edges)  # O(E log E)

# 밀집 그래프 → Prim
# 간선이 많음
prim(vertices, adj_list)  # O((V+E) log V)</code></pre>
<p><strong>최단 경로 알고리즘 선택</strong></p>
<pre><code class="language-python"># 단일 출발점 + 양수 가중치 → Dijkstra
dijkstra(graph, start)  # O((V+E) log V)

# 단일 출발점 + 음수 가중치 → Bellman-Ford
bellman_ford(graph, start)  # O(VE)

# 모든 쌍 → Floyd-Warshall
floyd_warshall(graph)  # O(V³)

# 작은 그래프면 Floyd-Warshall
# 큰 그래프면 Dijkstra V번</code></pre>
<p><strong>위상 정렬 선택</strong></p>
<pre><code class="language-python"># 사이클 탐지도 필요 → DFS
topological_sort_dfs(graph)

# 단순 위상 정렬 → Kahn
topological_sort_kahn(graph)

# 둘 다 O(V+E), 취향 차이</code></pre>
<hr>
<h2 id="🎯-핵심-정리">🎯 핵심 정리</h2>
<p><strong>최소 신장 트리</strong></p>
<pre><code>목표: 모든 정점 연결 + 최소 비용

Kruskal:
- 간선 정렬 후 선택
- Union-Find
- O(E log E)

Prim:
- 정점 확장
- 우선순위 큐
- O((V+E) log V)</code></pre><p><strong>최단 경로</strong></p>
<pre><code>Dijkstra:
- 단일 출발점
- 양수 가중치
- O((V+E) log V)

Bellman-Ford:
- 단일 출발점
- 음수 가중치 허용
- O(VE)

Floyd-Warshall:
- 모든 쌍
- 동적 계획법
- O(V³)</code></pre><p><strong>위상 정렬</strong></p>
<pre><code>목표: 선후 관계를 만족하는 순서

DFS 기반:
- 재귀
- 사이클 탐지

Kahn:
- 진입 차수
- 큐 사용

둘 다 O(V+E)</code></pre><hr>
<h2 id="🔗-다음-글에서는">🔗 다음 글에서는</h2>
<p><strong>[06-09] 문자열 알고리즘</strong></p>
<ul>
<li>문자열 매칭: 패턴을 효율적으로 찾는 다양한 방법</li>
<li>KMP 알고리즘: 실패 함수를 이용한 빠른 탐색</li>
<li>라빈-카프: 해싱을 이용한 패턴 매칭</li>
<li>트라이 자료구조: 문자열 집합의 효율적 저장과 검색</li>
</ul>
<hr>
<p><strong>이전 글</strong>: <a href="#">[06-07] 무작위 알고리즘</a><br><strong>다음 글</strong>: <a href="#">[06-09] 문자열 알고리즘</a><br><strong>시리즈</strong>: <a href="#">P1. Computer Science</a></p>
]]></description>
        </item>
    </channel>
</rss>