经典的状态压缩DP 。
根据DP的阶段定义,我们需要枚举每一个教师进行递推,但是由于每个教师可以教授的课程是复杂多样的,所以使得状态变得难以转移 。那么要怎么样表示状态呢? 显然增加一两个维度是无法胜任的,所以我们可以用二进制枚举子集的方法,用一个整数通过位运算充当一个集合 。 C++提供的位运算符极像对集合的操作,我们恰好可以利用这一点 。
用d[i][s1][s2]表示考虑了前i个人时的最小花费 。 其中s1表示恰有一个人教的科目集合,s2表示至少有两个人教的科目集合 。因为由这两个量就可以推出一个人也没有教的科目集合,所以用这两个量已经可以表示所有状态了。 然后就是各种精彩的位运算操作 ,代码中最难的部分大概就是这两行位运算 :
int m0 = st[i] & s0, m1 = st[i] & s1; s0 ^= m0; s1 = (s1 ^ m1) | m0; s2 |= m1;既然要选择当前这名教师,那么他所教的课程就要使得集合改变 。 m0就是指他能教而且当前没有人教的课 ,m1指他能教且当前只有一个人教的课 。 ^运算符使得相同的为0,不同的为1 ,所以使得那些他能教的课在集合s0中变成了0 ,也就是说这些课升级为恰有一人教的课。 那么显然还要更新s1,(s1 ^ m1)和前面说的作用一样,使得这些课升级为至少有两个人教的,但是别忘了还有一些课升级为恰有一人教的,所以用|(或)操作,将这些课加进s1 中, s2当然也要加进s1中升级的元素 。
位运算不但节省空间,而且节省时间, 所以多花一些时间练习用位运算写代码还是很值得的 。
#include<bits/stdc++.h> using namespace std; const int INF = 100000000; int s,n,m,a,c[125],st[125],d[125][1<<8][1<<8]; int dp(int i,int s0,int s1,int s2) { if(i == m+n) return s2 == (1<<s) - 1 ? 0 : INF;//如果仍未达到要求,无解 int& ans = d[i][s1][s2]; if(ans >= 0) return ans; ans = INF; if(i >= m) ans = dp(i+1,s0,s1,s2);//不选该教师 int m0 = st[i] & s0, m1 = st[i] & s1; //对集合的位运算操作,极为经典 。要熟练掌握 s0 ^= m0; s1 = (s1 ^ m1) | m0; s2 |= m1; ans = min(ans,c[i] + dp(i+1,s0,s1,s2));//选 return ans; } int main() { while(~scanf("%d%d%d",&s,&m,&n)&&s) { memset(d,-1,sizeof(d)); for(int i=0;i<m+n;i++) { scanf("%d",&c[i]); st[i] = 0; while(true) { scanf("%d",&a); a--; st[i] = st[i] | (1 << a);//将该元素加入集合中 char c = getchar(); if(c == 10) break;//换行的ascii码 } } int ans = dp(0,(1<<s)-1,0,0) ; printf("%d\n",ans); } return 0; }