题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1074
题意:按照字母从小到大的顺序给你n们功课的名字,每一门功课有截至的时间,有花费的时间,若做功课的时间超过截至时间1天,就会扣一分。求一个最小扣分,和做功课的顺序。顺序要求字典序最小。
题解:这道题,状压DP来解决。这点好想。因为功课只有最多有15门,所以状态一共只有 1 << 15 种。
我们还要输出路径,所以需要一个 now 来记录当前做了那门功课,pre记录该状态是从哪一个状态转移的。
还要记录当前时间time,和总扣分数 score。
若状态 i 是由 j 转移 过来的。
中间正在写k这个 作业
(dp【j】.time + hw【k】.cost - hw【k】.cost ) + dp【j】.score < dp【i】.score
那么就转移转移。
关键在于字典序最小的处理的问题。不好理解。
首先题目给你的功课的名字都是按照字母从小到大的。而不是字典序从小的顺序。
因为 同一位置 字母越小的字符串 字典序越小。所以给你功课的名字从小到大是方便处理。
在状态转移的过程中从字母最大的开始转移。那么最后的这门功课的顺序就会在最后。
这样就满足了字典序最小了。
看代码
#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
#define MAX 1 << 15
struct DP{
int pre; // 当前状态从那个一个状态转过来的
int score; // 当前状态的最小扣分数
int time; // 当前 状态的时间
int now; // 当前状态写完了那门功课。
}dp[MAX];
struct HW{
int cost,limit; // 功课的花费和 截至时间
char name[111]; // 功课的名字
}hw[33];
int main(){
int z;
scanf("%d",&z);
while(z--){
int n;
scanf("%d",&n);
memset(dp,0,sizeof(dp));
for(int i = 0 ; i < n ; i ++) scanf("%s%d%d",hw[i].name,&hw[i].limit,&hw[i].cost);
int End = 1 << n; // 总状态数
for(int i = 1 ; i < End ; i ++){
dp[i].score = INF;
for(int j = n - 1 ; j >=0 ; j --){ // 从字母大的开始 遍历。
int k = 1 << j;
if(k & i){ // 若当前功课 有做过
int last = i - k; // 没做过 j 功课的状态
int pass = dp[last].time + hw[j].cost - hw[j].limit;
if(pass < 0) pass = 0; // pass 小于0表示当前功课无花费。也就是 在截至时间之前。
if(pass + dp[last].score < dp[i].score){ // 更优解
dp[i].score = pass + dp[last].score;
dp[i].now = j;
dp[i].pre = last;
dp[i].time = dp[last].time + hw[j].cost;
}
}
}
}
stack <int> s;
int tmp = End - 1;
printf("%d\n",dp[tmp].score);
while(tmp){ // 压到栈里输出
s.push(dp[tmp].now);
tmp = dp[tmp].pre;
}
while(!s.empty()){
printf("%s\n",hw[s.top()].name);
s.pop();
}
}
return 0;
}