Sudoku

链接🔗:

QOJ
洛谷1
洛谷2
洛谷的两道题只是输入格式不一样

前言:

自从暑假大礼包开始,题目越来越难,昨天的就是道紫题,我就有预感今天的题不简单,果不其然今天就是黑题了(Σ(っ °Д °;)っ

题目大意📕:

题目说的简单明了,就是给个数独16宫格来让你填。

思路🖊:

一 . 搜索

这个搜索量肯定很大,但不是不可能实现的,要加亿点剪枝优化,但我们今天的主题不是这个,所以大家自行上网搜dalao们的题解或者自行尝试吧(逃ε=ε=ε=┏(゜ロ゜;)┛


二 . DLX

没错!这是个没学过的知识,今天我将给大家讲解一下这个奇怪的玩意儿。

DLX,全名:Dancing Links X ,是算法X加上著名程序设计大师Knuth所设计的————双向循环交叉链表,又称舞蹈链(请大家记住它)

这个东东是解决一类叫做精准覆盖的问题。

什么是“精准覆盖”问题呢??请看下面:

已知目前有r个集合,
请选出其中的一些集合使它们的并集为1,2,3,...,n
并且你要让这些选出的集合的交集为空集。

我们可以怎样解决呢?接下来就要请到X算法了:

我们可以用一个 $ r $ 行 $ n $ 列的矩阵表示精准覆盖问题

第 $ i $ 行第 $ j $ 个元素代表第 $ i $ 个集合是否存在元素 $ j $

举个栗子

对于数据 $ n = $ 7, $ r = $ 6:

6 7
1 0 0 1 0 0 1
1 0 0 1 0 0 0
0 0 0 1 1 0 1
0 0 1 0 1 1 0
0 1 1 0 0 1 1
0 1 0 0 0 0 1

接下来我们考虑采用回溯法解决这个问题。

每一次选择一个没有被覆盖的元素,也就是一列。然后选择包含这个元素的集合进行覆盖,也就是一行。那么在矩阵中,我们每选择一个没有被删除的,接着看一下这一列中还有哪些行是1,尝试删除该行,回溯时恢复改行。当然,如果我们选了一行,那么就把这一行其它的1所在的列也应当删除。恢复同理。

这种思路就是我们的X算法了,但可惜的是,恢复与删除这个操作做起来不太容易,那怎么办呢?现在就是用到舞蹈链(双向循环交叉链表)的时候了

我将分为“双向”、“循环”、“交叉”三个部分讲解。(链表都知道撒)

1 . 双向

双向,顾名思义,就是左右两边,我们可以矩阵中每一个点用指针与其左右两边的点相连,左边的点我们用 $ l [ i ] $ 表示,右边的点用 $ r [ i ] $ 表示。

2 . 交叉

什么叫交叉,横纵即为交叉!所以我们再给每一个点用指针与其上下两个点都连起来。上: $ u [ i ] $ , 下: $ d [ i ] $

3 . 循环

循环,就是让一行或者一列能够进行循环往复,那么我们可以让最右边的所有节点的R指向最左边的节点,最左边节点的L指向最右边的节点,上下也是如此。


舞蹈链讲完了,那该如何运用呢???如何进行删除,恢复两个操作呢???

我们先来看第一个问题,如何删除一列?(也就是找到一个元素后选中它)

我们可以在每一列的顶端添加一个节点——————虚拟节点

该节点的 $ u $ 数组指向其所在列的最后一个节点, $ d $ 数组指向其所在列的第一个节点。 $ l $ 与 $ r $ 分别指向旁边两列的虚拟节点。

那么删除一列只需要将该列虚拟节点左边的节点的 $ r $ 指向该列虚拟节点右边的虚拟节点,将该列虚拟节点的右边虚拟节点的 $ l $ 指向该列虚拟节点的左边的虚拟节点。(就是正常链表删除一个点的操作)

问题是删完了吗?没有,这一列里还有节点跟其他节点连着呢。

所以我们应当将该列所有节点找到,然后再把这列所有节点所在的所有行的其它节点的上下节点互指。这样就删完一列了。

那么恢复一列不就是逆操作吗?(懒,懒得讲了) (逃ε=ε=ε=┏(゜ロ゜;)┛

给个大概的示意图吧:

image

管你听没听懂,往下看就完了

还看不懂??给个代码吧:

struct DLX
{
    int n,m;///行列的规模
    int u[maxnode],d[maxnode],l[maxnode],r[maxnode];///结点四个方向的指针
    int col[maxnode],row[maxnode];///结点的行列指针
    int h[maxn],s[maxn];
    ///h[]行首结点(额外的行结点),s[]每一列的个数
    bool visit[maxn];///v[]是H()函数的标记数组
    int ansed,ans[maxn],siz;///答案的个数,答案,总结点数

    void ini(int _n,int _m)///初始化
    {
        n=_n,m=_m;///规模
        for(int i=1;i<=m;i++)///第1->m个结点作为列首节点
        {
            u[i]=i;///上下指针都指向自己
            d[i]=i;
            l[i]=i-1;///左右指针相连
            r[i]=i+1;
            col[i]=i;///列首节点的列指针指向本列
            row[i]=0;///列首节点的行指针为第0行
        }
        ///第0个结点(表头head)左指针指向node1,右指针指向nodem
        ///第m个结点右指针指向head,使得行首结点首尾相接
        l[0]=m,r[0]=1;
        r[m]=0;
        siz=m;
        clclow(h);///列首结点初始化为-1,表明该行全为0,没有指向哪个为1的结点
        clc(ans);
        ansed=0;///答案的数量为0
    }

    void Link(int R,int c)
    {
        siz++;///结点数+1
        ///接下来的操作是在Node_c和Node_c->down之间插入一个结点
        ///该列从上到下的行号不一定是从小到大的,但是插入的复杂度为O(1)
        u[siz]=c;///插入结点的up指针指向c
        d[siz]=d[c];///插入结点的down指针指向c->down
        u[d[c]]=siz;///结点c->down的up指针指向插入结点
        d[c]=siz;///列首结点c的down指针指向插入结点
        row[siz]=R,col[siz]=c;///设置行标和列标
        if(h[R]<0)///第r行还没有元素
        {
            h[R]=siz;///第r行的行指针指向插入结点
            r[siz]=l[siz]=siz;///插入的结点的左右指针都指向自己
        }
        else
        {///在行首结点H[r]和H[r]->right插入结点
            l[siz]=h[R];
            r[siz]=r[h[R]];
            l[r[h[R]]]=siz;
            r[h[R]]=siz;
        }
        return ;
    }

    void del(int c)///删除列及对应的行
    {
        r[l[c]]=r[c];///删除列首结点
        l[r[c]]=l[c];
        for(int i=d[c];i!=c;i=d[i])///遍历该列每个结点node_i
            for(int j=r[i];j!=i;j=r[j])///将与node_i同列的结点删除
            {
                d[u[j]]=d[j];
                u[d[j]]=u[j];
            }
        return ;
    }

    void recover(int c)///恢复列及对应的行
    {
        for(int i=u[c];i!=c;i=u[i])
            for(int j=l[i];j!=i;j=l[j])///恢复行
            {
                d[u[j]]=j;
                u[d[j]]=j;
            }
        r[l[c]]=c;///恢复列首结点
        l[r[c]]=c;
        return ;
    }

    bool Dancing(int dep) ///开始跳舞(前方高能!!!主要部分)
    {
     if(r[0]==0) ///表头的右指针指向自己
        {
            ansed=dep;
            return true;
        }
        int c=r[0];///表头右指针指向的列首结点c
        del(c);///删除第c列,及该列的1所在的行
        for(int i=d[c];i!=c;i=d[i])
        {
            ans[dep]=row[i];
            for(int j=r[i];j!=i;j=r[j])
                del(col[j]);
            if(Dancing(dep+1))
                return true;
            for(int j=l[i];j!=i;j=l[j])
                recover(col[j]);
        }
        return false;
    }
}

这里为什么要讲DLX?肯定是因为它可以解决数独啊!

而且DLX还是目前已知最快解决数独的方法

怎么用它来处理数独呢?那么我们就要把数独问题转换成精确覆盖问题了

这也谈到了一个精确覆盖问题的建模。不过也非常简单,每列代表一个限制条件,每行代表一种方案,若方案i满足限制j,则矩阵 $ mp[i][j]= $ 1,因此矩阵的构造既要满足所有限制条件,又不能漏掉可能的方案。

每行中每个数只能出现一遍,row(x,v)表示第x行有数字v。

每列中每个数只能出现一遍,col(x,v)表示第x列有数字v。

每宫中每个数只能出现一遍,block(i,v)表示编号为i的九宫格块内有数字v

每格中只能填一个数,exist(x,y)表示第x行y列有数字。

现在不就搞明白了吗?这道题就是纯纯的DLX的模板。直接上代码!!!


代码

#include <bits/stdc++.h>

using namespace std;

const int MAXN = 20000;
int m = 16 * 16 * 4, tmp;
int u[MAXN], d[MAXN], l[MAXN], r[MAXN];
int s[MAXN], col[MAXN], row[MAXN];
int ans[MAXN], top;
char mp[20][20];

struct Node{
	int x, y;
	char z;
}node[MAXN];

inline void init(){
	for (register int  i = 0; i <= m; ++i ){
		l[i] = i - 1;
		r[i] = i + 1;
		s[i] = 0;
		d[i] = u[i] = i;
	}
	l[0] = m;
	r[m] = 0;
	tmp = m + 1;
	
	return ;
}

inline void add(int &near1, int &near2, int x, int y){
	row[tmp] = x;
	col[tmp] = y;
	s[y] ++ ;

	u[tmp] = y;
	d[tmp] = d[y];
	u[d[y]] = tmp;
	d[y] = tmp;

	r[near1] = l[near2] = tmp;
	r[tmp] = near2;
	l[tmp] = near1;
	near2 = tmp++ ;

	return;
}

inline void remove(int p){
	r[l[p]] = r[p];
	l[r[p]] = l[p];
	for (register int i = d[p]; i != p; i = d[i])
		for (register int j = r[i]; j != i; j = r[j]){
			s[col[j]] -- ;
			u[d[j]] = u[j];
			d[u[j]] = d[j];
		}
	return ;
}

inline void resume(int p){
	for (register int i = u[p]; i != p; i = u[i]){
		for (register int j = l[i]; j != i; j = l[j]){
			u[d[j]] = j;
			d[u[j]] = j;
			s[col[j]] ++ ;
		}
	}
	r[l[p]] = p;
	l[r[p]] = p;
	
	return;
}

inline bool dfs(){
	if (!r[0]) return true;

	int minn = r[0];
	for (register int i = r[0]; i; i = r[i])
		if (s[i] < s[minn]) minn = i;

	remove(minn);
	for (register int i = d[minn]; i != minn; i = d[i]){
		ans[++top] = row[i];
		for (register int j = r[i]; j != i; j = r[j])
			remove(col[j]);
		if (dfs()) return true;
		for (register int j = l[i]; j != i; j = l[j])
			resume(col[j]);
		top--;
	}
	resume(minn);
	return false;
}

int main(){
	cin>>mp[0];
	while(1){
		for (register int i = 1; i < 16; ++i)
			cin>>mp[i];
		init();
		for (register int i = 0, n = 1; i < 16; ++i)
			for (register int j = 0; j < 16; ++j){
				int a = 0, b = 15;
				if (mp[i][j] != '-') a = b = mp[i][j] - 'A';
				for (register int k = a; k <= b; ++k, ++n){
					int near1 = tmp, near2 = tmp;
     				node[n] = {i, j, k + 'A'};
					add(near1, near2, n, i * 16 + j + 1);
					add(near1, near2, n, 256 + i * 16 + k + 1);
					add(near1, near2, n, 256 * 2 + j * 16 + k + 1);
					add(near1, near2, n, 256 * 3 + (i / 4 * 4 + j / 4) * 16 + k + 1);
				}
			}

		dfs();

		for (register int i = 1; i <= top; ++i){
			Node t = node[ans[i]];
			mp[t.x][t.y] = t.z;
		}

		for (register int i = 0; i < 16; ++i) puts(mp[i]);
		if (cin >> mp[0]) puts("");
		else break;
 	}

	return 0;
}

总结:

哈哈哈哈,又多了一个解决数独的方法,其实DLX听起来还是挺容易的,虽然这道题是黑题,其实难度也不是特别高,学会了DLX后,这就是道板子题!(以后解决数独就不用麻烦的写搜索了,狂喜)

附:

DLX可能有 许多 一些同学听的不是很懂,大家可以去网上自行搜索,或者去 OI WiKi 进行查阅!

看在写了这么长篇大幅的份上,路过的同学们给个赞行不行(坏笑~~

posted @ 2022-08-15 10:40  XDFZ武斌  阅读(145)  评论(0)    收藏  举报