UVA 10187 校长的烦恼

可以用二进制表示子集,这种表示方法真的非常省时间空间,其中从右往左第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;

}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值