状态压缩的位操作
- 取最小的 1:
i & -i - 去掉最小的 1:
i &= i-1 - 判断第
j
j
j 位是否为 1:
i>>j & 1
对于普通的状态压缩,由于位数都不多,我们可以直接用循环对每个位枚举即可。
令 f[s][i] 表示当前二进制状态为
s
s
s,最后一个加入的点为
i
i
i 的最优解。
由于二进制从第 0 0 0 为开始计算,所以通常下标要注意减 1 1 1。
枚举的顺序
- 初始只有 1 个点,依次增加,使用
for (int i = 1; i < (1<<n); ++i)枚举所有的状态; - 初始所有点都在,每次去掉一个点,倒着枚举,使用
for (int i = (1<<n)-1; i; --i)。
人人为我
f[s][i] <- f[s^(1<<i)][j],其中
s
s
s 是第
i
i
i 位为
1
1
1 的二进制状态,枚举 s^(1<<i) 状态中的每一个
1
1
1 来更新 f[s][i]。
我为人人
f[s^(1<<i)][i] <- f[s][j],其中
s
s
s 是第
j
j
j 位为
1
1
1,第
i
i
i 位为
0
0
0 的二进制状态,枚举 s 状态中的每一个
1
1
1 来更新 f[s^(1<<i)][i]。
CF11D-A Simple Task

思路:
数据规模不大,显然可采用状态压缩先构建所有的链,然后将首尾相连构成环进行计数即可。
主要思考如下两个问题:
-
如何不重复地构建链
每条链一定存在一个编号最小的点,我们保持每条链的其中一个端点是编号最小的点即可。具体做法,就是在枚举状态时,绝不连接终点为该状态最小编号的边。 -
如何构建环
每次连接时,枚举当前状态的每一个点,与状态中最小编号端点直接连接,均构成一个环,进行计数即可。
这里还要注意两个问题:
(1)每个环从最小端点朝两个方向都被计数一次,最终要除以 2。
(2)每条对称边也被计数,但只被计 1 次。
代码采用两种方式:
第一种:我为人人,用不在状态里的点去更新当前枚举的状态。这样可以把代码写得比较短,集中在一个循环里完成两种操作。
#include <iostream>
#include <unordered_map>
using namespace std;
const int N = 1<<19 | 7;
using LL = long long;
LL f[N][22];
bool g[22][22];
unordered_map<int, int> ump;
int main() {
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
ump[1<<i-1] = i, f[1<<i-1][i] = 1;
for (int i = 1, u, v; i <= m; ++i) {
scanf("%d%d", &u, &v);
g[u][v] = g[v][u] = true;
}
LL ans = 0;
for (int mask = 1; mask < (1<<n); ++mask) {
int mn = ump[mask & -mask];
for (int bi = mask; bi; bi &= bi-1) {
int t = bi & -bi, i = ump[t];
for (int j = 1; j <= n; ++j) {
if (j<mn || !g[i][j]) continue;
if (!(mask>>j-1 & 1))// j不在mask中
f[mask|1<<j-1][j] += f[mask][i];
else if (j == mn)// j恰为最小编号端点
ans += f[mask][i];
}
}
}
printf("%lld", ans-m>>1);
}
第二种:人人为我,用当前状态里的点去更新当前状态。第一次循环保证至少该状态要有 2 个点,第二次循环保证当前链至少有 3 个点,这样答案就不需要减去 m m m 了。
#include <iostream>
#include <unordered_map>
using namespace std;
const int N = 1<<19 | 7;
using LL = long long;
LL f[N][22];
bool g[22][22];
unordered_map<int, int> ump;
int main() {
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
ump[1<<i-1] = i, f[1<<i-1][i] = 1;
for (int i = 1, u, v; i <= m; ++i) {
scanf("%d%d", &u, &v);
g[u][v] = g[v][u] = true;
}
LL ans = 0;
for (int mask = 3; mask < (1<<n); ++mask) {
int t = (mask & -mask) ^ mask;
if (!t) continue;
for (int bi = t; bi; bi &= bi-1) {
int v = bi & -bi, i = ump[v], j;
for (int bj = mask^v; bj; bj &= bj-1) {
t = bj & -bj, j = ump[t];
if (!g[i][j]) continue;
f[mask][i] += f[mask^v][j];
}
}
}
for (int mask = 7; mask < (1<<n); ++mask) {
int t = mask & -mask, i = ump[t], ct = 0;
for (int bj = mask; bj; bj &= bj-1) ++ct;
if (ct < 3) continue;
for (int bj = t ^ mask; bj; bj &= bj-1) {
int j = ump[bj&-bj];
if (!g[i][j]) continue;
ans += f[mask][j];
}
}
printf("%lld", ans>>1);
}
利用位操作的状态压缩解决图论问题
文章介绍了如何使用状态压缩和位操作解决图论中的问题,特别是涉及动态规划的优化策略。通过枚举不同的二进制状态,结合图的邻接矩阵,文章展示了两种不同的代码实现,一种是‘我为人人’策略,另一种是‘人人为我’策略,来更新状态并计算环的数量。这两种方法都涉及到避免重复计数的技巧,并且提供了具体的C++代码示例。
3372

被折叠的 条评论
为什么被折叠?



