본문으로 건너뛰기
김신건의 로그

비트셋 LCS (Bitset LCS)

· 수정 · 📖 약 3분 · 816자/단어 #algorithm #dp #lcs #string #bitset-optimization
bitset LCS, 비트셋 LCS, bitset-lcs, Hyyro LCS, Allison-Dix LCS

정의

Bitset LCS 는 두 문자열 A, B 의 최장 공통 부분 수열 (LCS) 길이를 O(NM/w) 시간에 구하는 기법. 표준 LCS DP 의 O(NM) 를 비트 병렬 (bit-parallel) 로 w=64 배 가속.

w = CPU word size (64). std::bitset 또는 64-bit 정수로 DP 행을 표현해, 한 번에 64 개 DP 전이를 처리.

문제 상황과 동기

표준 LCS DP 는 O(NM) 시간. N = M = 10^5 이면 10^10 연산으로 불가.

  • naive DP: 2차원 테이블 채우기. O(NM), N=M=10^4 에서 10^8 로 간당간당.
  • bitset LCS: PyPy 에서 bitset 을 정수로 구현, O(NM/64). N=M=10^5 도 1.5 초 내.

핵심 통찰: LCS 점화식의 if A[i-1] == B[j-1] 부분은 비트 AND + shift 로 한 번에 처리. max(prev, curr-1)비트 OR + mask 로 표현.

Allison-Dix 공식

S[i] = A[0..i] 와 B 의 전체 LCS 길이가 증가하는 위치 집합. 이를 비트셋 M[i] 로 관리.

// 각 문자 c 에 대한 비트마스크 prec[c] = B 에서 c 의 위치들
// DP 는 bitset 연산으로:
for i in 1..N:
    x = prec[A[i-1]] | M[i-1]
    M[i] = M[i-1] | (x - ((x - M[i-1]) & ~x)의 MSB 조작)

실제 구현은 더 간단:

u = M[i-1] | prec[A[i-1]]
v = M[i-1] << 1 | 1
M[i] = M[i-1] | ((u - v) & ~u)

결과 LCS 길이 = popcount(M[N]).

알고리즘

bitset_lcs(A, B):
    N = len(A), M = len(B)
    prec[0..MAX_CHAR] = 0  // 각 문자의 B 내 위치 비트셋
    for j in 0..M-1:
        prec[B[j]] |= (1 << j)

    bitset cur = 0
    for i in 0..N-1:
        u = cur | prec[A[i]]
        v = (cur << 1) | 1
        cur |= ((u - v) & ~u)

    return popcount(cur)  // LCS 길이

핵심 트릭: (u - v) & ~uuv 의 LCS 전이를 한 연산에 계산. 자세한 증명은 Allison-Dix (1986) 참고.

구현

// Bitset LCS O(NM/64), Hyyro/Allison-Dix
#include <bits/stdc++.h>
using namespace std;
using ull = unsigned long long;

int lcs_bitset(const string& a, const string& b) {
  int n = a.size(), m = b.size();
  vector<ull> prec(26, 0);
  for (int j = 0; j < m; j++)
      prec[b[j]-'A'] |= (1ULL << j);

  ull cur = 0;
  for (int i = 0; i < n; i++) {
      ull u = cur | prec[a[i]-'A'];
      ull v = (cur << 1) | 1;
      cur |= ( (u - v) & ~u );
  }
  return __builtin_popcountll(cur);
}

int main() {
  string a, b;
  cin >> a >> b;
  cout << lcs_bitset(a, b) << "\n";
}
stdin
ABCBDAB
BDCAB
결과
4

복잡도

항목
시간 (최선)O(NM/w)
시간 (평균)O(NM/w)
시간 (최악)O(NM/w)
공간O(Σ + M/w) (알파벳 개수 + 결과 bitset)
wCPU word size (64)

표준 DP O(NM) 대비 약 64 배 빠름. N=M=10^5 에서 10^10 -> 1.5x10^8 연산으로 실용적.

변형

w-bit 한계 돌파

M > 64 이면 unsigned long long[] 배열로 bitset 확장. (256-bit, 512-bit) 벡터 명령어 (SIMD) 로 더 가속 가능.

Longest Common Substring

bitset 기법은 substring (연속) 에도 적용 가능. shift + AND 로 공통 부분 문자열 탐색.

Edit Distance

비슷한 bit-parallel 기법 (myers_diff) 으로 edit distance O(NM/w) 계산.

함정

1. 알파벳 크기

prec 배열은 알파벳 (또는 문자 집합) 만큼. 대문자 26 개면 충분하지만, 아스키 전체면 더 큰 배열 필요.

2. shift overflow

cur << 1 이 64 비트 넘으면 상위 비트 손실. M=64 이면 (cur << 1) 에서 MSB 가 carry out 되지 않도록 M < 64 로 제한하거나 bitset 배열 사용.

3. traceback 불가

bitset LCS 는 길이만 O(1) 에 구함. 수열 복원 필요시 Hirschberg 또는 표준 DP traceback.

4. (u - v) & ~u 의미

이 수식이 한 번에 LCS 전이를 표현함. 직관적이지 않으므로 필요시 논문 참고. 틀리면 계산이 훨씬 더 빨라지거나 (의미 없는 값) 느려짐.

BOJ 연습 문제

번호제목정답률링크
BOJ 9251LCS-kokoa-lab
BOJ 9252LCS 2 (traceback)-kokoa-lab
BOJ 1958LCS 3-kokoa-lab

참고

이 글의 용어 (3개)
최장 공통 부분 수열 (Longest Common Subsequence)algorithm
정의 최장 공통 부분 수열 (Longest Common Subsequence, LCS) 은 두 수열 A, B 에서 순서를 유지하며 공통으로 등장하는 부분 수열 중 가장 긴 것을 …
Bitset Optimizationalgorithm
정의 Bitset Optimization 은 불리언 / 비트 단위 정보 를 또는 배열에 패킹해, 한 명령어로 64 비트씩 병렬 연산 함으로써 시간 복잡도를 /64 (또는 , w …
Hirschberg (히르쉬버그) LCSalgorithm
정의 Hirschberg 알고리즘 은 두 문자열 A, B 의 LCS 를 O(N) 공간 (메모리) 만 사용해 구하는 분할 정복 DP. 표준 LCS 의 O(NM) 공간을 O(N) 으…

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기