【Coel.学习笔记】舞蹈链(Dancing Links X)

本来想补充几个 LCT 例题的,但还是太懒放弃了。
有这个时间多点几个技能不好吗?反正 LCT 又不是 NOI 的考点~
说得好像舞蹈链在大纲里面一样

引入

舞蹈链(\(\text{Dancing Links}\)) 是一种支持上下左右查询的双向十字链表,配合 X 算法使用合称 \(\text{Dancing Links X}\)(下称 DLX) 可以解决精确覆盖问题(使用普通搜索)和重复覆盖问题(使用启发式迭代加深搜索)。
朴素的搜索算法时间复杂度为 \(O(nm2^n)\),DLX 则可以做到 \(O(c^k)\),其中 \(k\) 为能贡献答案的点的个数, \(c\) 为一个非常接近 \(1\) 的常数。实际上,DLX 在实际应用中很难达到上限,对于 \(k\leq 10^4\) 的数据也能非常快地通过。

具体操作

DLX 重在建模,所以先看例题。

【模板】舞蹈链(DLX)

洛谷传送门
给定一个 \(01\) 矩阵,在矩阵中选出若干行,使得这些行的每一列都有且仅有一个数字 \(1\)

解析:这是一个 \(\mathsf{NP}\) 完全的问题,所以只能爆搜。当然,即使是爆搜也有优化的方式。

先考虑如下问题:

怎么存矩阵?

使用十字链表。对于每个数字 \(1\) 建立节点,然后看看每个节点的上、下、左、右四个方向应该与谁相连。在实际操作中,我们通常按行建立链表。
先做初始化,然后用两个指针维护左右关联的节点。每次插入时上下和本行一致,左右利用指针即可。

怎么利用链表搜索?

从没选到的行中任意选择一行开始做 dfs,再递归处理。显然,这样做可以保证搜到所有结果。

做哪些剪枝?

  1. 每次枚举的时候选择 \(1\) 的个数最少的列。
  2. 删掉不合法的所有列。比如说选择了第一行,这时第一行有 \(1\) 的列是第二列,那么第二列有 \(1\) 的都得删除。别忘了回溯时还得恢复整个链表,操作和删除操作正好相反。

\(\text{Talk is cheap, show you the code.}\)
代码注释粗略解释了各个操作的做法。

#include <algorithm>
#include <cstdio>
#include <iostream>
#include <vector>

using namespace std;

const int maxn = 1e4 + 10;

int n, m;
int l[maxn], r[maxn], u[maxn], d[maxn];  //上下左右
int row[maxn], col[maxn], s[maxn], cnt;  //行,列,本列 1 的个数
vector<int> ans;

void init_DLX() {  //初始化,没什么好说的
    for (int i = 0; i <= m; i++) {
        l[i] = i - 1, r[i] = i + 1;
        u[i] = d[i] = i;
    }
    l[0] = m, r[m] = 0;
    cnt = m + 1;
}

void add(int &head, int &tail, int x, int y) {  //给某一个点创建节点
    row[cnt] = x, col[cnt] = y, s[y]++;
    u[cnt] = y, d[cnt] = d[y], u[d[y]] = cnt, d[y] = cnt;
    r[head] = l[tail] = cnt, r[cnt] = tail, l[cnt] = head;
    tail = cnt++;
}

void remove(int p) {  //删除节点,只要维护四个数组就行了
    //这个操作和普通的链表没什么大区别,只不过上下要循环修改
    r[l[p]] = r[p], l[r[p]] = l[p];
    for (int i = d[p]; i != p; i = d[i])
        for (int j = r[i]; j != i; j = r[j]) {
            s[col[j]]--;
            u[d[j]] = u[j], d[u[j]] = d[j];
        }
}

void resume(int p) {  //恢复节点,和删除对偶
    for (int i = u[p]; i != p; i = u[i])
        for (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;
}

bool dfs() {
    if (!r[0]) return true;  //搜到边界
    int p = r[0];            // p 为当前所有列中 1 最少的
    for (int i = r[0]; i; i = r[i])
        if (s[i] < s[p]) p = i;
    remove(p);
    for (int i = d[p]; i != p; i = d[i]) {
        ans.push_back(row[i]);  //保存答案
        for (int j = r[i]; j != i; j = r[j]) remove(col[j]);
        if (dfs()) return true;  //往下能到达边界,答案正确
        for (int j = l[i]; j != i; j = l[j])
            resume(col[j]);  //答案不正确,恢复现场
        ans.pop_back();      //删除不正确的答案
    }
    resume(p);  //所有答案均不正确,恢复 p
    return false;
}

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    init_DLX();
    for (int i = 1; i <= n; i++) {
        int head = cnt, tail = cnt;
        for (int j = 1, x; j <= m; j++) {
            // x = 1 才需要建节点
            cin >> x;
            if (x) add(head, tail, i, j);
        }
    }
    if (dfs()) {
        sort(ans.begin(), ans.end());
        for (auto x : ans) cout << x << ' ';
    } else
        cout << "No Solution!";
    return 0;
}

例题讲解 - 重复覆盖问题

DLX 和网络流一样,解题使用的模板都差不多,但要认真分析题目性质、建立模型才能求解,这也就需要大量的练习与思考。

下面看几道 DLX 的应用。

SP1110 SUDOKU - Sudoku

洛谷传送门 双倍经验
给定一个 \(16\times 16\) 的数独,用大写英文字母 \(A\sim P\) 填满整个数独,使得每行、每列、每个 \(4\times 4\) 的宫中不存在重复字母。数据保证数独有且只有一组解。

解析:精确覆盖问题有一个常用的方法,把选法对应为 DLX 的行,限制对应为 DLX 的列。

对于这题,我们把每个格子填入的字母对应成行。列呢?数独限制比较多,我们仔细梳理一下。

  1. 每个格子只填一个字母。
  2. 每行的字母 \(A\sim P\) 都恰好只出现一次。
  3. 每列的字母都恰好只出现一次。
  4. 每宫的字母都恰好只出现一次。

对每个限制都开设一个节点,就成功建立了模型。类似网络流,可以证明这个模型做出的答案可以与原问题的方案一一对应,因此这个模型是可行的。

此外注意交到 UVA 上的时候最后不能输出两个 \n,要特判一下。

int main(void) {
    bool OvO = false;
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    while (cin >> g[0]) {
        if (OvO) cout << '\n';
        OvO = true;
        for (int i = 1; i < 16; i++) cin >> g[i];
        init_DLX();
        for (int i = 0, n = 1; i < 16; i++)
            for (int j = 0; j < 16; j++) {
                int a = 0, b = 15;
                if (g[i][j] != '-') a = b = g[i][j] - 'A';
                for (int k = a; k <= b; k++, n++) {
                    int head = cnt, tail = cnt;
                    op[n] = {i, j, char(k + 'A')};
                    add(head, tail, n, i * 16 + j + 1);
                    add(head, tail, n, 256 + i * 16 + k + 1);
                    add(head, tail, n, 256 * 2 + j * 16 + k + 1);
                    add(head, tail, n,
                        256 * 3 + (i / 4 * 4 + j / 4) * 16 + k + 1);
                }
            }
        dfs();
        for (auto x : ans) {
            auto t = op[x];
            g[t.x][t.y] = t.z;
        }
        for (int i = 0; i < 16; i++) cout << g[i] << '\n';
    }
    return 0;
}

[NOI2005] 智慧珠游戏

洛谷传送门
给出一个智慧珠的初始布局,求一种可行的智慧珠摆放方案,使所有的零件都能放进盘件中。智慧珠的摆放规则参见原题面。

解析:这是一道非常恶心的码农题,需要很强的代码力和 Debug 力……不管怎样,我们先看看怎么把问题转化成精确覆盖问题的模型。
对于行,我们用每种零件的摆法对应。零件最多翻转四次、旋转四次、平移 \(55\) 个格子,其中翻转又有 \(12\) 种零件,总共就是 \(12\times 8 \times 55=5280\) 行。
对于列,我们有如下几个限制:

  1. 每个格子只能放上零件的一个珠子,对应 \(55\) 列;
  2. 每个零件只能用一次,对应 \(12\) 列。
    这样,对应到 DLX 上的 \(1\) 就有 \(5280 \times(5+1)=31680\) 个。

在实际建立十字链表的时候,我们显然不能写上千行特判,要“少写多想”简化代码。对于翻转操作,沿中线交换两边即可;对于旋转操作,可以用两次翻转操作替代;对于平移操作,暴力枚举偏移量。

下面的代码把 DLX 部分用一个类封装起来。以后写数据结构也会尽量用这种方式~

#include <algorithm>
#include <cstring>
#include <iostream>
#include <set>
#include <vector>

using namespace std;

const int maxn = 5e4 + 10, m = 55 + 12;

char Pearl[12][4][5] = {
    //字符数组存图,暴力但优雅
    {"OO", "O "},                      // A
    {"OOOO", "    ", "    ", "    "},  // B
    {"OOO", "O  ", "   "},             // C
    {"OO", "OO"},                      // D
    {"O  ", "O  ", "OOO"},             // E
    {"OOOO", " O  ", "    ", "    "},  // F
    {"OOO", "O O", "   "},             // G
    {"OOO", "OO ", "   "},             // H
    {"OOO ", "  OO", "    ", "    "},  // I
    {" O ", "OOO", " O "},             // J
    {"O  ", "OO ", " OO"},             // K
    {"OOOO", "O   ", "    ", "    "},  // L
};
int id[10][10], s[maxn];
char chr[maxn], g[20][20];

vector<vector<vector<int>>> sol;
set<vector<vector<int>>> S;

class Dancing_Links_X {
   private:
    int u[maxn], d[maxn], l[maxn], r[maxn];
    int row[maxn], col[maxn];

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

    void resume(int p) {
        for (int i = u[p]; i != p; i = u[i])
            for (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;
    }

   public:
    int cnt = 0;
    vector<int> ans;

    void init() {
        for (int i = 0; i <= m; i++) {
            l[i] = i - 1, r[i] = i + 1;
            u[i] = d[i] = i;
        }
        l[0] = m, r[m] = 0;
        cnt = m + 1;
    }

    void add(int& head, int& tail, int x, int y) {
        row[cnt] = x, col[cnt] = y, s[y]++;
        u[cnt] = y, d[cnt] = d[y], u[d[y]] = cnt, d[y] = cnt;
        r[head] = l[tail] = cnt, r[cnt] = tail, l[cnt] = head;
        tail = cnt++;
    }

    bool dfs() {
        if (!r[0]) return true;
        int p = r[0];
        for (int i = r[0]; i; i = r[i])
            if (s[i] < s[p]) p = i;
        remove(p);
        for (int i = d[p]; i != p; i = d[i]) {
            ans.push_back(row[i]);
            for (int j = r[i]; j != i; j = r[j]) remove(col[j]);
            if (dfs()) return true;
            for (int j = l[i]; j != i; j = l[j]) resume(col[j]);
            ans.pop_back();
        }
        resume(p);
        return false;
    }

} DLX;

void rev_plk(char str[4][5]) {  //水平
    int len = strlen(str[0]);
    for (int i = 0, j = len - 1; i < j; i++, j--)
        for (int k = 0; k < len; k++) swap(str[i][k], str[j][k]);
}

void rev_dia(char str[4][5]) {  //对角线
    int len = strlen(str[0]);
    for (int i = 0; i < len; i++)
        for (int j = 0; j < i; j++) swap(str[i][j], str[j][i]);
}

auto get(char str[4][5], int a, int b) {
    vector<vector<int>> res;
    for (int i = 0; i < 4; i++)
        for (int j = 0; j < 4; j++)
            if (str[i][j] == 'O') res.push_back({i + a, j + b});
    return res;
}

bool check(char str[4][5], int c, int a, int b) {
    auto A = get(str, a, b);
    vector<vector<int>> B;
    for (int i = 0; i < 10; i++)
        for (int j = 0; j <= i; j++)
            if (g[i][j] == c + 'A') B.push_back({i, j});
    if (!B.empty()) {
        sort(A.begin(), A.end());
        sort(B.begin(), B.end());
        return A == B;
    }
    for (auto& p : A) {
        int x = p[0], y = p[1];
        if (x < 0 || x >= 10 || y < 0 || y >= 10) return false;
        if (g[x][y] != '.') return false;
    }
    return true;
}

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    DLX.init();
    for (int i = 0, k = 1; i < 10; i++)
        for (int j = 0; j <= i; j++, k++) id[i][j] = k;
    for (int i = 0; i < 10; i++) cin >> g[i];
    for (int i = 0; i < 12; i++)
        for (int u = 0; u < 2; u++) {
            for (int j = 0; j < 4; j++) {
                for (int a = -10; a < 10; a++)
                    for (int b = -10; b < 10; b++) {
                        if (check(Pearl[i], i, a, b)) {
                            auto t = get(Pearl[i], a, b);
                            if (S.count(t)) continue;
                            int head = DLX.cnt, tail = DLX.cnt;
                            for (auto& p : t) {
                                int x = p[0], y = p[1];
                                DLX.add(head, tail, sol.size(), id[x][y]);
                            }
                            DLX.add(head, tail, sol.size(), 56 + i);
                            chr[sol.size()] = i + 'A';
                            sol.push_back(t);
                            S.insert(t);
                        }
                    }
                rev_plk(Pearl[i]);
                rev_dia(Pearl[i]);
            }
            rev_plk(Pearl[i]);
        }
    if (DLX.dfs()) {
        for (auto x : DLX.ans) {
            auto& l = sol[x];
            char z = chr[x];
            for (auto& p : l) {
                int x = p[0], y = p[1];
                g[x][y] = z;
            }
        }
        for (int i = 0; i < 10; i++) cout << g[i] << '\n';
    } else
        cout << "No solution";
    return 0;
}

上面提到的都是精确覆盖问题的模型,下面看几个重复覆盖问题模型。

例题讲解 - 重复覆盖问题

下面的题有可能洛谷未收录,就放其他 OJ 的传送门了。

【模板】舞蹈链 - 精确覆盖问题

AcWing传送门
给定一个 \(01\) 矩阵,找到最少的行数,使得这些行的每一列都有 \(1\)

解析:重复覆盖问题使用启发式迭代加深搜索(IDA*)实现。

搜索的顺序和精确覆盖问题是一样的,区别在于选择一行时,只会删除与其相关联的所有其他列,而不会连锁删除。为了保证搜索的效率,加上迭代加深,迭代层数为当前使用的行数;并使用启发式搜索,把估价函数设计为“最少还需要选择多少行才能完全覆盖所有列”。实践证明,这个估价函数是合理的。

尽管加上了各种优化,重复覆盖问题效率仍然不如精确覆盖问题,只能解决 \(n\leq 100\) 的数据范围。

#include <cstring>
#include <iostream>
#include <vector>

using namespace std;

const int maxn = 100 * 100 + 10;

int n, m, dep;

class Dancing_Links_X {
   private:
    int l[maxn], r[maxn], u[maxn], d[maxn];
    int col[maxn], row[maxn], s[maxn];
    bool vis[maxn];

    int h() {
        int res = 0;
        memset(vis, 0, sizeof(vis));
        for (int i = r[0]; i; i = r[i]) {
            if (vis[col[i]]) continue;
            res++;
            vis[col[i]] = true;
            for (int j = d[i]; j != i; j = d[j])
                for (int k = r[j]; k != j; k = r[k]) vis[col[k]] = true;
        }
        return res;
    }

    void remove(int p) {
        for (int i = d[p]; i != p; i = d[i]) r[l[i]] = r[i], l[r[i]] = l[i];
    }

    void resume(int p) {
        for (int i = u[p]; i != p; i = u[i]) r[l[i]] = i, l[r[i]] = i;
    }

   public:
    int cnt = 0, ans[maxn];

    void init() {
        for (int i = 0; i <= m; i++) {
            l[i] = i - 1, r[i] = i + 1;
            col[i] = u[i] = d[i] = i;
            s[i] = 0;
        }
        l[0] = m, r[m] = 0, cnt = m + 1;
    }

    void add(int &head, int &tail, int x, int y) {
        row[cnt] = x, col[cnt] = y, s[y]++;
        u[cnt] = y, d[cnt] = d[y], u[d[y]] = cnt, d[y] = cnt;
        r[head] = l[tail] = cnt, r[cnt] = tail, l[cnt] = head;
        tail = cnt++;
    }

    bool IDA(int k, int dep) {
        if (k + h() > dep) return false;
        if (!r[0]) return true;
        int p = r[0];
        for (int i = r[0]; i; i = r[i])
            if (s[p] > s[i]) p = i;
        for (int i = d[p]; i != p; i = d[i]) {
            ans[k] = row[i];
            remove(i);
            for (int j = r[i]; j != i; j = r[j]) remove(j);
            if (IDA(k + 1, dep)) return true;
            for (int j = l[i]; j != i; j = l[j]) resume(j);
            resume(i);
        }
        return false;
    }

} DLX;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    DLX.init();
    for (int i = 1; i <= n; i++) {
        int head = DLX.cnt, tail = DLX.cnt;
        for (int j = 1, x; j <= m; j++) {
            cin >> x;
            if (x) DLX.add(head, tail, i, j);
        }
    }
    while (!DLX.IDA(0, dep)) dep++;
    cout << dep << '\n';
    for (int i = 0; i < dep; i++) cout << DLX.ans[i] << ' ';
    return 0;
}

[POJ1084] Square Destroyer

洛谷传送门
给定一个由若干根火柴组成的网格(不一定完整),求出至少拿走多少根火柴棒可以使得网格中没有正方形。
解析:重复覆盖问题的模型和精确覆盖问题一样,都是用行表示选择,列表示限制。本题中,行表示选择的火柴,列对应正方形。

在建模过程中,如果某根火柴可以破坏掉某些正方形,就意味着模型中火柴对应的行可以覆盖上正方形对应的列,给相交区域建立节点即可。这样就可以保证,重复覆盖问题的模型中每一个方案都能与原问题的解一一对应,那么模型求出的最优方案就是原问题的最优解。

另外,我们还得考虑一下怎么实现建图。先枚举所有边长为 \(1\) 的正方形,再枚举边长为 \(2\) 的正方形……对于每一个正方形,我们都可以用一个 vector 存下所有关联的火柴,然后对每根火柴,暴力枚举正方形中是否存在这根火柴。如果有,就可以直接在 DLX 里加节点了。

火柴最多有 \(2\times 5\times(5+1)=60\) 根,正方形最多有 \(1^2+2^2+3^2+4^2+5^2=55\) 个,最多就有 \(60\times 55=3300\) 个节点。实际上 DLX 中的节点量比这个值少很多,可以轻松通过本题。

#include <algorithm>
#include <cstring>
#include <iostream>
#include <vector>

using namespace std;

const int maxn = 3300 + 50;

int n, m, k, T, dep;
bool vis[100];
vector<int> rec[100];

class Dancing_Links_X {
   private:
    int l[maxn], r[maxn], u[maxn], d[maxn];
    int col[maxn], row[maxn], s[maxn];

    int h() {
        int res = 0;
        memset(vis, 0, sizeof(vis));
        for (int i = r[0]; i; i = r[i]) {
            if (vis[i]) continue;
            vis[i] = true;
            res++;
            for (int j = d[i]; j != i; j = d[j])
                for (int k = r[j]; k != j; k = r[k]) vis[col[k]] = true;
        }
        return res;
    }

    void remove(int p) {
        for (int i = d[p]; i != p; i = d[i]) r[l[i]] = r[i], l[r[i]] = l[i];
    }

    void resume(int p) {
        for (int i = u[p]; i != p; i = u[i]) r[l[i]] = l[r[i]] = i;
    }

   public:
    int cnt;

    void init() {
        for (int i = 0; i <= m; i++) {
            l[i] = i - 1, r[i] = i + 1;
            col[i] = u[i] = d[i] = i;
            s[i] = 0;
        }
        l[0] = m, r[m] = 0;
        cnt = m + 1;
    }

    void add(int& head, int& tail, int x, int y) {
        row[cnt] = x, col[cnt] = y, s[y]++;
        u[cnt] = y, d[cnt] = d[y], u[d[y]] = cnt, d[y] = cnt;
        r[head] = l[tail] = cnt, r[cnt] = tail, l[cnt] = head;
        tail = cnt++;
    }

    bool IDA(int k, int dep) {
        if (k + h() > dep) return false;
        if (!r[0]) return true;
        int p = r[0];
        for (int i = r[0]; i; i = r[i])
            if (s[p] > s[i]) p = i;
        for (int i = d[p]; i != p; i = d[i]) {
            remove(i);
            for (int j = r[i]; j != i; j = r[j]) remove(j);
            if (IDA(k + 1, dep)) return true;
            for (int j = l[i]; j != i; j = l[j]) resume(j);
            resume(i);
        }
        return false;
    }

} DLX;

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> T;
    while (T--) {
        memset(vis, 0, sizeof(vis));
        m = 0;
        cin >> n >> k;
        for (int i = 1, x; i <= k; i++) {
            cin >> x;
            vis[x] = true;
        }
        for (int l = 1; l <= n; l++)                    //正方形边长
            for (int a = 1; a <= n + 1 - l; a++)        //正方形行号
                for (int b = 1; b <= n + 1 - l; b++) {  //正方形列号
                    auto& S = rec[++m];
                    S.clear();
                    int d = n * 2 + 1;
                    for (int i = 0; i < l; i++) {
                        S.push_back((a - 1) * d + b + i);
                        S.push_back((a - 1) * d + b + i + d * l);
                        S.push_back((a - 1) * d + b + n + d * i);
                        S.push_back((a - 1) * d + b + n + d * i + l);
                    }
                    for (auto x : S)
                        if (vis[x]) {
                            m--;
                            break;
                        }
                }
        DLX.init();
        for (int i = 1; i <= n * (n + 1) * 2; i++) {
            if (!vis[i]) {
                int head = DLX.cnt, tail = DLX.cnt;
                for (int j = 1; j <= m; j++) {
                    auto& S = rec[j];
                    if (find(S.begin(), S.end(), i) != S.end())
                        DLX.add(head, tail, i, j);
                }
            }
        }
        dep = 0;
        while (!DLX.IDA(0, dep)) dep++;
        cout << dep << '\n';
    }

    return 0;
}
posted @ 2022-08-05 17:47  秋泉こあい  阅读(116)  评论(0编辑  收藏  举报