POJ 2441 状压DP 由滚动数组到枚举子集

大致题意

农场主安排 N头公牛打篮球,每头公牛只会在特定的几个喜欢的篮球场独自打球,篮球场数为 M。求每一头公牛都安排到的方案数。1 <= N <= 20, 1 <= M <= 20

M < N 时没有可行方案。爆搜复杂度O(nm2nm),TLE。状态压缩n或者m其中一维,复杂度降为O(nm2n)或O(nm2m)。

第一版:滚动数组

M >= N ,压缩 n 复杂度更低。为了不 MLE,滚动数组求解。顺序搜索,dp即搜索到篮球场 i时牛的状态 s 对应的方案数。因为每次遍历0 ~ (1 << N) 的状态,除了转移状态的方案数,还要保留原状态的方案数,保证各方案都遍历到。

#include <cstdio>
#include <STDLIB.H>
#include <cmath>
#include <algorithm>
#include <string>
#include <iostream>
#define min(a,b)    (((a) < (b)) ? (a) : (b))
#define max(a,b)    (((a) > (b)) ? (a) : (b))
#define abs(x)    ((x) < 0 ? -(x) : (x))
#define INF 0x3f3f3f3f
#define eps 1e-4
#define M_PI 3.14159265358979323846
#define MAX_M 20
#define MAX_N 20
using namespace std;
int N, M;
int L[MAX_N][MAX_M]; //L[i][j], 牛i是否喜欢篮球场j
int dp[2][1 << MAX_N];

void init(){
	memset(L, false, sizeof(L));
	for(int c = 0; c < N; c++){
		int p, b;
		scanf("%d", &p);
		while(p--){
			scanf("%d", &b);
			L[c][b - 1] = true;
		}
	}
}
void solve(){
	if(N > M){
		printf("0\n");
		return;
	}
	memset(dp, 0, sizeof(dp));
	int *crt = dp[0], *next = dp[1];
	crt[(1 << N) - 1] = 1;
	for(int i = 0; i < M; i++){
		//保留原状态方案数
		memcpy(next, crt, sizeof(int) * (1 << N));
		for(int j = 0; j < N; j++){
			if(!L[j][i]) continue;
			for(int s = 0; s < 1 << N; s++){
				if(!(s >> j & 1)){
					next[s] += crt[s | 1 << j];
				}
			}
		}
		swap(crt, next);
	}
	printf("%d\n", crt[0]);
}
int main(){
	while(~scanf("%d%d", &N, &M)){
		init();
		solve();
	}
	return 0;
}
第二版 枚举子集

搜索篮球场时,因为 M >= N ,不能确定搜索到某个篮球场时安排公牛的数量,只能遍历一遍状态,比较浪费。反过来,顺序搜索公牛时,根据篮球场的状态可以确定牛的数量。

通过枚举子集,按子集大小顺序递推,可以只使用一维数组,而且减小了无用状态的枚举。因为子集大小为 0 时无法返回,要特别处理一下。最后枚举大小为 N 的子集即可求出答案。

当前搜索到第 i 头牛,枚举大小为 1 << (i - 1) 的篮球场的子集(即前 i - 1 头牛的安排方案),之后转移状态即可。

枚举子集方法

《挑战程序设计竞赛》提供了一种简单地按照字典序升序枚举所有大小为 k的子集的方法。

按照字典序的话,最小的子集是(1 << k) - 1,所以用它作为初始值。例如0101110之后的是0110011,0111110之后的是1001111。下面是求出comb下一个二进制码的方法。
(1)求出最低位的1开始的连续的区间(0101110 -> 0001110)
(2)将这一区间全部变为0,并将区间左侧的那个 0 变为 1 (0101110 -> 0110000)
(3)将(1)步里取出的区间右移,只道剩下的 1 的个数减少了 1 个(0001110 -> 0000011)
(4)将第(2)步和第(3)步的结果按位取或(0110000|0000011 = 0110011)

int comb = (1 << k) - 1;
//因为是从 n 个元素的集合中进行选择, comb 值不能大于 1 << n
while(comb < 1 << n) {
	//这里进行针对组合的处理
	
	//对于非零整数,x & (-x)的值就是将最低位的 1 独立出来后的值
	//这是由于计算机中负数采用补码表示,-x 实际上对应于(~x)+1(将x按位取反后加1)
	int x = comb & -comb;
	//将 comb 从最低位的 1 开始的连续 1 都置 0 了
	//连续 1 区间左侧的 0 变为 1,y恰好是(2)步要求的值
	int y = comb + x;
	//比较一下 ~y 和 comb,在 comb 中加上 x 后没有变化的位,在~y中全都取反
	//最低位 1 开始的连续区间在 ~y 中依然是 1
	//区间左侧的那个 0 在 ~y 中也依然是 0
	// z 即是(1)步要求的值
	int z = comb & ~y;
	//将 z 不断右移,直到最低位为 1 ,通过 z / x 即可完成
	//将 z / x 右移 1 位就得到第(3)要求的值
	//按位取或就求得了 comb 之后的下一个二进制
	comb = (z / x >> 1) | y;
}
题解代码
#include <cstdio>
#include <STDLIB.H>
#include <cmath>
#include <algorithm>
#include <iostream>
#define min(a,b)    (((a) < (b)) ? (a) : (b))
#define max(a,b)    (((a) > (b)) ? (a) : (b))
#define abs(x)    ((x) < 0 ? -(x) : (x))
#define INF 0x3f3f3f3f
#define eps 1e-4
#define M_PI 3.14159265358979323846
#define MAX_M 20
#define MAX_N 20
using namespace std;
int N, M;
int L[MAX_N][MAX_M];
int dp[1 << MAX_M];

void init(){
	memset(L, false, sizeof(L));
	for(int c = 0; c < N; c++){
		int p, b;
		scanf("%d", &p);
		while(p--){
			scanf("%d", &b);
			L[c][b - 1] = true;
		}
	}
}
//按字典序枚举大小为i的子集
int next_sub(int comb){
	int x = comb & -comb, y = comb + x;
	return ((comb & ~y) / x >> 1) | y;
}
void solve(){
	if(N > M){
		printf("0\n");
		return;
	}
	memset(dp, 0, sizeof(dp));
	for(int i = 0; i < M; i++){
		if(L[0][i]) dp[1 << i] = 1;
	}
	for(int i = 1; i < N; i++){
		for(int comb = (1 << i) - 1; comb < 1 << M; comb = next_sub(comb)){
			if(dp[comb]){
				for(int j = 0; j < M; j++){
					if(L[i][j] && !(comb >> j & 1)) dp[comb | 1 << j] += dp[comb];
				}
			}
		}
	}
	int res = 0;
	for(int comb = (1 << N) - 1; comb < 1 << M; comb = next_sub(comb)) res += dp[comb];
	printf("%d\n", res);
}
int main(){
	while(~scanf("%d%d", &N, &M)){
		init();
		solve();
	}
	return 0;
}
发布了6 篇原创文章 · 获赞 0 · 访问量 46

猜你喜欢

转载自blog.csdn.net/neweryyy/article/details/104365666