1、问题
设有 (n) 项任务,加工时间分别表示为正整数(t_{1},t_{2},...,t_{n}).现有2台同样的机器,从 (0) 时刻开始安排对这些任务的加工。规定只要有待加工的任务,任何机器就不得闲置。如果直到时刻 (T) 所有任务都完成了,总加工时间就等于 (T) 。设计一个算法找到使得总加工时间T达到最小的调度方案。
2、解析
-
观察所给问题,发现只要有代加工的任务,任何机器都不得空闲,那么即从 (0) 时刻开始,机器不停在运转,设两台机器的加工时间为 (T_{1},T_{2}),有一下两个约束条件:1、 (T=max(T_{1},T_{2})) , 2、(T_{1}+T_{2}=sum^{n}_{i=1} t_{i})。
-
那么显然问题就可以转化为把 (t_{1},t_{2},...,t_{n})划分成两部分,一部分之和为(T_{1}),另一部分之和为(T_{2}),使得 (max(T_{1},T_{2}))最小。
-
因为 (sum^{n}_{i=1} t_{i}) 是固定的,所以条件 (max(T_{1},T_{2}))最小等同于 (abs(T_{1}-T_{2}))最小。也就是说让 (T_{1},T_{2})都尽可能的接近背包的一半。
-
也就是对于数组 (t) 中的元素,我们有两种选择,第一种就是放到第一台机器上,第二种就是放到第二台机器上,很显然这就相当与选和不选两种选择,也就可以转化为 01背包 问题。
-
最终问题可以这样描述:从 (n) 个数中选择一些数,让这些数之和尽量接近这 (n) 个数的总和 (sum) 的一半,并输出选择方案。
-
暴力方法的时间复杂度高达 (O(2^n)),显然不可行。
-
考虑动态规划去求解,可以直接用 01背包 求解,背包容量为 (sum/2),目标就是尽量填满背包。每个物品的体积就是数的大小。
-
原因:我们选择了一些数,那么另外一些数就可以确定,那么总和小的一部分数必定是小于等于 (sum/2),那么为了目标最优,也就是让这一部分数尽量填满 (sum/2) 的容量。
-
设 (dp[i][j]) 表示当背包容量为 (j) 时,从前 (i) 项任务中可选出的最大体积。转移方程为:
.$dp[i][j]=max(dp[i-1][j],dp[i][j-t[i]]+t[i])(jgeq t[i]) $ -
我们可以对空间进行进一步优化,即使用滚动数组把二维优化到一维。 (dp[j]) 表示对于体积为 (j)的背包,从所有任务中可选出的最大体积。转移方程为:
.$dp[j]=max(dp[j],dp[j-t[i]]+t[i])(jgeq t[i]) $
-
接下来考虑怎么输出方案,我们考虑在 (dp) 过程中遇到答案更新就标记在这个位置进行了更新,即标记 (path[i][j]=1),表示第 (i) 件任务在当背包容量为 (j) 时被选取了。然后就可以对包的容量 (V) 从大到小遍历,从此同时对物品 (i) 也从大到小遍历,对于某一对 (i,j),如果 (path[i][j]=1),此时就说明第 (i) 件任务被选取,然后为了继续向下回溯,此时 (j=j-t[i]),直到 (j=0)。
3、设计
- 01背包部分
int V=sum/2;
for(int i=1;i<=n;i++){
for(int j=V;j>=t[i];j--){
if(dp[j-t[i]]+t[i]>dp[j]){
dp[j]=dp[j-t[i]]+t[i];
path[i][j]=1;
}
}
}
(max(dp[V],sum-dp[V]))即最少总加工时间
- 输出方案部分
for(int i=n,j=V;i>=1&&j>0;i--){
if(path[i][j]){
ans[i]=1;
j-=t[i];
}
}
(ans[i])就是任务的分组
4、分析
考虑01背包的时间复杂度,两层循环嵌套,外层是任务总数 (O(n)),内层就是背包的容量 (O(V)),总体时间复杂度(O(n imes V))。对于方案的输出同理,那么总体时间复杂度就是 (O(n imes V))。
5、源码
https://github.com/HaHe-a/Algorithm-analysis-and-design-code/blob/master/final.cpp