前面的文字介绍了舞蹈链,这里就不详细描述什么是舞蹈链了,舞蹈链(Dancing links)是一种数据结构,可以用来实现X算法,以解决精确覆盖问题。本篇的内容主要把舞蹈链Dancing links应用于实际问题,通过实践才能帮助大家更好的理解,片面的了解理论知识,最终不能落到实处,也就没有意义了。

当然,把Dancing links应用于实际问题时,只有一个难点,就是如何把具体的问题转换为可以精确覆盖的01矩阵模型,一旦完成了这个步后,直接套用模板就可以解决问题了。


应用之一:伤脑筋十二块               

伤脑筋十二块是Dancing links精确覆盖的典型应用,理解起来最容易      

伤脑筋十二块

图1:12片5格骨牌的拼图

题目描述:         

给你12个如上图的5格骨牌,如何让程序拼出如上的“口”字图形。          

上图是题目的一个答案,你知道程序如何得到这个答案的吗?没错,就是用Dancing links的精确覆盖。        

我们想象一个有72列的矩阵,其中12列是12个骨牌,剩下60列是60个非中心部分的格子。         

所有可能的行来代表在一块骨牌在棋盘上得放置方案;每行有一些“1”,用来标识被覆盖的格子,5个1标识一个骨牌放置的位置(恰有1568个这样的行)             

我们将最前面的12列命名为F,I,L,P,N,T,U,V,W,X,Y,Z,并且我们可以用两个数字i,j给矩阵中对应棋盘上的第i行第j列格子的那一列命名。通过给出那些出现了“1”的列的名字,可以很方便地表示每一行。        

例如,图2就是与下面12行的对应的精确覆盖。            

精确覆盖

1568个行指的是12个骨牌可放置方案的总和,比如长条骨牌I共有64种放置方案,1568中就包含了这64种                              这1568行中,每行都有6个1,分布在72个列中                                 

这个矩阵的构造思路是:                                  

首先找约束关系,这里只有两个约束关系,                              

(1)12个骨牌,每种只有1个;                             

(2)60个空格中,一个位置只能放一种骨牌(否则就要重叠着放了);                            

因为第一个约束关系,有了12个列来区分骨牌种类,因为第二个约束关系,有了60个选5个来表示骨牌放置 。


应用之二:数独问题 sudoku           

解数独,生成数独,都可以使用精确覆盖,要把数独问题构造成01矩阵还是有一定的难度。

首先找约束关系,这里只有四个约束关系,

(1)81个格子中每个格子只能放一个数字  

(2)每一行的数字不能重复      

(3)每一列的数字不能重复      

(4)每一九宫内的数字不能重复

数独问题 sudoku

四个约束关系中,每个约束对应一个列域,对于第二个约束关系,数独中共有9行,每行可以填9个不同的数字;

因此第二个列域,共有9* 9,81个列,依此类推,数独问题共有列324个;

由于81个格子,每个格子都最多有9种选择,所以行最多有81*9=729行;

这样01矩阵的每行都有4个1,第一个1分布在1到81列,第二个1分布在82到162列,第三个1分布在163到243列;

最后一个1分布在其余列区域。 

思考:为什么不能这样构造01矩阵,用5个1,第一个1表示格子序号,有81个列,第二个1表示数字,从1到9有9个列,第三个1表示行号,有9行,第四个1表示列号也有9个,第五个1表示九宫格序号,也有9个,这样共有117列。            

为了便于理解,举个例子             

9,2,0,0,0,0,0,0,0,        

5,0,0,8,7,0,0,0,0,        

0,3,8,0,9,1,0,0,0,        

0,5,2,9,3,0,1,6,0,        

0,9,0,0,0,0,0,3,0,        

0,7,3,0,6,4,9,8,0,        

0,0,0,4,1,0,2,5,0,        

0,0,0,0,5,3,0,0,1,        

0,0,0,0,0,0,0,7,3         

如上数独有空格40个,已知格子41个,把这个数独构造成01矩阵,矩阵的行有40*9+41共401行。

对于第一个数字9,在1到81列的第一列,在82到162列的第9个,即90列,在163列到243列的第9个,在244到324列的第9个各占一个1 ;

对于第三个数字0,由于有9个选择,所以在构造01矩阵时,要向矩阵插入9个行,来表示各种可能;

对于第四个数字8,它在二行四列,把这个数字写入dancing link的网状数据结构时,需要新增四个节点,这四个节点都在同一行,它们的列序号分别为13, 81 + 9 + 8 - 1,81 + 81 + 3 * 9 + 8 - 1,81 + 81 + 81 + 9  + 8 - 1,  序号是从0开始的,所有要减去一。

现在假设,2行6列的空格是数字8,那么这个数字也会对应四个节点,列序号分别为15,81 + 9 + 8 - 1, 81 + 81 + 5 * 9 + 8 - 1, 81 + 81 + 81 + 9 + 8 - 1,

可以看到, 这两个8的行域,九宫格域都是相同的,表示这两个数字的行和九宫格都相冲了,四个列域,只要有一个相冲,两条记录就不能共存。这两个8显然不能共存。

数独还有一个变种,对角线数独,两条对角线的数字也不能重复,这时构造01矩阵模型时,就需要额外增加两个列域,左对角线域,右对角线域。增加的两个列域都只有9列,

对于1行1列的数字,会在01矩阵模型中对应5个节点,

对于2行3列的数字,由于不位于两条对角线上,会在01矩阵模型中只对应4个节点, 

对于5行5列的数字,恰好在两条对角线的交汇处,会在01矩阵模型中对应6个节点

对于数独的生成             

总体思路是一行一行的生成,第一行可以用一个随机的1到9的排列,接下来的8行,每行都要用dancinglink求解可行的排序             

(1)先对1到9这9个数进行随机排列,把这个排列作为数独终盘布局的第一行             

(2)自己写函数筛选出下一行,每个格子可以填写的数字集合,筛选时不用考虑行冲突        

比如对于排列5,9,7,4,2,6,8,3,1             

筛选结果如下:123468,123468,123468,135789,135789,135789,245679,245679,245679  

表示对于下一行的1,2,3列,可以选择的数字集合有1,2,3,4,6,8.           

下一行的4,5,6列,可以选择的数字集合有1,3,5,7,8,9       

下一行的7,8,9列,可以选择的数字集合有2,4,5,6,7,9       

这时,构造01矩阵,就只有2个约束关系           

(1)对于下一行的9个格子,每个格子只能放一个数字    

(2)对于下一行的9个格子中的数字,每个数字都不能重复 

因为第3个和4个约束,已经在筛选时考虑进去,这里不需再多此一举          

这时的01矩阵,列有9+ 9=18个,行有6* 9 = 54行(6+6+6+6+6+6+6+6+6)。


应用之三:N皇后         

N皇后问题也可以转换为Dancing links的精确覆盖问题,这里只讲如何把n皇后问题转换为01矩阵,首先有四个约束关系

(1)所有皇后不能在同一行       

(2)所有皇后不能在同一列       

(3)所有皇后不能在同一左斜线             

(4)所有皇后不能在同一右斜线             

为了便于理解,举个例子:n=8时,有8行,8列,15个左斜线,15个右斜线(2*n-1),这样构造的矩阵有46个列,8*8=64个行,矩阵的每行都有4个1,分别分布在行域,列域,左斜线域,右斜线域,在编程求解这个问题时,需要做一点变通,因为左斜线域,右斜线域的列不可能被全部覆盖,因此只需行域和列域被完全覆盖就算找到问题的一个解了。

附:

dancing LInks 求解数独的C++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#include<iostream>
#include<string.h>
  
using namespace std;
  
  
struct Node 
{
    Node *up;
    Node *down;
    Node *left;
    Node *right;
    Node *colRoot; //列首
    int row; //所在行
    int sum; //此列节点总数
};
  
#define R 729
#define C 324
  
  
class Dlx
{
public:
    Node *nodes,*row,*col,*head;//可用节点,行首,列首,总头节点
    int rowNum,colNum,nodeCount;//行数,列数,总节点数
    int *result,resultCount;//结果,结果行数
  
    Dlx()
    {
        nodes=new Node[R*C];//直接用数组竟然运行不起,栈溢出了,还得放在堆里
        row=new Node[R];
        col=new Node[C+1];
        result=new int[R];
    }
    ~Dlx()
    {
        delete []nodes;
        delete []row;
        delete []col;
        delete []result;
  
    }
    void init(int r,int c);//初始化
    void cover(Node *t);//覆盖一列
    void uncover(Node *t);//取消覆盖
    bool solove(int k=0);//搜索出结果
    void addNode(int r,int c);//添加一个节点
};
  
  
void Dlx::init(int r,int c)
{
    int i;
     
    rowNum=r;
    colNum=c;
    //将各列连起来,col[colNum]为总头节点
    for(i=0;i<=colNum;i++)
    {
        col[i].up=col[i].down=col+i;
        col[i].left=col + (i+colNum)%(1+colNum);
        col[i].right=col + (i+1)%(1+colNum);
        col[i].sum=0;
    }
    head=col+colNum;
  
    //将各行节点数清零
    for(i=0;i<rowNum;i++)
    {
        row[i].up=row[i].down=row[i].left=row[i].right=row[i].colRoot=row+i;
    }
     
    nodeCount=0;//总节点数清零
}
  
void Dlx::addNode(int r,int c)
{
    nodes[nodeCount].up=col[c].up;
    nodes[nodeCount].down=col+c;
    nodes[nodeCount].left=row[r].left;
    nodes[nodeCount].right=row+r;
    nodes[nodeCount].row=r;
    nodes[nodeCount].colRoot=col+c;
    col[c].up=col[c].up->down=row[r].left=row[r].left->right=nodes+nodeCount++;  
    col[c].sum++;
}
  
  
void Dlx::cover(Node *t)
{
    Node *p,*q;
    t->left->right=t->right;
    t->right->left=t->left;
    for(p=t->down;p!=t;p=p->down)
    {
        for(q=p->right;q!=p;q=q->right)
        {
            q->up->down=q->down;
            q->down->up=q->up;
            q->colRoot->sum--;
        }
    }  
}
  
void Dlx::uncover(Node *t)
{
    Node *p,*q;
    for(p=t->up;p!=t;p=p->up)
    {
        for(q=p->left;q!=p;q=q->left)
        {
            q->up->down=q->down->up=q;
            q->colRoot->sum++;
        }
    }
    t->left->right=t->right->left=t;
}
  
bool Dlx::solove(int k)
{
    //是否还有未覆盖的列
    if(head->right==head)
    {
        //记录完成覆盖所用行数
        resultCount=k;
        return true;
    }
     
    Node *pMin,*p,*q;
    //找到节点数最少的一列,并覆盖
    for(pMin=head->right,p=pMin->right;p!=head;p=p->right)
    {
        if(pMin->sum>p->sum)
            pMin=p;
    }
    cover(pMin);
     
  
    for(p=pMin->down;p!=pMin;p=p->down)
    {
        result[k]=p->row;
        //选定此列上的一个节点,将此节点所在行上所有节点的对应列进行覆盖
        for(q=p->right;q!=p;q=q->right)
            cover(q->colRoot);
        if(solove(k+1))
            return true;
        //如果不能成功,则取消覆盖
        for(q=p->left;q!=p;q=q->left)
            uncover(q->colRoot);
    }
  
    uncover(pMin);
    return false;
}
int getRowIndex(int rowNum)
{
    int num = rowNum%9;
    int rowIndex = rowNum / 81;
    return 81 + rowIndex*9 + num;
}
int getColIndex(int rowNum)
{
    int num = rowNum%9;
    int index = rowNum/9;    //位置
    int colIndex = index%9;
    return 162 + colIndex*9+num;
}
int getSquareIndex(int rowNum)
{
    int num = rowNum%9;
    int index = rowNum/9;    //位置
    int rowIndex = index / 9;
    int colIndex = index%9;
    int squareIndex = int(rowIndex/3)*3 + colIndex/3;
    return 243 + squareIndex*9+num;
}
int main3()
{
    int i,j;
    int node4=0;
    char str[82];
    Dlx dlx;
     
    //cin>>n;
    dlx.init(729,324);
    //for(i=0;i<9;i++)
    //{
    //  cin>> (str+i*9);
    //}
    //......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.
    const char *input = ".2738..1..1...6735.......293.5692.8...........6.1745.364.......9518...7..8..6534.";
    strcpy(str,input);
    for(i=0;i<729;i++)
    {
        //cout << "row=>" << i << "\tcol=> 位置" << i/9 <<"\t行"<<81+i/9/9*9+i%9<<"\t列"<<162+i/9%9*9+i%9<<"\t块"<< 243+(i/9/9/3*3+i/9%9/3)*9+i%9;
        //cout << "row=>" << i << "\tcol=> 位置" << i/9 <<"\t行"<<getRowIndex(i)<<"\t列"<<getColIndex(i)<<"\t块"<<getSquareIndex(i);
        if(str[i/9]=='.' || str[i/9]-'1'==i%9)
        {
            node4++;
            int rowIndex = i;
            int colIndex = i/9;
            dlx.addNode(rowIndex,colIndex);//位置冲突
  
            dlx.addNode(rowIndex,getRowIndex(i));//行冲突
  
            dlx.addNode(rowIndex,getColIndex(i));//列冲突
  
            dlx.addNode(rowIndex,getSquareIndex(i));//块冲突
        //  cout << "\t<=";
        }
        //cout << endl;
    }
    if(dlx.solove())
    {
        //结果存到字符串中
        for(i=0;i<81;i++)
        {
            j=dlx.result[i];
            str[j/9]='1'+j%9;
        }
        //输出字符串
        for(i=0;i<9;i++)
        {
            for(j=0;j<9;j++)
                cout<<str[i*9+j];
            cout<<endl;
        }
    }
     
    return 0;
}
点赞(228)

C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:

一点编程也不会写的:零基础C语言学练课程

解决困扰你多年的C语言疑难杂症特性的C语言进阶课程

从零到写出一个爬虫的Python编程课程

只会语法写不出代码?手把手带你写100个编程真题的编程百练课程

信息学奥赛或C++选手的 必学C++课程

蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程

手把手讲解近五年真题的蓝桥杯辅导课程

Dotcpp在线编译      (登录可减少运行等待时间)