集合划分(状压dp)

该博客介绍了一个使用C++编写的程序,通过位运算和动态规划来解决集合划分问题,判断组合是否能形成质数。程序采用了素数筛法优化,并在每个集合状态中计算组合总数,最后输出可行划分的总数。

传送门

#include<bits/stdc++.h>
using namespace std;
const int maxn=(1<<16)+5;
int n,a[20],dp[maxn],sum[maxn]={0};//dp[s]表示出现s集合,可行划分的总数 
int c[maxn],ispri[1700]={1,1};
int main()
{
	int t,s;
	scanf("%d",&t);
	for(int i=0;i<16;i++) c[1<<i]=i+1;
	for(int i=2;i<=850;i++)//s筛法 
		for(int j=i<<1;j<1700;j+=i)//<<1为两倍,三倍则+i(+比*快) 
			ispri[j]=1;
	while(t--){
		scanf("%d",&n);
		for(int i=1;i<=n;i++) scanf("%d",&a[i]);
		s=(1<<n)-1;
		for(int i=1;i<=s;i++){//枚举集合 
			int li=-i&i,hi=i^li;
			if(li==i) sum[i]=a[c[i]];//单个数字的组合 
			else sum[i]=sum[hi]+sum[li];//多个数字的组合 
			dp[i]=ispri[sum[i]]==0;//集合不拆分是否质数
			//枚举组合状态 
			for(int j=hi;j>0;j=(j-1)&hi) dp[i]+=dp[j]*(ispri[sum[j^i]]==0);
		}
		printf("%d\n",dp[s]);
	} 
	return 0;
}
动态规划(State Compression Dynamic Programming,简称DP)中,子集枚举是一种常见的技巧,尤其在处理集合态时非常有效。它通常用于将态空间缩为一个整数表示,从而实现高效的动态规划计算。 ### 子集枚举的基本原理 在DP中,态通常使用一个整数来表示集合。例如,对于一个包含 `n` 个元素的集合,可以使用一个长度为 `n` 的二进制整数来表示集合态。每一位的 `1` 或 `0` 表示该元素是否属于当前集合。例如,对于 `n = 4`,整数 `0b1011`(即十进制 `11`)表示集合包含第 `0`、`1` 和 `3` 号元素。 子集枚举的核心思想是:对于一个给定的集合态 `s`,枚举其所有子集 `s0`,并基于这些子集进行动态规划转移。例如,动态规划态转移可能涉及 `f[s] = max(f[s], f[s ^ s0] + cost)`,其中 `s0` 是 `s` 的一个子集。 ### 子集枚举的实现方法 子集枚举可以通过以下方式高效实现: #### 1. 枚举所有子集 对于一个集合 `s`,其所有子集可以通过以下循环进行枚举: ```cpp for (int s0 = s; s0; s0 = (s0 - 1) & s) ``` 该循环通过从 `s` 开始,逐步减少子集的大小,直到子集为空。其核心在于 `(s0 - 1) & s` 操作,它能够确保每次迭代生成的是 `s` 的一个子集,并且不会重复。 #### 2. 态转移 在枚举子集的过程中,可以对每个子集 `s0` 进行态转移。例如,在 UVA-11825 中,动态规划态转移公式为: ```cpp if (b[s0] == all) f[s] = max(f[s], f[s ^ s0] + 1); ``` 其中,`b[s0]` 是子集 `s0` 的某种属性,`all` 是目标态,`s ^ s0` 表示集合 `s` 去掉子集 `s0` 后的态。该公式表示:如果子集 `s0` 满足某种条件(如覆盖所有目标节点),则可以基于 `s ^ s0` 的态更新 `s` 的态。 #### 3. 预处理 在某些问题中,需要对态进行预处理以计算某些属性。例如,在引用 [4] 中,通过以下方式预处理每个集合的某些值: ```cpp F(i, 0, U) F(ii, 0, n-1) F(jj, ii+1, n-1) if ((i & (1 << ii)) && (i & (1 << jj))) f[i] += a[ii][jj], g[i] += b[ii][jj]; ``` 这一步骤可以提前计算出每个集合的某些属性,从而在态转移时直接使用这些值。 ### 子集枚举的应用场景 1. **覆盖问题**:如 UVA-11825,需要覆盖所有节点。 2. **集合划分问题**:如零件组装问题,需要将集合划分为多个子集。 3. **图论问题**:如旅行商问题(TSP),需要记录已经访问过的城市集合。 ### 示例代码 以下是一个典型的子集枚举实现,用于解决覆盖问题: ```cpp #include <bits/stdc++.h> using namespace std; int main() { int n; cin >> n; int all = (1 << n) - 1; // 全集 int f[1 << n]; // 动态规划数组 memset(f, 0, sizeof(f)); // 枚举所有态 for (int s = 1; s < (1 << n); s++) { for (int s0 = s; s0; s0 = (s0 - 1) & s) { // 如果子集 s0 满足条件,则进行态转移 if ((s0 & 0b101) == 0b101) { // 示例条件 f[s] = max(f[s], f[s ^ s0] + 1); } } } cout << f[all] << endl; return 0; } ``` ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值