알고리즘 입문
개념 배우기직접 풀어보기
놀면 뭐 하니,
그냥 하는거야.

집합을 숫자 하나로! 비트마스킹

코딩냥

안녕, 나 코딩냥이다냥! 오늘은 정수 하나로 집합을 표현하는 마법을 배운다냥. 비트 하나가 원소 하나의 있다/없다를 나타낸다냥.

11장 완전탐색의 부분집합을 훨씬 깔끔하게 만들 수 있고, 비트 DP 라는 강력한 도구로도 이어진다냥.

오늘의 학습 목표

  • 비트로 집합을 표현한다.
  • 원소를 넣고 빼고 확인한다 (| & ^).
  • 2ⁿ 개의 부분집합을 훑는다.
  • 같은 순회로 XOR·OR 조건을 센다.
  • 비트 DP 의 맛을 본다.

1단계 · 비트가 집합이라냥 (10분)

코딩냥

정수를 2진수로 보면 0과 1의 줄이라냥. 각 자리를 원소 하나로 보면, 1 은 "있다", 0 은 "없다" 라냥. 그럼 정수 하나가 곧 집합이라냥.

비유로 이해하기 — 전등 스위치 줄

스위치 20개가 줄지어 있다냥. 켜진 스위치가 "그 원소가 집합에 있다" 라냥. 스위치 전체의 상태를 숫자 하나로 적어두는 게 비트마스크라냥.

{1, 3} 은 1번과 3번 스위치만 켜진 것 → 2진수 101 → 숫자 5 라냥.

연산비트 연산이 곧 집합 연산이라냥

i번 원소는 1 << (i-1) 이라는 비트로 나타낸다냥 (1번은 맨 오른쪽).

  • 넣기: mask |= (1 << (i-1)) — 그 비트를 켠다
  • 빼기: mask &= ~(1 << (i-1)) — 그 비트를 끈다
  • 확인: mask & (1 << (i-1)) — 켜져 있으면 0 이 아니다
  • 토글: mask ^= (1 << (i-1)) — 켜져 있으면 끄고, 꺼져 있으면 켠다

전체와 비움도 간단하다냥.

  • 1~20 을 모두 켜기: mask = (1 << 20) - 1
  • 통째로 비우기: mask = 0

오늘 1번 문제가 이 여섯 연산이라냥. 집합 자료구조 없이 정수 하나로 다 된다냥.

2단계 · 넣고 빼고 확인하기 (10분)

코딩냥

1번 문제를 미리 맛보자냥. 연산 코드에 따라 위의 비트 연산을 골라 쓰면 된다냥.

mask = 0
FULL = (1 << 20) - 1
for _ in range(q):
    code, x = map(int, input().split())
    if code == 1:                          # 넣기
        mask |= 1 << (x - 1)
    elif code == 2:                        # 빼기
        mask &= ~(1 << (x - 1))
    elif code == 3:                        # 확인
        print(1 if mask & (1 << (x - 1)) else 0)
    elif code == 4:                        # 토글
        mask ^= 1 << (x - 1)
    elif code == 5:                        # 전체
        mask = FULL
    elif code == 6:                        # 비움
        mask = 0

1 << (x - 1) 은 "x번 자리만 켠 비트" 라냥. x 가 1이면 맨 오른쪽, x 가 20이면 스무 번째 자리라냥. 전체·비움 연산에서는 x 를 쓰지 않으니 비트를 계산하지 말자냥. (x 가 0으로 들어와서 1 << -1 이 되면 에러라냥.)

3단계 · 2ⁿ 부분집합 훑기 (13분)

코딩냥

이게 비트마스킹의 진짜 힘이라냥. 11장에서 재귀로 뻗던 모든 부분집합을, 이제 반복문 하나로 훑는다냥.

핵심0부터 2ⁿ-1 까지가 모든 부분집합이라냥

원소가 n개면 부분집합은 2ⁿ개라냥. 그런데 0 부터 2ⁿ - 1 까지의 정수가 정확히 그 부분집합들이라냥. 각 정수의 켜진 비트가 고른 원소라냥.

for mask in range(1 << n):        # 모든 부분집합
    for i in range(n):
        if mask & (1 << i):       # i 번째를 골랐냥?
            ...                   # nums[i] 를 쓴다

11장에서 본 그 부분집합이라냥

11장에서는 "각 원소를 고르거나 안 고르거나" 를 재귀로 뻗었다냥. 그게 바로 각 비트를 1로 하거나 0으로 하는 것과 똑같다냥.

재귀 대신 mask 를 0 부터 세면, 재귀 없이 모든 경우가 나온다냥. 코드가 훨씬 짧아진다냥.

크기가 1 이상인 부분집합만 셀 땐 1 부터라냥.

mask = 0 은 아무것도 안 고른 빈 부분집합이라냥. 빈 집합을 빼려면 range(1, 1 << n) 으로 1 부터 돌면 된다냥.

오늘 2번 문제(XOR)에서 빈 집합을 실수로 세면 틀린다냥. 특히 목표가 0 일 때, 빈 집합의 XOR 도 0 이라 하나가 더 세진다냥.

4단계 · 같은 순회, 다른 연산 (12분)

코딩냥

2ⁿ 순회 하나로 여러 문제를 푼다냥. 부분집합 안의 원소들을 무엇으로 합치느냐만 바뀐다냥.

for mask in range(1, 1 << n):
    x = 0
    for i in range(n):
        if mask & (1 << i):
            x ^= nums[i]      # XOR 로 합치면 2번 문제
            # x |= nums[i]    # OR 로 합치면 3번 문제
    if x == target:
        cnt += 1
정리XOR 이냐 OR 이냐
  • 2번 문제 — 부분집합의 XOR 이 목표 k 가 되는 개수
  • 3번 문제 — 부분집합의 OR 이 전체 OR 과 같아지는 개수

순회는 똑같고, ^= 냐 |= 냐, 목표가 k 냐 "전체 OR" 이냐만 다르다냥. 비트마스킹의 유연함이라냥.

n 이 커지면 2ⁿ 도 폭발한다냥. n 이 16이면 6만 5천쯤이라 괜찮지만, 30이 넘으면 못 쓴다냥. 그래서 이 방식은 n 이 작을 때 쓰는 무기라냥. 오늘 2·3번은 n 이 16까지라 안전하다냥.

5단계 · 비트 DP 맛보기 (13분)

코딩냥

마지막이라냥. 마스크를 DP 의 상태로 쓰는 거라냥. "어떤 원소들을 이미 처리했나" 를 마스크 하나로 기억한다냥.

배정일 나눠주기 (5번 문제)

사람 n명에게 일 n개를 하나씩 배정해 비용을 최소로 하고 싶다냥.

  • 상태: dp[mask] = 이미 끝낸 일들의 집합이 mask 일 때 최소 비용
  • 다음 사람 번호 = 켜진 비트 수(mask 에 들어간 일의 수)
  • 그 사람에게 아직 안 한 일 하나를 맡기며 mask 에 비트를 추가한다
dp = [INF] * (1 << n)
dp[0] = 0
for mask in range(1 << n):
    worker = bin(mask).count("1")     # 지금 몇 번째 사람 차례냥
    for job in range(n):
        if not (mask & (1 << job)):
            nxt = mask | (1 << job)
            dp[nxt] = min(dp[nxt], dp[mask] + cost[worker][job])
print(dp[(1 << n) - 1])

왜 마스크가 상태가 되냥?

13장 DP 에서 상태는 보통 숫자 하나(dp[i])였다냥. 그런데 "어떤 것들을 골랐나" 같은 집합이 상태여야 하는 문제가 있다냥. 그때 마스크가 상태가 된다냥.

2ⁿ개의 상태라, 이것도 n 이 작을 때만 쓴다냥. 오늘 5번은 n 이 15까지라, 상태가 3만 개쯤이라 충분하다냥. 4번(집합 커버)도 같은 아이디어라냥.

오늘 배운 내용 정리

코딩냥

먼저 스스로 답해보고, "정답 보기"를 눌러서 맞는지 확인해보자냥!

Q1.비트마스크로 집합을 어떻게 표현하냥?
Q2.원소를 넣고 빼고 확인하는 비트 연산이 뭐냥?
Q3.n개 원소의 모든 부분집합을 어떻게 훑냥?
Q4.크기 1 이상인 부분집합만 세려면 어떻게 하냥?
Q5.비트 DP 에서 마스크는 무엇을 뜻하냥?

이제 직접 풀어볼 차례다냥

코딩냥

오늘 문제는 다섯 개라냥. 1번 집합 연산, 2번 XOR 부분집합, 3번 OR 부분집합 (2번과 같은 순회라냥), 4번 최소 집합 커버, 5번 일 배정이라냥.

1·2·3번으로 "비트로 집합·부분집합" 을 익히고, 4·5번으로 "마스크가 상태" 인 비트 DP 를 맛본다냥. 가보자냥~