精确覆盖问题、Algorithm X 与 Dancing Links(DLX)

精确覆盖(Exact Cover)问题

精确覆盖

S 为集合 X 的若干个子集构成的集合,若存在 S 的一个子集 S,满足 X 中的元素有且只有一次包含在 S 的一个元素中,那么称 S* 为 X 的一个精确覆盖。

S*属性

根据定义 S* 具有以下属性:

  1. S* 中任意两个元素交集为空。
  2. S* 中所有元素并集为 X。

例子

假定 S = { A, B, C, D} ,X = {1, 2, 3, 4, 5, 6} ,其中:
A = {1, 3, 5}
B = {2, 4}
C = {2, 3, 4, 5, 6}
D = {2, 4, 6}
那么 S* = {A, D} 是 X 的一个精确覆盖。而 {A, B}、{A, C}
分别不满足上述属性2、1,因而不是 X 的一个精确覆盖。

精确覆盖问题

精确覆盖问题是指给定一个 S,确定是否存在对 X 的一个精确覆盖,这是一个 NP 难问题。
这个问题可以用一个矩阵来表示


矩阵表示

在矩阵表示情形下,精确覆盖问题可以表述为选择特定的行使得所有的列有且只含有一个1。


精确覆盖

Algorithm X

X

不要被名字吓到,Donald E. Knuth 这么叫它的,本质上还是一个回溯法。其步骤如下所示:
维护两个集合——当前尚未满足的限制条件和可供选择的选项。
1.检查当前未满足的限制条件集合,若为空,则表示问题已经解决,输出答案并返回
2.否则选择其中一个不满足的限制条件。(可以使用启发式算法尽量加快代码运行)
3.找满足限制条件的选项集合,若为空,则表示当前这个位置不可能产出正确答案,返回。(剪枝)
4.否则,遍历上述选项集合(这一步可以是随机选择,这样会导致一个 non-deterministic 算法):
(1)当前选项可能满足多个限制条件,故对每个满足的限制条件,将其从未满足限制条件集合中移除,并且将所有能满足这个限制条件的选项也移除(因为精确覆盖要求不可重叠,若考虑完全覆盖则此时不需要移除)
(2)上述操作完成后形成“尚未满足的限制条件”和“可供选择的选项”这两个集合的子集合——即子问题,对此子问题递归求解。
(3)复原:上述递归完成后,在测试下一个选项前,我们需要恢复原来两个集合的样子。所以要将之前移除的条件和选项全部恢复。
使用 X 算法对上述例子求解如下:


X算法求解

Dancing Links

可以看出上述操作需要一对对偶操作——将一个元素从矩阵中移除,一番操作完成后还要将移除的这个元素重新添加回矩阵中去,这个用普通结构实现可能会比较复杂。

Insight——双向链表操作

将双向链表中的一个元素拿掉

x.left.right=x.right
x.right.left=x.left
双向链表删除

将该元素重新加入链表

x.left.right=x
x.right.left=x
双向链表恢复

DLX

我们可以类似地将每个节点都弄成四路链表,将所有连同节点连接起来,如下图所示:


4 way mesh

再加上约束条件属性和一些其他属性,我们的节点可以声明为如下形式:

struct Node
{
public:
    struct Node *left;
    struct Node *right;
    struct Node *up;
    struct Node *down;
    struct Node *column;
    int rowID;
    int colID;
    int nodeCount;
};

问题矩阵转化为DLX
只需要找到每个节点四个方向最邻近的“1”即可。这相当于将原先二维的一张平面扭曲成一个三维圆环面,如下图所示(来自3blue1brown):

平面2环面

Node *createToridolMatrix()
{
    for(int i = 0; i <= nRow; i++)
    {
        for(int j = 0; j < nCol; j++)
        {
          
            if(ProbMat[i][j])
            {
                int a, b;
                // first row represent constraints
                if(i) Matrix[0][j].nodeCount += 1;
                Matrix[i][j].column = &Matrix[0][j];
                Matrix[i][j].rowID = i;
                Matrix[i][j].colID = j;

                // Left pointer
                a = i; b = j;
                do{ b = getLeft(b); } while(!ProbMat[a][b] && b != j);
                Matrix[i][j].left = &Matrix[i][b];
                // Right pointer
                a = i; b = j;
                do { b = getRight(b); } while(!ProbMat[a][b] && b != j);
                Matrix[i][j].right = &Matrix[i][b];
                // Up pointer
                a = i; b = j;
                do { a = getUp(a); } while(!ProbMat[a][b] && a != i);
                Matrix[i][j].up = &Matrix[a][j];
                // Down pointer
                a = i; b = j;
                do { a = getDown(a); } while(!ProbMat[a][b] && a != i);
                Matrix[i][j].down = &Matrix[a][j];
            }
        }
    }

    //configure header
    header->right = &Matrix[0][0];
    header->left = &Matrix[0][nCol-1];
    Matrix[0][0].left = header;
    Matrix[0][nCol-1].right = header;
    return header;
}

删除节点
类比双向链表的删除即可,其主要删除两部分:属性和满足该属性行除本属性外其它所有子节点。略拗口,还是看图示吧。

remove

这是因为无论具体选择哪一行,另一行的其它节点都因为本属性互斥和不能发挥作用消除限制条件),所以把他们暂时屏蔽(删除)掉。
另外要注意只删除上下链,不删除左右链!

void remove(struct Node *targetNode)
{
    struct Node *row, *rightNode;
    struct Node *colNode = targetNode->column;
    //delete constraints
    colNode->left->right = colNode->right;
    colNode->right->left = colNode->left;

    //remove each row
    for(row = colNode->down; row != colNode; row = row->down)
    {
        for(rightNode = row->right; rightNode != row;
            rightNode = rightNode->right)
        {
            rightNode->up->down = rightNode->down;
            rightNode->down->up = rightNode->up;
            Matrix[0][rightNode->colID].nodeCount -= 1;
        }
    }
}

恢复节点
理解了删除节点,恢复节点就容易了,是上述过程的逆过程。

void resume(struct Node *targetNode)
{
    struct Node *rowNode, *leftNode;
    struct Node *colNode = targetNode->column;
    //resume row    
    for(rowNode = colNode->up; rowNode != colNode; rowNode = rowNode->up)
    {
        for(leftNode = rowNode->left; leftNode != rowNode;
            leftNode = leftNode->left)
        {
            leftNode->up->down = leftNode;
            leftNode->down->up = leftNode;          
            Matrix[0][leftNode->colID].nodeCount += 1;
        }    }

    // resume header
    colNode->left->right = colNode;
    colNode->right->left = colNode;
}

启发式选择列
我们先选择列节点数个数最少的来挑选,因为先选择其它的这个可能就被限制无法进行选择了。

Node *getMinColumn()
{
    struct Node *h = header;
    struct Node *min_col = h->right;
    h = h->right->right;
    do
    {
        if(h->nodeCount < min_col->nodeCount)
        {
            min_col = h;
        }
        h = h->right;
    }while(h != header);
    return min_col;
}

Let's dancing!
首先选择某一列,然后对满足该列的行进行遍历,每遍历一行,删除此行中同时满足其它列,进行递归即可。注意递归完成要恢复原样。

void dancing(int k)
{
    struct Node *rowNode;
    struct Node *rightNode;
    struct Node *leftNode;
    struct Node *column;
    //found solution
    if(header->right == header)
    {
        printSolutions();
        return;
    }
    // heuristic choose column
    column = getMinColumn();
    // remove chosen column
    remove(column);

    for(rowNode = column->down; rowNode != column;
        rowNode = rowNode->down )
    {
        solutions.push_back(rowNode);

        for(rightNode = rowNode->right; rightNode != rowNode;
            rightNode = rightNode->right)
            remove(rightNode);
        // recursive solve problem
        search(k+1);
        // if solution in not possible, backtrack (resume)
        solutions.pop_back();
        column = rowNode->column;
        for(leftNode = rowNode->left; leftNode != rowNode;
            leftNode = leftNode->left)
            resume(leftNode);
    }
    resume(column);
}

应用

对于有约束条件的搜索问题,我们可以将约束条件转化为每一列,而将所有可能情形转化为行,那么问题就转化为了前文中的矩阵情形:找出所有可能的行组合使得所有列有且只有一个1!举例如下:
1.拼图问题
找出是否存在一种方案,使得使用给定积木恰好填满给定区域。
[图片上传失败...(image-cae76b-1515323886474)]
2.数独求解


数独

3.N皇后问题


N皇后问题

具体怎么映射限制条件还是有一定 trick 的,这个以后有时间再单独拎出来分析一哈吧。

总结

本文主要介绍了精确覆盖问题、Algorithm X 与 DLX,给出了 DLX 的主要操作 remove 和 resume。同时点出了其在解决完全覆盖中的可能方案。最后介绍了精确覆盖问题在实际中的一些应用。

致谢

本文图片部分来自 Wei Li & Atul Kumar & 3blue1brown。同时由衷感谢 Donald E. Knuth 老爷爷!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,647评论 18 139
  • 前言 其实读完斯坦福的这本《互联网大规模数据挖掘》,让我感觉到,什么是人工智能?人工智能就是更高层次的数据挖掘。机...
    我偏笑_NSNirvana阅读 12,563评论 1 23
  • 简介 用简单的话来定义tcpdump,就是:dump the traffic on a network,根据使用者...
    保川阅读 5,953评论 1 13
  • 姓名:祝章冬 公司:宁波绿之品电器科技有限公司 第259期学员利他二组 【日精进打卡第27天】 【知~学习】 朗读...
    祝章冬阅读 159评论 0 0
  • 原创 2017-11-15 Lily妈 快乐育儿 俗话说:“三月的天,孩子的脸。”我们经常发现自己的孩子说不高兴就...
    Lily妈儿童早教阅读 11,924评论 0 1