http://acm.hdu.edu.cn/showproblem.php?pid=1074
题意:给出n(n <= 15)个科目的作业,每个作业分别有截止日期、完成天数,如果超出了截止日期还没完成作业,则会被扣掉 (完成日期 - 截止日期) 的学分,问怎样安排各科作业的先后顺序,使得被扣除的学分最少,如果有多种安排方案,按字典序输出。(数据保证输入时,科目名是按照字典序的)
由数据规模可以知道可以用状态压缩DP来解决。一般的DP题目都可以用搜索和递推两种方法解决,这道题来说,递推思路会比较直观,而递推的时间效率高很多!
用搜索来解决,主要要注意两个地方,一是状态的转移,二是剪枝。由状态0开始进行DFS,每次添加一个当前还没有安排的科目,然后递归进入下一层,当层数等于科目数的时候,即表示所有科目的顺序已经安排完毕!如当n = 3时,当前递归的状态的压缩表示是010,可以递归到的下一层是010 + 1(新安排第一个科目) 或者010 + 100(新安排第三个科目),如此类推。递归的时候,要注意记录当前新加入的科目,这样就完成了记录路径的工作,相当方便,还要注意当前日期的变换、扣除学分的累加等等。最后要注意的就是当前状态被扣除的学分大于等于以前搜索过的情况时,剪枝!
用递推的方法,要注意三个地方,一是状态的转移(这里比上一种方法复杂),二是记录路径(记录路径和最终回溯都比上一种方法麻烦多了),三是按字典序输出答案。注意到,每一个状态都是由一种or多种(前一个状态 + 新安排的科目)转移得到的,如当前状态的压缩状态表示为111,则有可能是由110 + 1(新加入第一个科目),或者101 + 10(新加入第二个科目),或者001 + 100(新安排第三个科目)转移得到,所以只需取上述情况中被扣学分最少的状态来构造当前状态就可以了(当然还要注意当被扣学分相同时,按照字典序来安排科目,但这个只需要一个小技巧即可解决)。知道状态转移的方法后,依次将1 ~ (1<<n)-1的状态当成当前状态遍历,
首先枚举一下当前状态可以由哪些前驱状态得到,然后取所有前驱状态的最小值即可,因为是按顺序计算状态的,所以前驱状态必定是在之前已经计算好了,直接提取前驱状态即可,具体参照代码中past -> now的过程
代码:
//搜索 296MS
#include<iostream>
#include<cstring>
#include<cstdio>
#include<string>
#include<algorithm>
#define inf 1e9
using namespace std;
struct Data {
string name;
int deadline, days;
int vis, mark;
}sub[20];
int minus_score;
int DP[40000]; //记录状态的数组
bool cmp1(Data a, Data b) { return a.name < b.name; } // 先按字典序排序
bool cmp2(Data a, Data b) { return a.mark < b.mark; } // 输出时按课程优先级排序
void DFS(int n, int depth, int score, int date, int now) {
// n是科目数,depth是递归深度,score是当前减少的学分,
// date是当前日期,now是当前状态的压缩表示
int i;
if(n == depth) {
if(score < minus_score) {
minus_score = score;
for(i = 0; i < n; ++i) sub[i].mark = sub[i].vis;
}
return;
}
if(DP[now] > score) DP[now] = score; // *注意这个剪枝
else return;
for(i = 0; i < n; ++i) {
if(sub[i].vis) continue;
sub[i].vis = depth + 1;
int temp = 0;
if(sub[i].days + date > sub[i].deadline)
//完成时间超出了截止时间,累计扣除的学分
temp = sub[i].days + date - sub[i].deadline;
DFS(n, depth + 1, score + temp, date + sub[i].days, now + (1<<i));
// 深度+1,累计扣除的学分,累计日期,状态转移
sub[i].vis = 0;
}
}
int main() {
int T;
cin>>T;
while(T--){
int num;
cin>>num;
int i;
for(i = 0; i < num; ++i) {
cin>>sub[i].name>>sub[i].deadline>>sub[i].days;
sub[i].vis = 0;
}
for(i = 0; i < 40000; ++i) DP[i] = inf;
minus_score = inf;
//sort(sub, sub + num, cmp1);
// 如果题目没保证按字典序输入,则需要多一次排序
DFS(num, 0, 0, 0, 0);
sort(sub, sub + num, cmp2);
cout<<minus_score<<endl;
for(i = 0; i < num; ++i)
cout<<sub[i].name<<endl;
}
return 0;
}
//递推 15MS
#include<iostream>
#include<cstring>
#include<cstdio>
#include<string>
#include<stack>
#define inf 1e9
using namespace std;
struct Data1 {
string name;
int deadline, days;
}sub[20];
struct Data2 {
int date, score, pos, last;
//当前状态下的日期、扣除的学分,转移时新增的科目、转移前的状态
}DP[40000];
int main() {
int T;
cin>>T;
while(T--) {
int n, i, j;
cin>>n;
for(i = 0; i < n; ++i)
cin>>sub[i].name>>sub[i].deadline>>sub[i].days;
DP[0].date = 0, DP[0].score = 0;
for(i = 1; i < (1<<n); ++i) {
// 转移后的状态
DP[i].score = inf;
for(j = n-1; j >= 0; --j) {
//这里逆序遍历状态转移时添加的科目,可以保证相同score时,按字典序输出
if( i & (1<<j) ) { //符合条件,状态则由past转移到now
int now = i;
int past = i - (1<<j);
int temp = 0;
if(DP[past].date + sub[j].days > sub[j].deadline)
temp = DP[past].date + sub[j].days - sub[j].deadline;
if(DP[past].score + temp < DP[now].score) {
DP[now].score = temp + DP[past].score;
DP[now].date = DP[past].date + sub[j].days;
DP[now].pos = j; // 记录转移的“路径”
DP[now].last = past;
}
//cout<<past<<": "<<DP[past].date<<" "<<DP[past].score<<endl;
//cout<<i<<": "<<DP[i].date<<" "<<DP[i].score<<endl;
}
}
}
int Max = (1<<n) - 1;
cout<<DP[Max].score<<endl;
stack<int> sta;
int p = Max;
while(p != 0) {
// 回溯路径
sta.push(DP[p].pos);
p = DP[p].last;
}
while(!sta.empty()) {
int top = sta.top();
sta.pop();
cout<<sub[top].name<<endl;
}
}
return 0;
}