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

Knuth X 알고리즘 (Exact Cover)

· 수정 · 📖 약 4분 · 1,114자/단어 #algorithm #search #knuth-x #backtracking #exact-cover #dancing-links
Knuth X, exact cover, dancing links, DLX, 정확한 커버, knuth-x

정의

Knuth XExact Cover 문제를 푸는 backtracking 알고리즘. 0-1 행렬이 주어졌을 때 모든 열이 정확히 한 번씩 커버되도록 행의 부분 집합을 선택. Dancing Links (DLX) 라는 자료구조로 행렬의 원소를 효율적으로 삽입/삭제하며 탐색. Donald Knuth 가 2000 년 The Art of Computer Programming 에서 제안.

문제 상황과 동기

Exact Cover: 0-1 행렬 A 에서 각 열을 정확히 한 번 포함하는 행의 부분 집합 S 를 찾아라.

  • naive backtracking: 각 열을 아직 커버 안 한 행들을 순회. 매 단계마다 행렬을 복사하면 O(2^(행 개수)).
  • Dancing Links (DLX): doubly linked list 로 행렬을 표현. 행/열 삭제가 O(1) (포인터 재연결만). Backtrack 이 undo 도 O(1). 탐색 속도 극적 향상.

핵심 통찰: Doubly linked list 에서 노드를 “잠시 떼는” 것과 “다시 붙이는” 것이 정확히 역연산. 행렬을 복사하지 않고도 상태 복원 가능.

시각화

핵심 아이디어

Exact Cover 문제 형식: 행 = 후보 선택 (예: 스도쿠 칸에 숫자), 열 = 제약 조건 (“각 칸에 숫자 하나”, “각 행에 1-9”). 목표: 모든 열을 정확히 한 번씩 만족하는 행 집합.

Dancing Links (DLX): 0-1 행렬의 각 1 을 하나의 노드로 표현. 좌/우/상/하 doubly linked list.

search():
    if head.right == head: return SUCCESS
    c = select_column()         # MRV heuristic (최소 1 의 열)
    cover(c)
    for each row r in column c:
        O ← O ∪ {r}
        for each column j in row r: cover(j)
        search()
        for each column j in row r (reversed): uncover(j)
        O ← O \ {r}
    uncover(c)

cover(c): c 열 제거 + 해당 열이 포함된 모든 행 제거 (링크 재연결). uncover(c): 제거된 상태에서 원복. cover/uncover 순서가 정확히 역순이어야 스택 없이 원복 가능.

구현

// DLX (Dancing Links) for exact cover
#include <bits/stdc++.h>
using namespace std;

struct Node {
  Node *L, *R, *U, *D, *col;
  int row, cnt;
};

struct DLX {
  vector<Node> nodes;
  Node *head;
  vector<int> ans;

  DLX(int cols) {
      nodes.resize(cols+1); head=&nodes[0];
      head->L=head->R=head->U=head->D=head; head->col=head;
      for(int i=1;i<=cols;i++){
          auto* c=&nodes[i];
          c->L=head; c->R=head->R; head->R->L=c; head->R=c;
          c->U=c->D=c; c->col=c; c->cnt=0;
      }
  }
  void add_row(int r, const vector<int>& cols){
      Node* f=nullptr;
      for(int c:cols){
          nodes.emplace_back(); auto* n=&nodes.back();
          n->row=r; n->col=&nodes[c]; n->col->cnt++;
          if(f){ n->L=f->L; n->R=f; f->L->R=n; f->L=n; }
          else{ f=n; n->L=n->R=n; }
          n->U=n->col->U; n->D=n->col; n->col->U->D=n; n->col->U=n;
      }
  }
  void cover(Node* c){
      c->R->L=c->L; c->L->R=c->R;
      for(auto* i=c->D;i!=c;i=i->D)
          for(auto* j=i->R;j!=i;j=j->R){ j->D->U=j->U; j->U->D=j->D; j->col->cnt--; }
  }
  void uncover(Node* c){
      for(auto* i=c->U;i!=c;i=i->U)
          for(auto* j=i->L;j!=i;j=j->L){ j->col->cnt++; j->D->U=j; j->U->D=j; }
      c->R->L=c; c->L->R=c;
  }
  bool search(){
      if(head->R==head) return true;
      Node* c=head->R;
      for(auto* p=head->R;p!=head;p=p->R) if(p->cnt<c->cnt) c=p;
      cover(c);
      for(auto* r=c->D;r!=c;r=r->D){
          ans.push_back(r->row);
          for(auto* j=r->R;j!=r;j=j->R) cover(j->col);
          if(search()) return true;
          for(auto* j=r->L;j!=r;j=j->L) uncover(j->col);
          ans.pop_back();
      }
      uncover(c); return false;
  }
};

int main(){
  int N,M; cin>>N>>M; DLX dlx(M);
  for(int i=1;i<=N;i++){
      vector<int> cols;
      for(int j=1;j<=M;j++){ int x; cin>>x; if(x) cols.push_back(j); }
      if(!cols.empty()) dlx.add_row(i,cols);
  }
  if(dlx.search()){ for(int r:dlx.ans) cout<<r<<" "; cout<<"\n"; }
  else cout<<"No solution\n";
}
stdin
4 4
1 1 0 1
0 1 1 0
0 0 1 1
1 0 0 0
결과
2 3

복잡도

항목
시간 (최악)O(2^N) (backtracking, 일반적)
시간 (평균, MRV)훨씬 빠름, 대부분 다항식 시간
공간O(M) (M = 1의 개수)
cover / uncoverO(1) per node (링크 재연결만)

변형 / 활용

스도쿠 (Sudoku)

9x9 스도쿠를 729 행 (각 칸에 1-9) x 324 열 (4 가지 제약) 의 exact cover 로 변환. DLX 로 가장 빠르게 풀림.

N-Queens, Pentomino

모든 타일링/퍼즐 문제를 exact cover 로 모델링 가능.

Knuth X (=Algorithm X) 는 Dancing Links 가 아닌 일반 backtracking 버전. DLX 는 그 구현체.

함정

1. cover/uncover 순서

cover 하면서 제거된 열들은 정확히 역순으로 uncover 해야 함. 순서가 뒤바뀌면 포인터가 꼬여서 복원 실패.

2. MRV (Minimum Remaining Values)

가장 적은 1을 가진 열을 먼저 선택하는 heuristic. 이거 없으면 스도쿠 같은 문제에서 일반 backtracking 과 동일한 속도.

3. 행이 없는 열

처음부터 어떤 열에도 1이 없는 행이 없어야 함. 있으면 미리 제거.

BOJ 연습 문제

번호제목정답률링크
BOJ 2580스도쿠-kokoa-lab
BOJ 22965k-Dilation-kokoa-lab
BOJ 15312Exact Cover-kokoa-lab

참고

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

💬 댓글

사이트 검색 / 명령어

검색

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