参照大白书上面的解法,总共三个步骤,前两个步骤都较好理解。P[i]是用位表示的当选中i时,总共有0~n-1总共有多少个数字被覆盖。cover[S]则表示,当子集为S时,0~n-1中能够被覆盖的位数。若cover[S]的每位都为1,则说明子集S能对全集进行覆盖,当然可能子集S的子集就能做到这一点了。
关键的步骤是对状态转移方程的理解。书中的状态转移方程是f(S)=max{f(S0)|S0是S的子集,cover[S0]等于全集}+1,其实对于这个方程我一直感觉怪怪的,并不十分理解它的意思,虽然它最后也能AC。根据我自己的理解的转移方程应当是这样的,f(S)=max{f(S0)+f(S^S0)|S0是S的子集,cover[S0]等于全集}且必须保证cover[S]为全集时,f(S)至少为1,这样做是为了保证S为最小覆盖集时,f(S)的值为1。这样的解法,最后也是能AC的,而且我个人认为思路比书上的更清晰一些。
至于为啥for(int S0=S;S0;S0=(S0-1)&S)能遍历S的所有子集,严密的证明方法还没想到。但直观上来看,可以发现。例如S为10101,则S0依次为10101,10100,10001....我们完全可以将10101中间的0忽略,因此整个for循环就变为了111,110,101,100..这样逐个减一的过程。其实仔细想来也是,S0减一的过程,就是将最低的非0位置0,然后将比该位低的所有位都置1,经过与S相与之后,则之前的低位又变为了开始时候的样子,因此整个过程可以看做是一个递归循环的过程。(说得一点都不清楚....自己模拟一下应该就能理解)
#include <iostream>
#include <cstdio>
#include <algorithm>
#define MAX 16+5
#define MAXN (1<<16)+5
using namespace std;
int N,Case=1;
int cover[MAXN],P[MAX],f[MAXN];
int main()
{
//freopen("data.txt","r",stdin);
while(cin>>N){
if(!N) break;
for(int i=0;i<N;++i){
int m,x;
cin>>m;
P[i]=1<<i;
while(m--){cin>>x;P[i]|=(1<<x);}
}
for(int S=0;S<(1<<N);++S){
cover[S]=0;
for(int i=0;i<N;++i){
if(S&(1<<i)) cover[S]|=P[i];//获得子集S的覆盖
}
}
f[0]=0;
int ALL=(1<<N)-1;//ALL表示全集
for(int S=1;S<(1<<N);++S){
if(cover[S]==ALL){
f[S]=1;
}
else{
f[S]=0;
continue;
}
for(int S0=S;S0;S0=(S0-1)&S)//枚举S的子集
if(cover[S0]==ALL) f[S]=max(f[S],f[S0^S]+f[S0]);
}
cout<<"Case "<<Case++<<": "<<f[ALL]<<endl;
}
return 0;
}