可以用二进制表示子集,这种表示方法真的非常省时间空间,其中从右往左第i位(从0开始编号)表示元素i是否在集合中(1表示“在”,0表示“不在”)
e.g.
二进制的1111换算成十进制就是15,如果用15代表全集A的话,那么1101b(b是二进制)即13d(十进制)就代表了A的一个具有第1、第3、第5个元素的子集B。
本题的做法有很多。一种相对容易实现的方法是:用两个集合s1表示恰好有一个人教的科目集合,s2表示至少有两个人教的科目集合,而d(i,s1,s2)表示已经考虑了前i个人时的最小花费。注意,把所有人一起从0编号,则编号0~m-1是在职教师,m~n+m-1是应聘者。状态转移方程为d(i,s1,s2) = min{d(i+1, s1’, s2’)+c[i], d(i+1, s1, s2)},其中第一项表示“聘用”,第二项表示“不聘用”。当i≥m时状态转移方程才出现第二项。这里s1’和s2’分别表示“招聘第i个人之后s1和s2的新值”,具体计算方法见代码。
下面代码中的st[i]表示第i个人能教的科目集合(注意输入中科目从1开始编号,而代码的其他部分中科目从0开始编号,因此输入时要转换一下)。下面的代码用到了一个技巧:记忆化搜索中有一个参数s0,表示没有任何人能教的科目集合。这个参数并不需要记忆(因为有了s1和s2就能算出s0),仅是为了编程的方便(详见s1’和s2’的计算方式)。
#include<iostream>
#include<string.h>
using namespace std;
#include<sstream>
#define maxs 10
#define maxn 130+10
#define maxm 30
#define INF 1000000000
int s,m,n,c[maxn],st[maxn],d[maxn][1<<maxs][1<<maxs];
int dp(int i,int s0,int s1,int s2)
{
if(i==m+n+1)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);//不选
//接下来是选这个人的情况,要更改三个集合的数值
//m0 是没有人教的课中他可以教的课 m1是恰好有一个人教的课中他可以教的课
int m0=s0&st[i];int m1=s1&st[i];
s0^=m0;//集合A和AB的交集的异或等于A去掉交集
s1=(s1^m1)|m0;
s2|=m1; //s2是至少有两个人教的课的集合
ans=min(ans,c[i]+dp(i+1,s0,s1,s2));
return ans;
}
int main()
{
while (~scanf("%d%d%d", &s, &m, &n) && s&&m&&n)
{
memset(d, -1, sizeof(d));
memset(st, 0, sizeof(st));
getchar();
for (int i = 1; i <= m + n; i++)
{
string str;
getline(cin, str);
stringstream ss(str);
int x, flag = 1;
while (ss >> x)
{
if (flag){ flag = 0; c[i] = x; }
else
{
x--; //将科目从0开始编号
st[i] |= (1 << x); //二进制的压缩存储
}
}
}
int ans = dp(1, (1 << s) - 1, 0, 0);
printf("%d\n", ans);
}
return 0;
}