http://acm.hdu.edu.cn/showproblem.php?pid=1074
题意:
有n个任务,每个任务有一个截止时间,超过截止时间一天,要扣一个分。
求如何安排任务,使得扣的分数最少。
Input
有多组测试数据。第一行一个整数表示测试数据的组数
第一行一个整数n(1<=n<=15)
接下来n行,每行一个字符串(长度不超过100)表示任务的名称和两个整数,分别表示任务的截止时间和完成任务需要的天数。 这n个任务是按照字符串的字典序从小到大给出。
Output
每组测试数据,输出最少扣的分数的。 并输出一个完成任务的方案,如果有多个方案,输出字典序最小的一个。
思路:
n位二进制表示状态(e.g. n=4):
0000表示一个作业都没写的状态
0001表示写了第一个作业
1111表示所有的作业都写完的状态
某个状态1101 可能由1100 // 1001 // 0101三个状态得到, 那么1101状态的扣分数就由
1100扣分数+1100结束的时间+第1个作业的时长-第1个作业的截止时间
1001扣分数+1001结束的时间+第3个作业的时长-第3个作业的截止时间
0101扣分数+0101结束的时间+第4个作业的时长-第4个作业的截止时间
来更新.
递推式:
dp[i]=min(dp[i],dp[pst]+t[pst]+len[j]-jz[j])
i表示当前状态, pst表示前一个状态, j表示从pst到i新增的作业
注意:
假设当前状态是1101, 那么它的前个状态可能是1100 // 1001 // 0101, 而如果这三个状态得到的dp[1101]都是一样的, 这三个状态最后写的作业分别是作业1, 作业3, 作业4, 显然最后写作业4是字典序最小的, 因此选择由0101转移到的情况.所以, 在实际更新过程中, 应该让字典序大的先来枚举,也就是按照 " 作业4 → 作业3 → 作业1 "的顺序进行更新.
代码:
#include<bits/stdc++.h>
#define fuck(x) std::cout<<"["<<#x<<"->"<<x<<"]"<<endl;
using namespace std;
typedef long long ll;
const int M=16;
const int inf=1e9+5;
int n;
int dp[1<<M];
int bf[1<<M];
int t[1<<M];
string nam[M];
int jz[M];
int len[M];
int main() {
int _;
cin>>_;
while(_--) {
memset(dp,0,sizeof(dp));
cin>>n;
for(int i=0; i<n; i++) {
cin>>nam[i]>>jz[i]>>len[i];
}
fill(dp,dp+(1<<M),inf);
memset(bf,0,sizeof(bf));
memset(t,0,sizeof(t));
int m=1<<n;
dp[0]=0;
for(int i=1; i<m; i++) {//枚举每一种状态
for(int j=n-1; j>=0; j--) {//用每一个作业去更新
int tem=1<<j;//tem是仅有1位是1的二进制数
if(i&tem) {//如果i状态已经做过tem代表的(j)作业
int pst=i-tem;//pst状态后最后新增作业为j作业的状态是i状态.
int change=t[pst]+len[j]-jz[j];//新扣的分数
if(change<0)
change=0;
if(dp[pst]+change<dp[i]) {
dp[i]=dp[pst]+change;
bf[i]=pst;//bf[i]储存i状态的前1状态
t[i]=t[pst]+len[j];//更新现在的时间
}
}
}
}
printf("%d\n",dp[m-1]);
//以下输出状态变化路径
stack<int>s;
int a=m-1,b;//m-1是全为1(即最后的)状态
while(a!=0){
b=bf[a];//b是a前一个状态
int x=a-b;//x仅有1位为1,代表a→b新增的作业
int pos=0;
for(;;pos++){
if(x==1)break;
x>>=1;
}//取出pos是这个作业的编号
s.push(pos);
a=b;
}
while(!s.empty()){
cout<<nam[s.top()]<<endl;
s.pop();
}
}
return 0;
}