비트 조작 (Bit Manipulation): 비트 연산과 마스킹 완전 정복

AND·OR·XOR·NOT·SHIFT 기본 연산부터 최하위 비트 추출·2의 거듭제곱 검사·XOR 스왑 트릭, 비트마스크 DP(TSP·집합 커버링), popcount 최적화까지 비트 조작의 모든 것을 정리합니다.

· 6 min read · PALDYN Team

지난 글에서 상태 공간 탐색을 다뤘다면, 비트 조작은 상태를 비트로 압축해 O(1) 또는 O(2^n)으로 처리하는 강력한 도구입니다. 면접에서도 자주 등장하고, 경쟁 프로그래밍에서는 비트마스크 DP가 없으면 풀 수 없는 문제가 있을 만큼 중요합니다.

기본 비트 연산

정수를 이진수로 다루는 6가지 연산:

a = 0b1100  # 12
b = 0b1010  # 10

print(a & b)   # AND:  0b1000 = 8
print(a | b)   # OR:   0b1110 = 14
print(a ^ b)   # XOR:  0b0110 = 6
print(~a)      # NOT:  -13 (2의 보수 표현)
print(a << 2)  # LEFT SHIFT:  48 (×4)
print(a >> 1)  # RIGHT SHIFT: 6  (÷2)

비트 연산 기본

필수 비트 트릭 모음

특정 비트 조작

n = 0b1010  # 10

# i번째 비트 확인
def check_bit(n, i):
    return (n >> i) & 1

# i번째 비트 세트 (1로 만들기)
def set_bit(n, i):
    return n | (1 << i)

# i번째 비트 클리어 (0으로 만들기)
def clear_bit(n, i):
    return n & ~(1 << i)

# i번째 비트 토글 (0↔1)
def toggle_bit(n, i):
    return n ^ (1 << i)

print(check_bit(10, 1))   # 1 (비트1 = 1)
print(set_bit(10, 2))     # 14 (0b1110)
print(clear_bit(10, 1))   # 8  (0b1000)
print(toggle_bit(10, 0))  # 11 (0b1011)

자주 쓰는 패턴

# 2의 거듭제곱 여부 확인
def is_power_of_two(n):
    return n > 0 and (n & (n - 1)) == 0

# 최하위 비트(LSB) 추출
def lowest_set_bit(n):
    return n & (-n)

# 최하위 세트 비트 제거
def remove_lowest_set_bit(n):
    return n & (n - 1)

# 부호 확인 (MSB)
def is_negative(n):
    return (n >> 31) & 1  # 32비트 기준

print(is_power_of_two(16))       # True
print(lowest_set_bit(0b1100))    # 4 (0b0100)
print(remove_lowest_set_bit(12)) # 8 (0b1000)

XOR 마법

XOR의 특성 a ^ a = 0, a ^ 0 = a를 이용한 트릭들:

# 변수 교환 (임시 변수 없이)
def swap(a, b):
    a ^= b
    b ^= a
    a ^= b
    return a, b

# 배열에서 유일한 원소 찾기 (나머지는 쌍)
def find_single(nums):
    result = 0
    for num in nums:
        result ^= num
    return result

print(find_single([2, 3, 2, 4, 3]))  # 4
# [2^2=0, 3^3=0, 4 남음]

Popcount — 세트 비트 개수

# 방법 1: bin 함수 (Python)
def popcount_py(n):
    return bin(n).count('1')

# 방법 2: Kernighan 알고리즘 O(popcount)
def popcount_kernighan(n):
    count = 0
    while n:
        n &= n - 1  # 최하위 1 비트 제거
        count += 1
    return count

# 방법 3: 하드웨어 지원 (C에서 __builtin_popcount)
# Python에서는 int.bit_count() (3.10+)
print((29).bit_count())  # 4 (0b11101)

비트마스크 DP

n개 원소의 부분 집합 상태를 정수 하나로 표현하면 DP 상태가 O(2^n)이 됩니다. n이 작을 때(보통 ≤20) 극적인 효율을 냅니다.

비트마스크 DP

부분 집합 열거

n = 4

# 모든 부분 집합 순회
for mask in range(1 << n):
    subset = []
    for i in range(n):
        if mask & (1 << i):
            subset.append(i)
    # print(mask, subset)

# 특정 마스크의 부분 집합만 열거 (빠른 방법)
def enumerate_subsets(mask):
    sub = mask
    while sub > 0:
        yield sub
        sub = (sub - 1) & mask

TSP (외판원 문제) — 비트마스크 DP의 대표 문제

n개 도시를 모두 방문하고 출발지로 돌아오는 최단 경로:

from math import inf

def tsp(dist):
    n = len(dist)
    INF = inf
    # dp[mask][i] = mask 집합을 방문하고 i에 있을 때 최소 비용
    dp = [[INF] * n for _ in range(1 << n)]
    dp[1][0] = 0  # 도시 0 출발 (mask=0b0001)

    for mask in range(1 << n):
        for u in range(n):
            if dp[mask][u] == INF:
                continue
            if not (mask >> u & 1):
                continue
            for v in range(n):
                if mask >> v & 1:
                    continue  # 이미 방문
                new_mask = mask | (1 << v)
                cost = dp[mask][u] + dist[u][v]
                if cost < dp[new_mask][v]:
                    dp[new_mask][v] = cost

    full = (1 << n) - 1
    return min(dp[full][i] + dist[i][0] for i in range(1, n))
  • 시간: O(2^n · n²), 공간: O(2^n · n)
  • 브루트 포스 O(n!)에 비해 n=20일 때 ~10^6배 빠름

집합 커버링 최소화

def minimum_set_cover(universe, sets):
    n = len(universe)
    m = len(sets)
    # 각 집합을 비트마스크로 표현
    masks = []
    for s in sets:
        mask = 0
        for e in s:
            mask |= (1 << e)
        masks.append(mask)

    full = (1 << n) - 1
    dp = [float('inf')] * (full + 1)
    dp[0] = 0

    for covered in range(full + 1):
        if dp[covered] == float('inf'):
            continue
        for mask in masks:
            new_covered = covered | mask
            dp[new_covered] = min(dp[new_covered], dp[covered] + 1)

    return dp[full]

실전 응용 문제

문제 유형핵심 트릭
배열에서 홀수 번 나타나는 수전체 XOR
해밍 거리bin(a^b).count('1')
비트 역전비트 분할 정복
2의 거듭제곱 여부n & (n-1) == 0
최대 AND 쌍그리디 + 비트 위에서부터
부분 집합 합 (small n)비트마스크 DP

지난 글: 백트래킹: 상태 공간 탐색과 가지치기

다음 글: KMP 알고리즘: 문자열 검색의 표준


읽어주셔서 감사합니다. 😊