DLX

news/2024/9/20 8:48:35

写这玩意的原因是初赛做到了
SadBee csdn

第一部分:DLX算法的提出

1.1一类被称为精确覆盖的问题

在计算机科学中,精确覆盖问题指找出这样的一种覆盖,或证明其不存在。
满足以下条件的集合为一个精确覆盖:

  1. S*中任意两个集合没有交集,即X中的元素在S*中出现最多一次
  2. S*中集合的全集为X,即X中的元素在S*中出现最少一次

合二为一,即X中的元素在S*中出现恰好一次。
这是一个NP-完全问题,也是卡普的二十一个NP-完全问题之一。

1.2经典的精确覆盖问题举例——矩阵覆盖

模板

1.2.1问题描述

给定一个N行M列的矩阵,矩阵中的每个元素要么是1,要么是0
现在需要在矩阵中挑选若干行,使得对于矩阵的每一列 j,在挑出的这些行中,有且仅有一行的第 j个元素为1,比如下面的选择方法:

简单的来说,就是从一个由0、1组成的矩阵中挑出若干整行,重新组成一个新的矩阵,使其满足下列条件:

  1. 设新矩阵的列数为M,则这个新矩阵中刚好含有M个1。
  2. 在这个新的矩阵中,任意两个1不位于同一列。(不知道看到这里有没有想到八皇后问题,这也是DLX的应用之一)

1.2.2从暴力算法到X算法

最直接也是最容易想到的方法应该就是暴力枚举算法了。从第一行开始,DFS枚举下面的所有行,利用判定函数判断是否成功。

如果刚好覆盖,层层跳出递归(利用flag);
如果到达底层都不成功则进行回溯,到上一层递归进入下一个状态,继续DFS搜索,直到找到对应的答案为止。
(暴力的代码就不写了,反正复杂度绝对死翘翘)

上述的暴力复杂度是多少呢?考虑到每一行都有两种状态:被选中以及未选中。因此递归的复杂度达到了2的N次方,妥妥的T飞。
下面进行一次优化,经过认真观察可以得到在搜索时如果有两个1位于同一列中,那么该种情况必定不合法,再继续搜索下去没有必要,此时直接回溯,可以极大降低时间复杂度。
进行第二次优化,既然不会有两个1出现在同一列中,也就是说如果递归选中了一行,那么该行中所有1所在的列也可以一起删除掉,以减小矩阵规模。
到了这里,基本与Donald E.Knuth的X算法很接近了

第二部分:精确覆盖问题的X算法

X算法的核心依然是采用回溯穷举搜索

2.1 X算法分析——矩阵覆盖问题的解决

Step 1:图中最上面一行(紫色标出)称为哨兵行,用于监视覆盖状态。首先选择一行(通常选择含1数量最少的一行),图中用红色标出。同时,在同一列中也有1且与红色行冲突的列用蓝色标出

Step 2:从所有蓝色的列看下去,把所有不属于红色行的1标注出来,并选中他们所在的行(图中用青色标注出来)

Step 3:根据前面的分析,图中用蓝色和青色标注出来的行和列都是不需要的,因此执行删除操作,大量减少搜索的范围。接着将剩下的矩阵单独提取出来。

Step 4:继续选择第一行,对剩下的矩阵进行同样的操作(递归性的体现),删除用红色、蓝色、青色标注出的行和列。

Step 5:经过步骤4的删除后,矩阵变成了空矩阵,但是哨兵栏并未清空,所以此次搜索失败,回溯到第三步的状态。

Step 6:回溯到之前的状态,这次选择第二行,再次进行查找、删除操作

Step 7:再次执行删除操作,矩阵中所有元素都被删除(包括哨兵栏)。至此,找到了一个精确覆盖的可行解。


第三部分:用于X算法的数据结构——双向十字循环链表

3.1主角登场——双向十字循环链表

概览上述X算法执行的过程中,出现了大量的删除、添加操作,最容易想到的数据结构是什么呢?当然是链表!
然而,不同的链表不能处理二维的数据,同时考虑到在此过程中大量的删除、添加操作,因此需要一种二维强化版的链表,这就是——双向十字循环链表(简称舞蹈链)。
先来看看整体的舞蹈链长什么样子:

整体链表的最上端是哨兵节点(上文有提过它的作用)。
对于每一个节点而言:

  1. 首先,对于每一个节点而言,他的四个方向都被链接,所以称为十字链表。
  2. 其次,每一条链接都是双向的,可以由左节点访问右节点,也可以有右节点访问左节点,所以它又是双向链表。
  3. 最后,它的行和列都是循环的,意味着可以通过每一列的哨兵节点向上访问列的最末节点。
    每一行也是同样的,具有循环链表的性质。

3.2详细分析——双向十字循环链表的操作

Part 1:舞蹈链的节点结构体定义

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int MAXN=250501;
int ansk[MAXN]; //存储答案的数组 
int node_size,row_head[MAXN]; //节点总数,第i行的头节点编号 
struct Node
{int left,right; //左右节点编号 int upper,down;	//上下节点编号 int row,col;	//节点所在行数、列数 int col_size;	//如果该节点为列哨兵节点,存储该列的节点总数 
}node[MAXN];

Part 2:舞蹈链的初始化函数

void init(int col_size)
{for(int i=0;i<=col_size;i++){node[i].col=i,node[i].row=0;	//初始化行号、列号 node[i].upper=node[i].down=i;	 //节点上下形成自环 node[i].left=i-1; node[i].right=i+1;	//左右与其他节点相连 node[i].col_size=0;	//列节点数初始化为0 }/*链接最左节点与最右节点,形成横向的环*/ node[col_size].right=0;node[0].left=col_size;memset(row_head,-1,sizeof(row_head));	//每一行的头节点初始化为-1 node_size=col_size+1;	//非常重要!!!!!!!!千万别忘了 
}

说明:舞蹈链的初始化主要针对第一行的哨兵节点,哨兵节点的上下形成自环,左右与其他节点相连。每列节点数初始化为0,每行头节点初始化为-1重要提醒:最后千万别忘了给节点总数赋值!!!不然建立新结点的时候会有下标冲突。
Part 3:新节点的建立与插入函数


void insert_node(int row,int col)
{node[col].col_size++;    //所在列的节点数+1node[node_size].row=row,node[node_size].col=col;node[node_size].down=col;    //新节点与哨兵节点相连node[node_size].upper=node[col].down;    //新节点与哨兵节点下方的节点相连node[node[col].down].down=node_size;    //哨兵节点下方的节点与新节点相连node[col].upper=node_size;    //哨兵节点与新节点相连if(row_head[row]==-1)    //如果当前所在行没有头节点{row_head[row]=node_size;    //当前节点作为头节点node[node_size].left=node_size;node[node_size].right=node_size;}else{/*与所在行的头节点以及头节点左侧的节点相连*/node[node_size].left=node[row_head[row]].left;node[node[row_head[row]].left].right=node_size;node[node_size].right=row_head[row];node[row_head[row]].left=node_size;row_head[row]=node_size;    //因为插入位置,代替了原来的头节点}node_size++;    //节点总数自增操作return;
}

说明:根据给出的行号、列号从内存池中建立相应的新节点,为新节点的结构体赋值。同时将新节点插入到原来已经构建好的关系网中。插入的原则为:在下方直接与所在列的哨兵节点相连,上方与原来哨兵节点连接的节点相连。
特别注意连接顺序(先连接哨兵节点连接的节点,再连接哨兵节点,同时注意边的双向性)。左侧与所在行的头节点的左节点(循环性,可以认为是末尾节点)相连,右侧与头节点相连。
特别注意:如果将要插入的行现在没有节点,那么当前节点作为该行的头节点。=)
Part 4:指定列的删除和恢复函数

void node_delete(int col)
{/*先把哨兵节点的左右节点链接,跳过哨兵节点*/node[node[col].left].right=node[col].right;node[node[col].right].left=node[col].left;/*从该行哨兵节点开始,自上而下遍历,并删除遍历到的节点所在行*/for(int i=node[col].down;i!=col;i=node[i].down){for(int j=node[i].right;j!=i;j=node[j].right){node[node[j].upper].down=node[j].down;    //将上方的节点连接至下方node[node[j].down].upper=node[j].upper;    //将下方的节点连接至上方node[node[j].col].col_size--;    //所在列的节点总数-1}}
}void node_reverse(int col)
{for(int i=node[col].upper;i!=col;i=node[i].upper){for(int j=node[i].left;j!=i;j=node[j].left){node[node[j].upper].down=j;    //删除逆操作node[node[j].down].upper=j;node[node[j].col].col_size++;}}/*把哨兵节点连接回去*/node[node[col].left].right=col;node[node[col].right].left=col;
}

说明:删除给定行时,先把哨兵节点移除,即把哨兵节点的左右节点互连,跳过哨兵节点。接下来自上而下遍历所有位于该列的节点,删除各个节点所在的行。回复给定行即为删除的逆操作。
Final Part:起舞函数——让指针跳起来

bool dance(int depth)
{/*如果当前哨兵节点已经被删除完了,说明递归成功,输出答案并返回*/if(node[0].right==0) {for(int i=0;i<depth;i++) cout<<ansk[i]<<" "; return true;}/*开始遍历:首先找到节点数最小的列*/int temp=node[0].right;for(int i=node[0].right;i!=0;i=node[i].right) if(node[i].col_size<node[temp].col_size) temp=i;node_delete(temp);    //删除该列for(int i=node[temp].down;i!=temp;i=node[i].down)    //自上而下遍历,逐层递归{ansk[depth]=node[i].row;    //修改当前答案数组里的值,如果有上一次递归失败的值,直接覆盖for(int j=node[i].right;j!=i;j=node[j].right) node_delete(node[j].col);if(dance(depth+1)) return true;    //递归下一层,如果下一层递归成功,返回for(int j=node[i].left;j!=i;j=node[j].left) node_reverse(node[j].col);}node_reverse(temp);    //恢复该列return false;
}

函数说明:主要看之前的X算法的过程理解。重要步骤:递归开始前寻找节点数最小的列进行递归。同时注意状态回溯的时候要把删除的列全部回溯到之前状态。
补充附录:示例程序

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int MAXN=250501;
int ansk[MAXN];
int node_size,row_head[MAXN];
struct Node
{int left,right;int upper,down;int row,col;int col_size;
}node[MAXN];void init(int col_size)
{for(int i=0;i<=col_size;i++){node[i].col=i,node[i].row=0;node[i].upper=node[i].down=i;node[i].left=i-1; node[i].right=i+1;node[i].col_size=0;}node[col_size].right=0;node[0].left=col_size;memset(row_head,-1,sizeof(row_head));node_size=col_size+1;
}void insert_node(int row,int col)
{node[col].col_size++;node[node_size].row=row,node[node_size].col=col;node[node_size].down=col;node[node_size].upper=node[col].down;node[node[col].down].down=node_size;node[col].upper=node_size;if(row_head[row]==-1){row_head[row]=node_size;node[node_size].left=node_size;node[node_size].right=node_size;}else{node[node_size].left=node[row_head[row]].left;node[node[row_head[row]].left].right=node_size;node[node_size].right=row_head[row];node[row_head[row]].left=node_size;row_head[row]=node_size;}node_size++;return;
}void node_delete(int col)
{node[node[col].left].right=node[col].right;node[node[col].right].left=node[col].left;for(int i=node[col].down;i!=col;i=node[i].down){for(int j=node[i].right;j!=i;j=node[j].right){node[node[j].upper].down=node[j].down;node[node[j].down].upper=node[j].upper;node[node[j].col].col_size--;}}
}void node_reverse(int col)
{for(int i=node[col].upper;i!=col;i=node[i].upper){for(int j=node[i].left;j!=i;j=node[j].left){node[node[j].upper].down=j;node[node[j].down].upper=j;node[node[j].col].col_size++;}}node[node[col].left].right=col;node[node[col].right].left=col;
}bool dance(int depth)
{if(node[0].right==0) {for(int i=0;i<depth;i++) cout<<ansk[i]<<" "; return true;}int temp=node[0].right;for(int i=node[0].right;i!=0;i=node[i].right) if(node[i].col_size<node[temp].col_size) temp=i;node_delete(temp);for(int i=node[temp].down;i!=temp;i=node[i].down){ansk[depth]=node[i].row;for(int j=node[i].right;j!=i;j=node[j].right) node_delete(node[j].col);if(dance(depth+1)) return true;for(int j=node[i].left;j!=i;j=node[j].left) node_reverse(node[j].col);}node_reverse(temp);return false;
}int main()
{int n,m; scanf("%d%d",&n,&m);init(m);for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){int temp; scanf("%d",&temp);if(temp) insert_node(i,j);}}if(!dance(0)) printf("No Solution!");return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/62403.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

【VMware VCF】使用 VCF Import Tool 将现有 vSphere 环境转换为管理域。

VMware Cloud Foundation 5.2 发布并引入了一个新的功能,借助 VCF Import Tool 工具可以将现有 vSphere 环境直接转换(Convert)为管理工作负载域或者导入(Import)为 VI 工作负载域。通过这种能力,客户无需购买新硬件和进行复杂的部署和迁移工作,即可将已有的环境快速转变…

大模型应用开发初探 : 通用函数调用Planner

自动函数调用对大模型有较高的要求,比如Azure OpenAI、智谱AI等这些收费的大模型产品就能很好地规划和处理函数调用,而像是一些开源的小参数量的模型例如qwen2-7b-instruct这种可能效果就不太好。刚好,之前在网上看到一位大佬的开源通用函数调用的方案,于是重构了一下上一篇…

vuex状态管理 - 使用教程

vuex使用教程一、概述 1.组件之间的数据共享 (小范围内的数据共享) ​ 父向子传值:v-bind 属性绑定 ​ 子向父传值:v-on 事件绑定 ​ 兄弟组件之间共享数据:$on 接受数据的那个组件 $emit 发送数据的那个组件2.vuex是实现组件全局状态(数据)管理的一种机制,可以…

dwg、dxf文件多行文字转单行文字

CAD 在CAD软件中,将多行文字转换为单行文字的过程相对简单。以下是具体的操作步骤: ‌使用“X”分解命令(炸开)‌:首先,书写多行文字,通过蓝色夹点可以看出这些文字是一个整体。然后,将刚写的多行文字作为选择对象选中,选择分解命令或命令行输入“X”,然后回车。完成…

django 缓存表格到内存

django 缓存表格到内存 在Django中,你可以使用缓存来提高性能。如果你想要缓存一个模型的表格数据到内存中,可以使用Cache类和caching framework。以下是一个简单的例子,展示了如何缓存一个模型的查询结果: 首先,确保你的settings.py中启用了缓存系统,并选择了一个缓存后…

MQTT——工具mosquitto使用及问题记录

================================================================================================在CentOS服务器使用Mosquitto安装和部署MQTT之前在Windows服务器上安装部署RabbitMQ3.8,今天又双叒叕换了软件,叫MQTT,首先先了解下软件,RabbitMQ : 是一个支持AMQP协议…

visio圆角矩形怎么改变圆角大小

visio圆角矩形怎么改变圆角大小在‌Visio中调整‌圆角矩形的大小‌ 在Visio中,可以通过以下步骤调整圆角矩形的大小:选择矩形工具,绘制一个矩形。 右键点击矩形,选择“设置形状格式”。 在弹出的窗口中,选择“线条”选项卡。 在“端点类型”中选择“圆形”,并设置圆角的…

.NET常见的几种项目架构模式,你知道几种?(附带使用情况投票)

前言 项目架构模式在软件开发中扮演着至关重要的角色,它们为开发者提供了一套组织和管理代码的指导原则,以提高软件的可维护性、可扩展性、可重用性和可测试性。假如你有其他的项目架构模式推荐,欢迎在文末留言🤞!!!项目架构模式使用情况收集(微信投票,请在微信中打开…