HDU - 1074(Doing Homework)(状压dp)

题目大意:

现在有n门课程的作业需要你来做,但是每个作业都有完成的最后期限,超过期限一天会扣一分,现在给出科目的名称,最后期限,完成该门课程所需要的时间,求出最少的扣分,并给出作业的完成顺序。

状压dp:

状压的意思是对状态进行压缩,用某个数的二进制表示当前的状态,比如我现在有3门课程的作业需要完成,假设现在是第t天,假设三门课程分别是A、B、C,0表示现在这门课的作业没有完成,1表示现在这门课的作业已经完成;

1.如何压缩状态:

那么二进制000(十进制就是0)表示就是ABC三门课都没有完成,010(十进制就是2)表示AC没完成但是B完成了,111(十进制就是7)表示ABC三门课都已经完成了,因此我们可以知道当有三门课的时候我们使用[0, 2^3 - 1]这个闭区间内的所有的数就可以表示现在的状态,其实是这些数的二进制对应着状态,也就是二进制枚举,那么以此类推一如果现在有n门课程需要我来完成,到某一天为止我就可以用[0, 2^n - 1],这个闭区间内的所有的数来表示当前n门课程的完成情况。

2.如何判断当前是什么状态:

也就是判断当前状态完成了多少门课程的作业,如果我们用now表示当前状态,其实我们只需要知道现在状态当中那几位是1就好办了,假设课程从0开始标号,那么第i门作业在当前状态中是否完成我们可以将1<<i之后now&(1<<i),如果结果是0那么表示第i门课程在当前装态是没有完成的,反之如果大于零就是完成的因为肯能是001,010,100这样的结果。

3.如何找到前一个状态:

对于动态规划我们需要找到当前状态的前一个状态,如果满足我们需要的条件才更新当前状态的dp值,那么我们怎么找到当前状态的前一个状态呢?假设现在是某一天我完成了B作业之后我就完成了全部的作业,也就是现在的状态是111但是上一个状态是101,再假设我完成了A作业之后就完成了AB作业,现在的状态是110,之前的状态是010。如果我们用now表示当前状态,用pre表示前一个状态,用表示做完第i件事情之后(i从0开始),pre变成了now,那么pre = now & (~(1<<i)),其实很好理解也就是将now二进制表示i的那一位变成0就到知道了上一个状态,同时pre = now - (1 << i),这两个是等价的。

问题分析:

现在我们用动态规划的思想去看这个问题,我们当前的状态是now,到这个状态需要t[now]天(不一定是到now的最少天数,但是一定是到now状态扣分最少时的所用天数),也就是我们今天的状态now是由改变任意一个now当中的二进制1得到的,也就是now的前一种状态是多样的。

对于状态now,我们遍历每一门课程,如果now可以由第j门课程完成那么我们可以计算出当前的扣分(sc = t[now] + cost - dead)(cost表示完成该门作业的天数,dead表示最后的期限),显然这个值可能小于0,也就是提前完成了,那么如果dp[pre] + sc < dp[now],我们就可以对当前的状态的最小扣分、到达当前状态的天数进行更新,并且保存到达当前状态需要完成的课程是哪一门。

状态转移方程就是dp[i] = min(dp[i], dp[i - (1 << j)]),那么最后的答案就是dp[2^n - 1];

在这里需要特殊说明一下,因为如果有多个解,应该是按照题目给出的先后顺序输出,也就是说越往后的状态应该是由越往后给出的课程完成后得出的,因此应该倒序遍历课程。

代码:

#include <bits/stdc++.h>
using namespace std;

const int INF = 0x3f3f3f3f;
const int maxn = 20;
struct subject
{
    string name;
    int dead, cost;
}p[maxn];
int n, dp[1 << maxn];
int tt[1 << maxn], preans[1 << maxn];

void print(int x)
{
    if(x <= 0)
        return ;
    print(x - (1 << preans[x]));
    cout << p[preans[x]].name << endl;
}

int main()
{
    ios::sync_with_stdio(false);
    int t;
    cin >> t;
    while(t--)
    {
        memset(preans, -1, sizeof(preans));
        cin >> n;
        for(int i = 0; i < n; i++)
            cin >> p[i].name >> p[i].dead >> p[i].cost;

        int all = 1 << n;
        for(int now = 1; now < all; now++)
        {
            dp[now] = INF;
            for(int j = n - 1; j >= 0; j--)
            {
                int temp = 1 << j;
                if((temp & now) == 0)
                    continue;
                int pre = now - temp;
                int sc = tt[pre] + p[j].cost - p[j].dead;
                sc = sc < 0 ? 0 : sc;
                if(sc + dp[pre] < dp[now])
                {
                    dp[now] = sc + dp[pre];
                    tt[now] = tt[pre] + p[j].cost;
                    preans[now] = j;
                }
            }
        }

        cout << dp[all - 1] << endl;
        print(all - 1);
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值