【笔记】DLX算法及常见应用

参考资料

精确覆盖问题讲解——grenet

数独模型转换——bl0ss0m

DLX算法求解数独——grenet

问题引入

精确覆盖问题:

有r个由1~n组成的集合S1,S2,S3....Sr,要求选择若干集合,使得1~n恰好只在一个集合里出现。

数独问题:

在9×9的矩阵里填数,使得每一行每一列每一个九宫格里1~9都恰好出现一次

解法分析

先考虑精确覆盖问题,我们将其建成一个r×n的01矩阵,第i行第j列为1表示第i个集合里有元素j,那选择若干集合就转化为选择若干行

一种很直观的想法是进行回溯深搜——每当选中一行,则将该行以及该行为1的列都删掉,这样就得到了一个更小的矩阵,回溯的时候将删掉的行加回来

这个算法我们称之为X算法,在求解的过程中有大量的缓存矩阵和回溯矩阵的过程,如何缓存矩阵以及相关的数据(保证后面的回溯能正确恢复数据)比较复杂低效。

于是有神犇想了一种数据结构来维护这种删除和插入的操作——提到高效插入和删除,可以想到链表吧。

DLX就是一种用了双向链表思想来维护矩阵的数据结构

模型

DLX用的数据结构是交叉十字循环双向链,每个元素不仅是横向循环双向链中的一份子,又是纵向循环双向链的一份子因为精确覆盖问题的矩阵往往是稀疏矩阵(矩阵中,0的个数多于1),DLX仅仅记录矩阵中值是1的元素。

每个元素有6个分量

  Left指向左边的元素、Right指向右边的元素、Up指向上边的元素、Down指向下边的元素、Col指向列标元素、Row指示当前元素所在的行

一些辅助元素

Ans():Ans数组,在求解的过程中保留当前的答案,以供最后输出答案用。

Head元素:求解的辅助元素,在求解的过程中,当判断出Head.Right=Head(也可以是Head.Left=Head)时,求解结束,输出答案。Head元素只有两个分量有用。其余的分量对求解没啥用

C元素:列标元素,每列有一个列标元素。本文开始的题目的列标元素分别是C1、C2、C3、C4、C5、C6、C7。每一列的元素的Col分量都指向所在列的列标元素。列标元素的Col分量指向自己(也可以是没有)。在初始化的状态下,Head.Right=C1、C1.Right=C2、……、C7.Right=Head、Head.Left=C7等等。列标元素的分量Row=0,表示是处在第0行。

结构体框架

#define FOR(i,A,s) for(int i = A[s]; i != s; i = A[i])

 

struct DLX {
  //成员变量
  int n, sz; // 列数,结点总数
  int S[MAXC]; // 各列结点数
  int row[MAXN], col[MAXN]; // 各结点行列编号
  int L[MAXN], R[MAXN], U[MAXN], D[MAXN]; // 十字链表
  vector<int>vec;
  int ansd, ans[MAXR]; ////成员函数
  void init(int n);//n为列数
  void remove(int c); 
  void restore(int c);
  bool dfs(int d);//d为递归深度
  bool solve();

  //数独所需函数
  void build();
  void decode(int code,int &a,int &b,int &c);
  inline int encode(int a,int b,int c);
  void output();
  void addRow(int r);
  inline int trans(int x,int y);

};

各部分实现

void init

建立头结点,列标元素,横向成环,纵向成环(指向自己),初始化各变量

void DLX::init(int n) { // n是列数
    this->n = n;
    for(int i = 0 ; i <= n; i++) {
      U[i] = i; D[i] = i; L[i] = i-1, R[i] = i+1;
    }
    R[n] = 0; L[0] = n;
    sz = n + 1;
    memset(S, 0, sizeof(S));
}

bool solve

解决精确覆盖问题的接口,如果有解返回true,无解返回false

bool DLX::solve() {
    f(!dfs(0)) return false;
    return true;
}

 

bool dfs

如果找到解(R[0] == 0 ) 则记录解的长度并返回

否则继续深搜下去,搜不到答案则返回false

bool DLX::dfs(int d) {
    if (R[0] == 0) { // 找到解
      ansd = d; // 记录解的长度
      return true;
    }
    // 找S最小的列c
    int c = R[0]; // 第一个未删除的列
    FOR(i,R,0) if(S[i] < S[c]) c = i;
    remove(c); // 删除第c列
    FOR(i,D,c) { // 用结点i所在行覆盖第c列
      ans[d] = row[i];
      FOR(j,R,i) remove(col[j]); // 删除结点i所在行能覆盖的所有其他列
      if(dfs(d+1)) return true;
      FOR(j,L,i) restore(col[j]); // 恢复结点i所在行能覆盖的所有其他列
    }
    restore(c); // 恢复第c列
    return false;
}

void remove

删除第c列

void DLX::remove(int c) {
    L[R[c]] = L[c];
    R[L[c]] = R[c];
    FOR(i,D,c)
      FOR(j,R,i) { U[D[j]] = U[j]; D[U[j]] = D[j]; --S[col[j]]; }
}

 

void restore

恢复第c列

void DLX::restore(int c) {
    FOR(i,U,c) FOR(j,L,i) { ++S[col[j]]; U[D[j]] = j; D[U[j]] = j; }
    L[R[c]] = c;R[L[c]] = c;
}

 void addRow

增加一行

void DLX::addRow(int r) {
    int first = sz;
    for(int i = 0; i < vec.size(); i++) {
      int c = vec[i];
    //    cout<<c<<" ";
      L[sz] = sz - 1; R[sz] = sz + 1; D[sz] = c; U[sz] = U[c];
      D[U[c]] = sz; U[c] = sz;
      row[sz] = r; col[sz] = c;
      S[c]++; sz++;
    }
    //    cout<<endl;
    R[sz - 1] = first; L[first] = sz - 1;
}

 

数独问题

1、把数独问题转换为精确覆盖问题

2、设计出数据矩阵

3、用舞蹈链(Dancing Links)算法求解该精确覆盖问题

4、把该精确覆盖问题的解转换为数独的解

首先看看数独问题(9*9的方格)的规则

1、每个格子只能填一个数字

2、每行每个数字只能填一遍

3、每列每个数字只能填一遍

4、每宫每个数字只能填一遍

把上面的表述换个说法

1、每个格子只能填一个数字

2、每行1-9的这9个数字都得填一遍(也就意味着每个数字只能填一遍)

3、每列1-9的这9个数字都得填一遍

4、每宫1-9的这9个数字都得填一遍

 那么我们现在将9×9数独问题转换成一个324列的精确覆盖问题:

4个限制条件,将其分别转换为81列

每个格子都要填入数字
  1到81列,表示数独中9*9=81个格子是否填入了数字。如果是,则选取的01行在该01列上为1

每一行都要有1~9填入
  81+1到81*2列,每9列就代表数独中的一行,如果该行有某个数字,则其对应的列上为1

每一列都要有1~9填入
  81*2+1到81*3列,每9列就代表数独中的一列

每一宫都要有1~9填入
  81*3+1到81*4列,每9列就代表数独中的一宫!

建好模型后跑DLX算法即可求解

void DLX::build() {
  for(int i=0;i<9;i++) {
      for(int j=0;j<9;j++) {
    for(int k=0;k<9;k++){
        if(sudoku[i][j]==-1||sudoku[i][j]==k){
        //cout<<i<<" "<<j<<" "<<k<<endl;
        vec.clear();
        vec.push_back(encode(0,i,j));
        vec.push_back(encode(1,i,k));
        vec.push_back(encode(2,j,k));
        vec.push_back(encode(3,trans(i,j),k));            
        addRow(encode(i,j,k));
        }
    }
      }
  }
  return;
}


inline int DLX::encode(int a,int b,int c){
  return 81*a + b*9+c+1;
}


void DLX::decode(int code,int &a,int &b,int &c){
  code--;
  c = code%9;code/=9;
  b = code%9;code/=9;
  a = code;
}

inline int DLX::trans(int x,int y){
  x = x/3;
  y = y/3;
  return x*3+y;
}

void DLX::output() {
  for(int i=0;i<ansd;i++){
      int r,c,v;
      decode(ans[i],r,c,v);
      sudoku[r][c]=v;
  }
  for(int i=0;i<9;i++){
      for(int j=0;j<9;j++){
      printf("%d",sudoku[i][j]+1);
      }
      putchar('\n');
  }
}

 

贴一下模板题poj2676的代码

#include<cstdio>
#include<vector>
#include<cstring>
#include<iostream>
using namespace std;

int _;
int sudoku[10][10];
const int MAXN = 81*4*81+10,MAXR = 9*9*9+10,MAXC = 81*4+10;
// 行编号从1开始,列编号为1~n,结点0是表头结点; 结点1~n是各列顶部的虚拟结点
struct DLX {
  //成员变量
  int n, sz; // 列数,结点总数
  int S[MAXC]; // 各列结点数
  int row[MAXN], col[MAXN]; // 各结点行列编号
  int L[MAXN], R[MAXN], U[MAXN], D[MAXN]; // 十字链表
  vector<int>vec;
  int ansd, ans[MAXR]; ////成员函数
  void init(int n);//n为列数
  void remove(int c); 
  void restore(int c);
  bool dfs(int d);//d为递归深度
  bool solve();
  void addRow(int r);

  //数独所需函数
  void build();
  void decode(int code,int &a,int &b,int &c);
  inline int encode(int a,int b,int c);
  void output();
  inline int trans(int x,int y);

};
#define FOR(i,A,s) for(int i = A[s]; i != s; i = A[i]) 
void DLX::output() {
  for(int i=0;i<ansd;i++){
      int r,c,v;
      decode(ans[i],r,c,v);
      sudoku[r][c]=v;
  }
  for(int i=0;i<9;i++){
      for(int j=0;j<9;j++){
      printf("%d",sudoku[i][j]+1);
      }
      putchar('\n');
  }
}
void DLX::addRow(int r) {
    int first = sz;
    for(int i = 0; i < vec.size(); i++) {
      int c = vec[i];
    //    cout<<c<<" ";
      L[sz] = sz - 1; R[sz] = sz + 1; D[sz] = c; U[sz] = U[c];
      D[U[c]] = sz; U[c] = sz;
      row[sz] = r; col[sz] = c;
      S[c]++; sz++;
    }
    //    cout<<endl;
    R[sz - 1] = first; L[first] = sz - 1;
}
inline int DLX::encode(int a,int b,int c){
  return 81*a + b*9+c+1;
}
inline int DLX::trans(int x,int y){
  x = x/3;
  y = y/3;
  return x*3+y;
}
void DLX::build() {
  for(int i=0;i<9;i++) {
      for(int j=0;j<9;j++) {
    for(int k=0;k<9;k++){
        if(sudoku[i][j]==-1||sudoku[i][j]==k){
        //cout<<i<<" "<<j<<" "<<k<<endl;
        vec.clear();
        vec.push_back(encode(0,i,j));
        vec.push_back(encode(1,i,k));
        vec.push_back(encode(2,j,k));
        vec.push_back(encode(3,trans(i,j),k));            
        addRow(encode(i,j,k));
        }
    }
      }
  }
  return;
}

void DLX::init(int n) { // n是列数
    this->n = n;

    // 虚拟结点
    for(int i = 0 ; i <= n; i++) {
      U[i] = i; D[i] = i; L[i] = i-1, R[i] = i+1;
    }
    R[n] = 0; L[0] = n;

    sz = n + 1;
    memset(S, 0, sizeof(S));
}
void DLX::remove(int c) {
    L[R[c]] = L[c];
    R[L[c]] = R[c];
    FOR(i,D,c)
      FOR(j,R,i) { U[D[j]] = U[j]; D[U[j]] = D[j]; --S[col[j]]; }
}

void DLX::restore(int c) {
    FOR(i,U,c)
    FOR(j,L,i) { ++S[col[j]]; U[D[j]] = j; D[U[j]] = j; }
    L[R[c]] = c;
    R[L[c]] = c;
}
bool DLX::dfs(int d) {
    if (R[0] == 0) { // 找到解
      ansd = d; // 记录解的长度
      return true;
    }

    // 找S最小的列c
    int c = R[0]; // 第一个未删除的列
    FOR(i,R,0) if(S[i] < S[c]) c = i;

    remove(c); // 删除第c列
    FOR(i,D,c) { // 用结点i所在行覆盖第c列
      ans[d] = row[i];
      FOR(j,R,i) remove(col[j]); // 删除结点i所在行能覆盖的所有其他列
      if(dfs(d+1)) return true;
      FOR(j,L,i) restore(col[j]); // 恢复结点i所在行能覆盖的所有其他列
    }
    restore(c); // 恢复第c列

    return false;
}

void DLX::decode(int code,int &a,int &b,int &c){
  code--;
  c = code%9;code/=9;
  b = code%9;code/=9;
  a = code;
}

bool DLX::solve() {
    if(!dfs(0)) return false;
    return true;
}


void input(){

    for(int i=0;i<9;i++) 
    for(int j=0;j<9;j++)
        scanf("%1d",&sudoku[i][j]),sudoku[i][j]--;
}
DLX dlx;
int main(){
    scanf("%d",&_);
    while(_--) {
    input();
    dlx.init(81*4);
    dlx.build();
    bool ok = dlx.solve();
    if(ok) {
        dlx.output();
    }

    }
}
poj2676

 

转载于:https://www.cnblogs.com/greenty1208/p/9898516.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值