介绍

我们希望在二进制下枚举所有的 mask,并且枚举每个mask的所有子集。

for (int mask = 0; mask < (1 << n); mask++) {  // i 枚举所有物体的选择情况
    for (int sub = mask; sub; sub = (sub - 1) & mask) {  // j 枚举了 i 的子集
        ...
    }
}

原理

每次 sub 都相当于移除了最后一个 $1$,然后再和 mask AND 一下。

复杂度

考虑 mask 中 $bit$ 的数量,如果有 $i$ 个 $1$,那么就有 $C_n^i$ 种。而 submask 中,每个 $1$ 可选可不选,所以有 $2^i$ 种。

总复杂度为(由二项式定理可得):

$$\sum\limits_{i=0}^n C_n^i 2^i = (1+2)^n = 3^n$$

例题

例1 Atcoder ABC187F. Close Group

题意

给定一个 $n$ 个节点,$m$ 条边的无向图,我们可以删除任意数量的边,使得删除后,每个联通块都是完全联通的(两两之间一定有边)。

求,在这样操作后,图中最少有多少个联通块?

其中,$1 \leq n \leq 18, 0 \leq m \leq \frac{n(n-1)}{2}$。

题解

很明显的二进制枚举。

int dp[mask] 为这个 mask 对应的最少联通块数量。

先处理 bool ok[mask] 判断一个mask是否已经完全联通。

然后枚举子集就可以了。

• 如果 masksubmask 都知道,计算 mask - submask 只要使用 XOR 即可。

代码
#include <bits/stdc++.h>
using namespace std;
const int maxm = 5e5+505;

int n, m;
int adj[20][20];
int dp[maxm];
bool ok[maxm];

int main() {
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int u,v; cin >> u >> v;
        adj[u][v] = adj[v][u] = 1;
    }
    for (int mask = 1; mask < (1<<n); mask++) {
        ok[mask] = 1;
        vector<int> vec;
        for (int j = 0; j < n; j++) {
            if (mask & (1<<j)) vec.push_back(j+1);
        }
        for (int i : vec) {
            for (int j : vec) {
                if (i != j && !adj[i][j]) {
                    ok[mask] = 0;
                    break;
                }
            }
        }
    }

    for (int mask = 1; mask < (1<<n); mask++) {
        dp[mask] = 1e9;
    }
    dp[0] = 0;
    for (int mask = 1; mask < (1<<n); mask++) {
        for (int sub = mask; sub; sub = (sub - 1) & mask) {
            if (ok[sub]) {
                dp[mask] = min(dp[mask], 1 + dp[mask ^ sub]);
            }
        }
    }
    cout << dp[(1<<n)-1] << endl;
}