문제 분류 수업 진도 대회 채점현황 순위 질문게시판
게임
스도쿠 타이핑 리듬 타자게임 타이핑 테트리스 타이핑 인베이더 타이핑 지뢰찾기 8192 공룡게임 타이핑 플래피버드 랭킹
로그인
← 수업 목록
▶ 슬라이드 시작

Ch 8. 알고리즘 설계 기법 (Algorithm Design Topics)

수업 목표

본 수업은 시간 복잡도 분석의 고급 기법인 분할 상환 분석(Amortized Analysis)의 원리를 이해하고, 이와 연계된 투 포인터(Two Pointers), 비트 평행(Bit-Parallel) 최적화, 그리고 삼분 탐색(Ternary Search)을 활용한 볼록 함수 극값 찾기 기법을 학습하는 것을 목표로 합니다. - 전체 연산량을 기준으로 평균 속도를 분석하는 분할 상환 기법의 개념을 파악합니다. - 슬라이딩 윈도우 기법에 딕셔너리를 결합하여 조건 만족 서브배열의 개수를 $O(N)$에 세는 법을 습득합니다. - 파이썬 대형 정수의 비트 연산 특성을 활용하여 부분집합의 합 문제를 고속화하는 비트셋 기법을 적용합니다. - 삼분 탐색을 통해 볼록 함수의 최솟값 또는 최댓값을 실수 및 정수 오차 없이 안정적으로 찾는 탐색 프로세스를 구현합니다.

학습 동기부여

단순히 모든 단계를 독립적으로 보고 시간 복잡도를 계산하면 $O(N^2)$ 처럼 지나치게 비관적인 수치가 도출될 수 있습니다. 그러나 각 단계의 개별 연산량이 다를지라도 '전체 단계 동안 일어나는 총 연산량'의 합이 작다면 평균 복잡도를 대폭 낮출 수 있습니다. 이를 분할 상환 분석이라고 하며, 투 포인터나 슬라이딩 윈도우 알고리즘의 타당성을 증명하는 기초가 됩니다. 또한, 하드웨어 연산 구조를 활용해 64비트 정수 연산 한 번으로 64개의 연산을 일시에 처리하는 비트셋 최적화나, 기울기의 증감을 기반으로 극값을 찾는 삼분 탐색은 수학과 알고리즘이 결합되어 실질 성능을 극대화하는 강력한 방법들입니다.

도입 스토리

기차 놀이를 할 때, 기차의 맨 앞 머리와 맨 뒤 꼬리를 조절하며 기차 안에 들어있는 아이들의 고유 색상 개수가 제한값을 넘지 않게 조절하는 과정을 상상해 봅시다. 기차가 앞으로 나아가면서 머리가 한 칸 전진하고 꼬리가 한 칸 따라올 때, 머리와 꼬리가 기차 전체를 훑는 횟수는 총 2번($2N$)을 넘지 않습니다. 따라서 개별 단계에서 꼬리가 여러 칸 움직이더라도 전체 기차 이동 속도는 $O(N)$에 비례합니다. 또한, 여러 건물의 높이를 통일시키는 최소 조절 비용 함수처럼, 어떤 점 $X$를 기준으로 비용이 점차 감소했다가 다시 증가하는 볼록한 골짜기 형태의 그래프를 마주했을 때, 우리는 골짜기의 중앙 삼등분점을 탐색하며 아주 영리하게 최소 비용을 만족하는 지점을 찾아낼 수 있습니다. 본 수업에서는 이러한 연산 최적화 설계 방식을 다룹니다.

1. 분할 상환 분석과 슬라이딩 윈도우 — 중복 없는 서브배열

서로 다른 원소의 개수가 최대 $K$개 이하인 연속된 부분 배열(서브배열)의 개수를 구합니다. 투 포인터를 사용하여 오른쪽 포인터 r을 전진시키며 딕셔너리에 빈도를 기록하고, 고유 원소 개수가 $K$를 초과하면 왼쪽 포인터 l을 당겨 고유 원소 개수를 $K$ 이하로 리셋합니다. r과 l은 각각 최대 $N$번만 전진하므로 전체 연산 횟수는 $2N$에 수렴하여 $O(N)$ 복잡도를 보장받습니다.
# 8045번: 중복 없는 서브배열 — O(N) 투 포인터
import sys
from collections import deque
input = sys.stdin.readline

n, k = map(int, input().split())
arr = list(map(int, input().split()))

counts = {}
l = 0
ans = 0
unique_cnt = 0

for r in range(n):
    val = arr[r]
    counts[val] = counts.get(val, 0) + 1
    if counts[val] == 1:
        unique_cnt += 1
        
    # 고유 원소 개수가 K를 초과하는 동안 왼쪽 포인터를 축소
    while unique_cnt > k:
        left_val = arr[l]
        counts[left_val] -= 1
        if counts[left_val] == 0:
            unique_cnt -= 1
        l += 1
        
    # r을 끝자리로 갖는 만족 서브배열의 개수 누적
    ans += (r - l + 1)

print(ans)

2. 투 포인터 심화 — 세 수의 합

배열의 서로 다른 세 수의 합이 정확히 $X$가 되는 위치를 찾을 때, 정렬을 거친 뒤 첫 번째 수 i를 고정하고 나머지 부분 구간에 대해 양방향 투 포인터를 탐색하는 방식을 적용하여 $O(N^2)$ 복잡도 안에 풀 수 있습니다.
# 8046번: 세 수의 합 — 정렬 후 O(N^2) 투 포인터
import sys
input = sys.stdin.readline

n, x = map(int, input().split())
vals = list(map(int, input().split()))

# 원래 인덱스를 유지하기 위해 (값, 원래 인덱스) 리스트 구성 후 정렬
arr = sorted([(vals[i], i + 1) for i in range(n)])

found = False
for i in range(n - 2):
    target = x - arr[i][0]
    l = i + 1
    r = n - 1
    while l < r:
        curr_sum = arr[l][0] + arr[r][0]
        if curr_sum == target:
            # 1-based 인덱스 출력
            res = sorted([arr[i][1], arr[l][1], arr[r][1]])
            print(*res)
            found = True
            break
        elif curr_sum < target:
            l += 1
        else:
            r -= 1
    if found:
        break

if not found:
    print("IMPOSSIBLE")

3. 비트 평행 알고리즘 — 부분집합의 합 (Bitset 최적화)

파이썬의 정수형은 메모리가 허용하는 한 비트 크기에 제한이 없습니다. 부분집합 합 문제에서 1차원 DP 테이블의 모든 값들을 거대한 하나의 정수(비트열)로 표현하면, 새로운 무게 $w$를 가산하는 전이 연산을 비트 시프트 연산 dp |= (dp << w) 단 한 줄로 수행할 수 있어 대량의 연산을 상수 시간에 가깝게($O(X \cdot N / 64)$) 한꺼번에 처리하는 비트 평행(Bit-Parallel) 효과를 냅니다.
# 8047번: 부분집합의 합 — 파이썬 고속 비트셋 연산
import sys
input = sys.stdin.readline

n, x = map(int, input().split())
w = list(map(int, input().split()))

# dp 비트셋 생성 (0번째 비트 1로 켜짐, 즉 dp[0]=True)
dp = 1

for val in w:
    # dp |= (dp << val)
    # val만큼 비트를 왼쪽으로 밀어서(시프트) 교집합 비트 합산
    dp |= (dp << val)

# 목표 값 x번째 비트가 켜져있는지 확인
if (dp >> x) & 1:
    print("YES")
else:
    print("NO")

4. 볼록 함수 극값 탐색 — 삼분 탐색 (Ternary Search)

삼분 탐색은 오목하거나 볼록한 함수에서 극값을 찾을 때 유용합니다. 탐색 구간 $[low, high]$를 3등분하는 m1, m2 위치에서의 비용을 비교하여 최소 비용을 포함하는 3분의 2 영역으로 탐색 구간을 좁혀나갑니다. $O(N \log(\text{max\_H}))$ 시간에 동작합니다.
# 8048번: 건물 높이 통일 — 삼분 탐색 구현
import sys
input = sys.stdin.readline

def get_cost(x, h, c):
    return sum(c[i] * abs(h[i] - x) for i in range(len(h)))

def solve():
    n = int(input())
    h = list(map(int, input().split()))
    c = list(map(int, input().split()))
    
    low = 0
    high = 10000
    
    # 정수 범위 삼분 탐색
    while high - low > 2:
        m1 = low + (high - low) // 3
        m2 = high - (high - low) // 3
        
        cost1 = get_cost(m1, h, c)
        cost2 = get_cost(m2, h, c)
        
        if cost1 < cost2:
            high = m2
        else:
            low = m1
            
    # 후보 정수 범위 내에서 최소값 검색
    ans = min(get_cost(x, h, c) for x in range(low, high + 1))
    print(ans)

t = int(input())
for _ in range(t):
    solve()

자주 하는 실수 & 오류 예방

- 비트 시프트 오프셋 초과 오류: 비트 연산 기반의 최적화를 적용할 때 목표값 $X$의 크기가 지나치게 커지면(예: $10^9$ 이상) 메모리 초과를 일으킵니다. $X \le 10^5$ 수준의 한도 조건이 있을 때만 비트셋 기법을 안전하게 적용하십시오. - 삼분 탐색 정수 나눗셈 소실 경계: 삼분 탐색 구간 $[low, high]$가 3 미만으로 좁혀졌을 때 정수 나눗셈 // 3을 반복하면 무한 루프에 빠지거나 경계 계산 오류를 유발합니다. 반드시 high - low > 2 조건으로 루프를 끊고 남은 3개 이하의 정수 후보들에 대해 직접 최솟값을 계산하십시오. - 투 포인터의 자료형 오버플로우: 중복 없는 서브배열 문제에서 결과 가짓수 ans는 최댓값 $N(N+1)/2$에 달해 32비트 정수 범위($2 \times 10^9$)를 가볍게 넘을 수 있습니다. 결과 누적용 변수 할당 시 오버플로우가 나지 않도록 구현하십시오.

핵심 정리

알고리즘 유형 대표 기법 핵심 시간 복잡도 주요 작동 방식 및 의의
분할 상환 분석 투 포인터 / 슬라이딩 윈도우 $O(N)$ 포인터들의 단방향 이동을 통해 평균 시간 소요 단축
비트 평행 최적화 정수 비트셋 연산 $O(N \cdot X / 64)$ 메모리 비트 시프트를 통해 DP 전이 루프 병렬화
삼분 탐색 Ternary Search $O(N \log H)$ 삼등분점 함수값 비교를 통해 볼록 영역 극값 추출

실습 문제

문제 번호 문제 제목 출처 난이도 핵심 알고리즘
8045 중복 없는 서브배열 CSES 보통 투 포인터 & 슬라이딩 윈도우
8046 세 수의 합 CSES 보통 정렬 & 투 포인터 응용
8047 부분집합의 합 SPOJ 보통 부분집합 DP & 비트셋 최적화
8048 건물 높이 통일 SPOJ 보통 볼록 함수 비용 계산 & 삼분 탐색
NOTICE  —  BFStudy OJ 정보샘
애들아, 문제 많이 풀고 게임 잠깐 하는거지?
— 정보샘