大致题意
农场主安排 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)步里取出的区间右移,z直到剩下的 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;
}