参照大白书上面的解法,总共三个步骤,前两个步骤都较好理解。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; }
时间: 2024-09-29 05:01:54