分析:n种家庭作业,全部做完有n!种顺序,但是n!太大了,而且对于作业1,2,3和1,3,2和2,1,3和3,2,1和3,1,2的顺序来说完成它们消耗的天数是一样的,只是顺序不同从而扣分不同,所以可将完成相同的作业的所有状态压缩成一种状态并记录扣的最少分即可 。
状态压缩DP采用二制进的思想,1, 0代表完成否,如:3的二进制为11, 代表完成了一,二科目的状态, 101代表完成了一、三科目的状态。这样,可从0->(1 << N)来获取所有状态,,进行适当的状态转移,对于该題D[s]代表集合s的状态, 要得到D[s], 可从0 - N 分别检查是否在集合s内s & (1 << i) > 0则表示i在集合s上, 如果i在s集合内, 刚D[s]可从D[s-{i}]来获得, s-{i},可以用s - (1<<i)来计算,这样表示在已完成了s-{i}的基础上再完成i后的装态,遍历i,取最优解。
用dp[i]记录完成作业状态为i时的信息,递推如下:
1.状态a能做第i号作业的条件是a中作业i尚未完成,即a&i=0。
2.若有两个状态dp[a],dp[b]都能到达dp[i],那么选择能使到达i扣分小的那一条路径,若分数相同,转入3
3.这两种状态扣的分数相同,那么选择字典序小的,由于作业按字典序输入,故即dp[i].pre = min(a,b);
关键要理解这张图,每一个状态表示完成了哪些任务,如二进制100表示完成了第三项作业,111表示三项作业都完成。状态转移为将能够到达的状态的某个位上的1去掉剩下的二进制序列能够转移到原状态,例如:110可能来自两个状态100 010。状态压缩dp就是将状态用二进制表示,接下来就是枚举,该题要注意的一点,输出的顺序,由于需要按照字母序输出,所以按照逆序遍历的意义在于,假设前面的1已经被添加过了,则先去添加后面的1。
#include<iostream> #include<limits.h> #include<string> using namespace std; struct homework { string name; //家庭作业名字 int dl; //截止时间 int ct; //需要的时间 } hw[16]; struct DP { int pre; //前一个状态 int curtime; //当前时间 int score; //分数 string name; } dp[65536]; //2^15=32768 void put(int end) //递归输出结果 { if(dp[end].pre==0) { cout<<dp[end].name<<endl; return ; } put(dp[end].pre); cout<<dp[end].name<<endl; } int solve(int n) { int i,j,end,tmp; dp[0].score=0; //初始状态0 dp[0].curtime=0; end=1<<n; for(i=1;i<end;i++) { dp[i].score=INT_MAX; //初始为最大值 for(j=n-1;j>=0;j--) if(i&(1<<j)) //若放入第j个作业 { tmp=0; if(dp[i^(1<<j)].curtime+hw[j].ct-hw[j].dl>0) //第j个作业是否会扣分 tmp=dp[i^(1<<j)].curtime+hw[j].ct-hw[j].dl; if(dp[i].score>dp[i^(1<<j)].score+tmp) { dp[i].score=dp[i^(1<<j)].score+tmp; dp[i].pre=i^(1<<j); //i^(1<<j)表示i的前一个状态,即i中出去位j dp[i].curtime=dp[i^(1<<j)].curtime+hw[j].ct; dp[i].name=hw[j].name; } } } return dp[end-1].score; } int main() { int i,T,N; cin>>T; while(T--) { cin>>N; for(i=0;i<N;i++) cin>>hw[i].name>>hw[i].dl>>hw[i].ct; cout<<solve(N)<<endl; //最小被扣分数 put((1<<N)-1); } return 0; }