您的位置:首页 > 其它

DP系列之二进制状态压缩--杭电1074

2013-01-28 21:17 489 查看
状态压缩的意图是用每一位二进制表示一个状态,0表示选中状态,1表示不选状态,如果有N个物体,从中选择若干个物体,那么最终选中的状态可以用一个N位的二进制位来表示

比如

若选择了第1个物体和第3个物体,这种状态为

0...0101 //前面的0的个数为N-3

若选择了第2个物体,第3个物体,第N个物体,这种状态为

1...0110

因此,无论选中什么状态,都可以用一个N位的二进制数来表示,最大值为(1<<N )- 1 (全部选中)

那么,如果要记录每一个状态的信息(结构体),我们就可以用一个数组来表示,比如,我要记住选中若干物体的重量和价格,只要定义一个结构体

struct Node {

  int weight;

  double price;

};

然后,开辟一个(1<<N )大的数组,data
,可以预处理将每一种选择状态对应的信息都记录在数组里面,然后,对于每一次选择某几个物品,只需要把对应物品的编号乘以2的幂数然后相加即可在O(1)时间内获取该选择策略的信息,这个信息在DP中非常有用,典型的例题便是杭电的1074题,原题见这里

题目的大致意思是现在某个家伙有N项作业要做,每项作业有个deadline和costtime,对于其中的每一项作业,如果在deadline之前能够完成,那么这个家伙将平安无事,否则,呵呵,对于每一项作业,完成的时间比deadline晚几天就扣几分,给出一系列的homework的名称和deadline,costtime,要求出扣分最少的方案

开始,我看n比较小,只有1-15这么点大,果断暴力全排列,结果一会功夫搞定之后一submit就LTE了,悲剧,后来简单计算了下时间复杂度,发现是n!,如果n是15的话 15! = 13076,7436,8000 不LTE才怪了,后来果断搜索各种解决方案,然后就了解到状态压缩DP这样一个东西,于是就有了这篇文章

我们把每一个作业按照字典序列编号(从1开始),然后,每选择一个作业,就会更新状态,这里的状态指的就是从第一个作业到现在为止一共耗费了多长时间,扣了多少分,如果用数组来表示的话,我们将要用N维的数组来表示这种状态的改变,并且这种状态的改变是在原来的基础上修改的,因此,我们想到用状态压缩,即用一个n(n为作业数目)位的二进制数来表示每一种状态,那么一共有1<<n种状态,对于每一种状态,我们要记录的是最小扣分的信息,利用动态规划中状态转移的思想,每一种状态(除非是刚开始选的时候)都是由另外一种状态演变而来,所以,如果要记录当前状态的最小扣分信息,我们只要求出当前记录的所有前导状态,分别计算以他们为真正的前导状态的扣分信息,选择扣分最小的作为当前状态的前导即可,这样,逐次递推,直到1<<n-1(由n个1组成),就能计算出所有作业做完的最小扣分数,并且,只要在递推的时候除了记录最小扣分信息,我们再加上当前状态选择哪个物品以及当前状态是由哪个状态演变而来的即可打印除选择物品的顺序

最后源码如下

#include <iostream>
#include <string>
#define N 15
#define INF 1 << 30
#define MAX 1 << N
using namespace std;

struct Work{
string name;
int deadline;
int costtime;
};

struct State {
int costtime; //已经花费的时间
int reduce; //被扣掉的学分
int pre; //上一个状态
int cur;//本次状态的最后一个作业
};
Work work[N+1];
State state[MAX];

int main() {
int tot, n, D, C;
string name;
cin >> tot;
while (tot--) {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> work[i].name >> work[i].deadline >> work[i].costtime;
}
int max_value = (1 << n);
state[0].costtime = 0;
state[0].reduce = 0;
state[0].pre = -1;
state[0].cur = 0;

for (int i = 1; i < max_value; ++i) {
state[i].reduce = INF;//当前状态的最小扣分设置为无穷大

for (int j = n; j >= 1; j--) {//每一个j都表示一个当前状态最后做的作业,由于题目需要按照字典序列输出,因此这里要逆序,即从字母序列最大的开始枚举,比如当我们求到最后一个状态的时候, 从 (......)->j 可以看出j当然越大越好,因为这样越大的可以尽量排在后面,假如先计算出最后一个选择n和最后一个选择n-1的情况下最小扣分数相同,由于"//*****"是严格小于,因此并不会将n-1换掉n,因此保证了字典序列
int tmp = (1 << (j-1));
//如果状态i本次完成第j项作业
if (i&tmp) {
int pre = i - tmp;//这里计算出的pre为i的前导状态
int reduce = state[pre].reduce;//表示在前导状态pre下完成j之后的扣的分数
if (state[pre].costtime + work[j].costtime > work[j].deadline) {
reduce += state[pre].costtime + work[j].costtime - work[j].deadline;
}

if (reduce < state[i].reduce) { //*****
state[i].costtime = state[pre].costtime + work[j].costtime;
state[i].reduce = reduce;
state[i].pre = pre;
state[i].cur = j;
}
}
}
}
printf("%d\n", state[max_value-1].reduce);

int cur = max_value - 1;
int *pos = new int[n+1];
int k = n;
while (state[cur].pre != -1) {
int now = state[cur].cur;
pos[k--] = now;
cur = state[cur].pre;
}
for (int i = 1; i <= n;++i)
cout << work[pos[i]].name << endl;

}
return 0;
}

注:鄙人最近按照杭电ACM分类来刷题,假期的最低限度是刷掉所有的DP类,并且每一道题目写一个解题报告,如果有志同道合的朋友,欢迎加QQ 823797837共同学习交流,也可以加群ACM新手群161986576,老鸟飞过
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: