HDU OJ 1074: Doing Homework

HDU OJ 1074: Doing Homework(状压DP)

序言:
关于DP的题目已经做了不少了,但是对于一些特殊的DP还是掌握不佳,今天啃了一道关于状态压缩的DP题,感觉挺有意思。

Problem Description:

Ignatius has just come back school from the 30th ACM/ICPC. Now he has
a lot of homework to do. Every teacher gives him a deadline of handing
in the homework. If Ignatius hands in the homework after the deadline,
the teacher will reduce his score of the final test, 1 day for 1
point. And as you know, doing homework always takes a long time. So
Ignatius wants you to help him to arrange the order of doing homework
to minimize the reduced score.

Input:

The input contains several test cases. The first line of the input is
a single integer T which is the number of test cases. T test cases
follow. Each test case start with a positive integer N(1<=N<=15) which
indicate the number of homework. Then N lines follow. Each line
contains a string S(the subject’s name, each string will at most has
100 characters) and two integers D(the deadline of the subject), C(how
many days will it take Ignatius to finish this subject’s homework).

Note: All the subject names are given in the alphabet increasing
order. So you may process the problem much easier.

Output:

For each test case, you should output the smallest total reduced
score, then give out the order of the subjects, one subject in a line.
If there are more than one orders, you should output the alphabet
smallest one.

Sample Input:

2
3
Computer 3 3
English 20 1
Math 3 2
3
Computer 3 3
English 6 3
Math 6 3

Sample Output:

2
Computer
Math
English
3
Computer
English
Math

Hint:
In the second test case, both Computer->English->Math and Computer-Math->English leads to reduce 3 points, but the word “English” appears earlier than the word “Math”, so we choose the first order. That is so-called alphabet order.

题目大意:
就是说有个选手,被分配了几个课程任务。完成每个 Course 都有其固定的时间;每个 Course 还有其截止期限。问如何分配做 Course 的顺序才能使延期最小化?要求输出最小的延期,以及字典序下的做 Course 的顺序。

解题思路:
题目给出 Course 数 1<=N<=15,显然用全排列做是必然超时了。我们马上会想到用DP做,但是如何来表示每个状态是个大问题。“状态压缩”DP就是用来解决这类问题的:
我们假定“0”代表暂时未做,“1”表示已做。那么,对于 Course 数为 N 的问题,状态

0...01(N10)

表示暂时只做了Course 1,再如状态
0..101(N30)

表示暂时只做了Course 1 和 Course 3。特别地
11..11(1)

表示全部 Course 都已经做完。
上述是一种二进制的记录方式,而每个二进制数唯一映射了一个十进制数。因此 [0, (1>>N) - 1] 这个范围内的每一个数便唯一映射了每个状态。不过,每个状态仅仅指明了当前已经做完的 Course 以及暂未做完的 Course,却未指明各个 Course 的完成顺序。因此,在记录每个状态时,我们还要保存其前驱状态,则当前状态和前驱状态的“差”便是当前状态新完成的Course。
另外,我们还可以用 1 >> (j - 1)来表示 Course j。例如,对于课程 2 而言:1 >> (j - 1) = 1 >> (2 - 1) = 2 = 0 … 1 0。可以看到,这个二进制数除了右数第 j 位是1之外,其他皆为0。我们试着将之与状态 i 进行“按位与(&)”,若结果为0,便可以判断状态 i 还未做过Course j,反之则判断已做。
如果状态 i 已经做过 Course j,我们可以假定 Course j 正是其刚刚做的Course(或者说最后做的)。那么,该状态的前驱便是 i - (1 >> (j - 1)),也就是做 Course j 前的状态。

现在思路已渐近清晰,我们先用伪代码试着完全打通思路。

DP 伪代码:

DP()
/* data -> Course表 */
//name -> Course名
//cost -> Course耗时
//dead -> Course限期

/*   dp -> 状态表    */
//totcost -> 某状态的总耗时
//delay -> 某状态的总延期
//prev -> 某状态的前驱
//cour -> 某状态的新选的课

//dp[0] 起始状态
dp[0].totcost = 0  dp[0].delay = 0
for i = 1 to (1>>N) -1  //从底至顶,遍历所有状态
    dp[i].delay = +inf  //至初始延期为无穷
    for j = N downto 1  //遍历所有课程
        curr = 1>>(j-1) //二进制表示
        if i & curr    //如果状态 i 做过课程 j
            prev = i - curr //定位前驱
            delay = dp[prev].delay //暂延时
            if dp[prev].totcost + data[j].cost > data[j].dead  
                delay += dp[prev].totcost + data[j].cost -data[j].dead
            if delay < dp[i].delay //若更优
                //状态更新
                dp[i].delay = delay
                dp[i].prev = prev
                dp[i].totcost = dp[prev].totcost + data[j].cost
                dp[i].cour = j

return dp

注意到,对 j 内循环是倒序遍历的。正序和倒序并不改变最终的总延期,但在某两个 Course 属性完全一致时出现选择顺序的不同。题目给定的输入是字典序的,它要求输出也按字典序。
暂且不管正序遍历还是倒序遍历,先来考虑该思路下记录的 Course 顺序:首先,dp[end].cour 是最后选择的 Course ,其应该最后输出。而 dp[dp[end].prev].cour 是倒数第二个选择的 Course,其应该倒数第二输出。显然,可以用一个递归函数进行输出。

Output 伪代码:

OUTPUT(k)
while k!=0
    OUTPUT(dp[k].prev)
    print data[dp[k].cour].name

最后,证明倒序遍历才是正确的。考虑输入
3
Computer 3 3
English 6 3
Math 6 3
这里 N = 3, i 的遍历区间是[1, 7],第一个 Course 选 Computer 没有争议,问题是第二门课先选 English 还是 Math。
当遍历到 7 = 111时,若内循环是倒序遍历的,我们会先检索到 3 ,即
Math,将之视为是刚刚完成的。之后会检索到2,即 English,但这种情况的delay并不优于 Math 的情况(准确说是相等,因此不优于),故不作数据更新。而若内循环是正序遍历的,则会导致输出不为字典序了。

至此,此题的思路已完全打通,最后附上C++代码。

C++ 代码:

#include<iostream>
#include<string>
#include<time.h>
#define Max 20
//#define test

using namespace std;
namespace
{
    struct course
    {
        string name;
        int dead;
        int cost;
    }data[Max];
    struct pos
    {
        int prev;//前一个状态
        int totcost;//当前消耗
        int delay;//当前延期
        int cour;//当前选的课
    }dp[1<<Max];
    void output(int k)//模拟栈
    {
        if(!k) return;
        output(dp[k].prev);
        cout<<data[dp[k].cour].name<<endl;
    }
}
int main()
{
#ifdef test
    freopen("/Users/oldman/Documents/Xcode/OJ/input.txt", "r", stdin);
    freopen("/Users/oldman/Documents/Xcode/OJ/output.txt", "w", stdout);
#endif
    int T;
    cin>>T;
    while(T--)
    {
        int N;
        while(cin>>N)
        {

            memset(data, 0, sizeof(data));
            for(int i = 1; i <= N; ++i)
                cin>>data[i].name>>data[i].dead>>data[i].cost;
            /*   设定边界  */
            dp[0].prev = -1;
            dp[0].totcost = 0;
            dp[0].delay = 0;
            for(int i = 1; i < (1<<N); ++i)//遍历每种状态
            {
                dp[i].delay = 999999;//初始置无穷
                for(int j = N; j >= 1; j--)//下个选j,注意反向!!
                {
                    int curr = 1 << (j - 1);
                    if(curr & i)//按位与 -> 如果说状态 i 做过 j 这个作业
                    {
                        int prev = i - curr;//我们假定 j 是当前最后一个作业
                        // i - curr 自然便是 i 状态前驱
                        int delay = dp[prev].delay;//前驱的延期
                        if(dp[prev].totcost + data[j].cost > data[j].dead)
                        {
                            delay += (dp[prev].totcost + data[j].cost - data[j].dead);
                        }
                        if(delay < dp[i].delay)
                        {
                            dp[i].prev = prev;
                            dp[i].delay = delay;
                            dp[i].totcost = dp[prev].totcost + data[j].cost;
                            dp[i].cour = j;
                        }
                    }
                }
            }
            cout<<dp[(1<<N) - 1].delay<<endl;
            output((1<<N) - 1);
        }
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/ajaxlt/article/details/78710609
今日推荐