CF11D-A Simple Task(状态压缩)

状态压缩的位操作

  1. 取最小的 1:i & -i
  2. 去掉最小的 1:i &= i-1
  3. 判断第 j j j 位是否为 1:i>>j & 1

对于普通的状态压缩,由于位数都不多,我们可以直接用循环对每个位枚举即可。

f[s][i] 表示当前二进制状态为 s s s,最后一个加入的点为 i i i 的最优解。

由于二进制从第 0 0 0 为开始计算,所以通常下标要注意减 1 1 1

枚举的顺序

  1. 初始只有 1 个点,依次增加,使用 for (int i = 1; i < (1<<n); ++i) 枚举所有的状态;
  2. 初始所有点都在,每次去掉一个点,倒着枚举,使用 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. 如何构建环
    每次连接时,枚举当前状态的每一个点,与状态中最小编号端点直接连接,均构成一个环,进行计数即可。
    这里还要注意两个问题:
    (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);
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值