以下为算法设计与分析赵老师上课的课件,用于自己复习和理解。
复习:(递推,分治)–每个阶段有一个状态(问题)且可由上一阶段状态(子问题)简单合并得到
(动态规划)–每个阶段的最优状态都是从上一个阶段的某些状态(子问题)通过决策得到而不管这些状态是如何得到的
(贪心策略)–每个阶段的最优状态都可由上一个阶段的局部最优策略(特定子问题)得到
(NP问题)–每个阶段的最优状态是由之前所有阶段的状态的组合得到的–(可优化的)全局搜索
有限离散问题总可以用全局搜索法求得问题得全部解。
全局搜索的优化求解方法:回溯法和分支界限法
本章内容
5.1回溯法基本概念和求解框架
5.2 两艘船的装载问题
5.3 0-1背包问题
5.4 批处理作业调度
5.1回溯法基本概念和求解框架
1.问题的解空间:问题的所有可能解(不一定可行)或状态,以树(或图)形式组织,通过对树的遍历搜索来找到问题的解/最优解。
2.深度优先搜索:从根节点开始,判断当前节点是否包含问题的解,如果包含则对子树继续按深度优先策略搜索,如果不包含则跳过对其子树的搜索并回溯到其父节点,至根节点所有子树搜索完毕。
3.剪枝优化:利用剪枝函数(约束函数+界限函数)对深度优先搜索过程进行优化。
例 0-1背包问题(0-1 Knapsack Problem,NP完全问题)
有n个物体和一个背包,物体i得重量为wi,价值为pi,背包的容量为c,若将物体i(1<=i<=n)装入背包,则有价值为pi。目标是找到一个方案,使得放入背包得物体总价值最高。
动态规划法算法复杂度:O(nc),设想c=2^n,为指数级复杂的
解空间的(Solution space)定义
*定义:S={(X1,…,Xn)|(X1,X2,…,Xn)为满足问题要求的所有可能解}
*这个空间必须至少包含该问题的一个解,而这个解也对某些问题可能就是一个最优解。
在具有n个商品的0/1背包问题中,解空间的一个最大选择是2^n个长度为n的{0,1}序列集合,这个集合表示所有将0和1指配给Xi的可能方法。
设n=3的时候,共有2^3=8个问题的可能解(解空间):(0,0,0),(0,0,1),(0,1,0),(0,1,1),(1,0,0),(1,0,1),(1,1,0),(1,1,1)
可表示为一颗4层的满二叉树
求解过程相当于在树的叶节点中搜索满足条件的节点。
时间复杂度:O(2^n)
深度优先搜索解空间
搜索开始的节点就成为第一个活结点,同时也成为当前的扩展结点。在当前的扩展结点处,以深度优先方式移至一个新结点(子结点),生成问题的新状态。这个新结点就成为一个新的活结点,并成为当前扩展结点。如果在当前的扩展结点处不能再向纵深方向移动。则当前扩展结点就成为死结点。此时,应往回移动至最近的一个活结点处,并使这个活结点成为当前的扩展结点。
回溯法即以这种工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已没有活结点时为止。
设N=3,W=(16,15,15),P=(45,25,25),C=30
商品:G1,G2,G3
首先从结点A开始,包状态C=30,A为活结点,当前扩展结点
之后到结点B,表示放入G1,包状态C=14,B为活结点,当前扩展结点
之后到结点D,表示放入G2,包状态C=14<W2=15,故不能放入G2,D为死结点,故往回移动至最近的一个活结点B处,B为当前扩展结点
之后到结点E,表示不放入G2,包状态C=14,E为活结点,当前扩展结点
之后到结点J,表示放入G3,包状态C=14<W3=15,故不能放入G3,J为死结点,故往回移动至最近的一个活结点E处,E为当前扩展结点
之后到结点K,表示不放入G3,包状态C=14,K为活结点,当前扩展结点,K之后不可以在深度优先搜索解空间,故K为死结点,故往回移动至最近的一个活结点E处,E为当前扩展结点,E之后不可以在深度优先搜索解空间,故E为死结点,故往回移动至最近的一个活结点B处,B为当前扩展结点,B之后不可以在深度优先搜索解空间,故B为死结点,故往回移动至最近的一个活结点A处,A为当前扩展结点,A可以扩展到结点C
之后到结点C,表示不放入G1,包状态C=30,C为活结点,当前扩展结点
之后到结点F,表示放入G2,包状态C=15,F为活结点,当前扩展结点
之后到结点L,表示放入G3,包状态C=0,C为活结点,当前扩展结点,L之后不可以在深度优先搜索解空间,故L为死结点,故往回移动至最近的一个活结点F处,F为当前扩展结点,F可以扩展到M
之后到结点M,表示不放入G3,包状态C=15,M为活结点,当前扩展结点
M之后不可以在深度优先搜索解空间,故M为死结点,故往回移动至最近的一个活结点F处
F之后不可以在深度优先搜索解空间,故F为死结点,故往回移动至最近的一个活结点C处
C可以扩展到G
之后到结点G,表示不放入G2,包状态C=30,G为活结点,当前扩展结点
之后到结点N,表示放入G3,包状态C=15,N为活结点,当前扩展结点,N之后不可以在深度优先搜索解空间,故N为死结点,故往回移动至最近的一个活结点G处,G为当前扩展结点,G可以扩展到O
之后到结点O,表示不放入G3,包状态C=30,O为活结点,当前扩展结点,O之后不可以在深度优先搜索解空间,故O为死结点,故往回移动至最近的一个活结点G处
G之后不可以在深度优先搜索解空间,故G为死结点,故往回移动至最近的一个活结点C处
C之后不可以在深度优先搜索解空间,故C为死结点,故往回移动至最近的一个活结点A处
A之后不可以在深度优先搜索解空间,故A为死结点
生成问题状态的几个基本概念
–活结点:一个自身已搜索到但其儿子结点还没有搜索完的结点称作活结点
–扩展结点:一个正待在扩展产生待搜索儿子结点的结点称为扩展结点
–死结点:一个所有儿子结点已经搜索完毕的结点称作死结点
深度优先的问题状态生成法:如果对一个扩展结点R,一旦产生了它的一个儿子结点C,就把C当作新的扩展结点。在完成对子树C(以C为根的子树)的穷尽搜索之后,将R重新变成扩展结点,继续生成R的下一个儿子结点(如果存在)
搜索过程中某个阶段活结点可能有多个;扩展结点只有一个且与某个活结点重合
旅行售货员问题(货郎问题)
有多个城市,已知其中任何两个城市之间有直接道路及道路的距离。一个售货员需要到每个城市巡回卖货,需要从某个城市出发,恰好经过其它每个城市一次,最后回到出发的城市。问怎样走使得总的路程最短。
实例:某售货员要到4个城市去推销商品,已知各城市之间的路程代价(路费/时间),如图。
请问他应该如何选定一条从城市1出发,经过每个城市一遍,最后回到城市1的路线,使得总的周游路程代价(路费/时间)最小?
1.A->B 城市1 旅行成本=0
B->C 城市2 旅行成本=30
C->F 城市3 旅行成本=35
F->L 城市4 旅行成本=55
回到城市1 旅行成本=59
2.A->B 城市1 旅行成本=0
B->C 城市2 旅行成本=30
C->G 城市4 旅行成本=40
G->M 城市3 旅行成本=60
回到城市1 旅行成本=66
3.A->B 城市1 旅行成本=0
B->D 城市3 旅行成本=6
D->H 城市2 旅行成本=11
H->N 城市4 旅行成本=21
回到城市1 旅行成本=25
4.A->B 城市1 旅行成本=0
B->D 城市3 旅行成本=6
D->I 城市4 旅行成本=26>25
I->O 城市2 旅行成本=36
回到城市1 旅行成本=66
5.A->B 城市1 旅行成本=0
B->E 城市4 旅行成本=4
E->J 城市2 旅行成本=14
J->P 城市3 旅行成本=19
回到城市1 旅行成本=25
6.A->B 城市1 旅行成本=0
B->E 城市4 旅行成本=4
E->K 城市3 旅行成本=24
K->Q 城市2 旅行成本=29
回到城市1 旅行成本=59
时间复杂度:O((N-1)!)
约束函数
****问题的解向量:回溯法希望一个问题的解能表示成一个n元式(x1,x2,…,xn)的形式。
****显约束:对分量xi的取值限定。
***隐约束:为满足问题的解而对不同分量之间施加的约束。
例如:在0-1背包中:目标
显约束:xi属于{0,1}
隐约束:和wix1<=c,i=1,2,…,n
限界函数
为了避免生成那些不可能产生最佳解的问题状态,可定义限界函数(bounding function)来“处死那些实际上不可能产生所需解的活结点”。
如旅行售货员问题中阶段值(从根节点到某扩展结点的部分路线费用)大于已求得的最优值(已知的从根到叶路线的费用)
具有约束函数+界限函数(剪枝函数)对解空间的深度优先搜索方法称为回溯法。
(回溯法=带剪枝的深度优先全局搜索)
1.子集树算法实现形式
子集树:找n个元素中找出满足某性质的元素子集(一组元素)
递归回溯实现模式
void Backtrack(int t)//t为当前搜索所处的层数
{
if(t>n) Output(x);//到达叶节点,输出可行解x[],从根到叶路径
else
for(int i=1;i>=0;i--){//每次扩展有两种选择,0或1
x[t]=i;//记录当前位置路径取值
if(Constraint(t)&&Bound(t)) Backtrack(t+1);
}//Constraint(t)是否满足约束条件,Bound(t)能否得到更优解,条件成立时递归到t+1层,不成立时继续扩展未搜索的右子树。递归返回则相当于在树中向上一层回溯。
}
2.排列数算法实现形式
排列数:确定n个元素满足某性质的排列顺序
递归回溯实现模式
void Backtrack(int t)//x[n]初始化为单位排列,如{1,2,3,4}
{
if(t>n) Output(x);
else
for(int i=t;i<=n;i++){//第t层有n-t+1种扩展选择
swap(x[t],x[i]);//在t层执行第i种扩展选择,记录当前选择
if(Constraint(t)&&Bound(t)) Backtrack(t+1);//迭代到一下层
swap(x[t],x[i]);//回溯到上一层时,恢复原来顺序
}
}
!!!回溯法的基本思想
(1)针对所给问题,定义问题的解空间
(2)确定易于搜索的解空间结构和节点的扩展规则
(3)以深度优先搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
两个常用的剪枝函数:
(1)约束函数:在扩展结点处减去不满足约束的子树
(2)界限函数:减去得不到最优解的子树
5.2两艘船的装载问题
问题描述:n个集装箱装到2艘载重量分别为c1,c2的货轮,其中集装箱i的重量为wi,且Σwi<=c1+c2.问题要求找到一个合理的装载方案可将这n个货箱全装上这2艘轮船。
例如:当n=3,c1=c2=50,w=[10,40,50],有解;
若w=[20,40,40],问题无解
********该问题为NP完全问题
若给定的装载问题有解,则可采用如下策略得一个装载方案:
(1)将第一艘轮船尽可能装满;
(2)将剩余得集装箱装到第二艘船上。
证明:因为第一艘船尽量多装,即已装载重量最大;
所以第二艘船需装载得剩余集装箱重量最小。
如果此方案下得不到一个装载方案,即剩余这些集装箱无法装上第二艘船,此时,无论如何调整第一艘船,剩余集装箱重量只会更大,因此无论如何都装不上第二艘船。这样,该问题便无解。与问题有解得假设矛盾。因此,上述方案可得到一个装载方案的解。
【回溯算法思路】:将第一艘船尽可能装满等价于如下问题
问题的解为n元向量{x1,…,xn},xi属于{0, 1}
可用子集树来表示船载重量约束:Σ wi<=c1
最优化目标(非价值):MaxΣ wixi
例如 n=4,c1=12,w=[8,6,2,3],bestw初值=0,cw为船当前载重;不可行解剪枝
public class Loading1{//相关变量和初始化
private int n;//集装箱数
private int[] w;//集装箱重量数组
private int c;//第一艘轮船的载重量
private int cw;//当前载重量
private int bestw;//当前最优载重量
private int r;//剩余集装箱重量
public static int maxLoading(int []ww,int cc)
{
w=ww;c=cc;cw=0,bestw=0;
for(int i=1;i<=n;i++) r+=w[i];
backtrack(1);
return bestw;
}
}
**利用约束函数剪枝**
//利用约束函数剪枝
public void backtrace(int i){
if(i>n){//i表示层数,到达叶节点
if(cw>bestw) bestw=cw;
return ;
}
if(cw+w[i]<=c){//满足约束时搜索左子树
cw+=w[i];
backtrace(i+1);
cw-=w[i];//回溯时修改当前载重量
}
backtrace(i+1);//搜索右子树
}
问题:为什么不对右子树用约束剪枝?
我的理解:右子树表示不放入集装箱,不存在大于船载重量的情况
限界函数设计
限界函数:cw+r>bestw
设bestw:当前最优载重量(某个叶节点);
cw=Σ wi xi:当前扩展节点的载重量,所有以确定装载与否集装箱的实际载重量和;
r=Σ wj:剩余未判断装载与否集装箱的重量和;
当cw+r<=bestw时,将cw对应的子树剪去。
剪枝条件
cw+wi+1>c1 装载量不符合约束,不可行剪枝
cw+r<=bestw 不能产生更优解,不更优剪枝
//利用限界函数剪枝
public void backtrace(int i){
if(i>n){//i表示层数,到达叶节点
if(cw>bestw) bestw=cw;
return ;
}
r-=w[i];//扩展时减少剩余集装箱重量
if(cw+w[i]<c){//满足约束时搜索左子树
cw+=w[i];
backtrace(i+1);
cw-=w[i];//回溯时修改当前载重量
}
if(cw+r>bestw) backtrace(i+1);//满足界限时搜索右子树
r+=w[i];//回溯时增加剩余集装箱重量
}
算法复杂性:O(2^n)子集树的叶节点数
问题:为什么不对左子树用限界函数剪枝?
扩展结点与左子节点载重量限值(cw+r)相等/不变,故满足限界条件。
求最优值时记录最优解
public class Loading2{
private int n;//集装箱数
private int[] w;//集装箱重量数组
private int c;//第一艘轮船的载重量
private int cw;//当前载重量
private int bestw;//当前最有载重量
private int r;//剩余集装箱重量
private int[] x;//当前解
private int[] bestx;//当前最优解
public static int maxLoading(int []ww,int cc,int []xx)
{
n=ww.length-1;w=ww;c=cc;cw=0;bestw=0;x=new int[n+1];bestx=x;
for(int i=1;i<=n;i++) r+=w[i];
backtrace(1);
return bestw;
}
public void backtrace(int i){
if(i>n){//i层数,到达叶节点
if(cw>bestw)
{
bestw=cw;
for(int j=0;j<n;j++) bestx[j]=x[j];
}
return ;
}
r=r-w[i];
if(cw+w[i]<c){//搜索左子树
x[i]=1;
cw+=w[i];
backtrace(i+1);
cw-=w[i];//回溯时修改当前载重量
}
if(cw+r>bestw)
{
x[i]=0;
backteace(i+1);//搜索右子树
}
r+=w[i];//回溯时增加剩余集装箱数量
}
}
算法复杂性:O(n*2^n)bestx最多会被更新2 ^n次,每次n个数赋值
回溯求解算法优化思路
1.运行Loading1得到最优值;复杂性O(2^n)
2.修改Loading2,将bestw改为Loading1得到值
if(i>n){//i层数,到达叶节点
输出x[],终止算法
}
if(cw+r>=bestw) 搜索右子树
即找到最优值即退出,返回当前解X数组(由于限界函数,第一次到达叶节点即最优解)
3.此时X即为最优解;复杂性O(2^n)
总体复杂性O(2^n)
迭代(基于循环方式)回溯
while(true){//迭代搜索
while(i<=n&&cw+w[i]<=c){//进入左子树
x[i]=1;
r-=w[i];
cw+=w[i];
i++;
}
if(i>n){//到达叶节点
for(int j=1;j<=n;j++) bestx[j]=x[j];//记录当前最优解/路径
bestw=cw;//记录当前最优值
}
else{//进入右子树
x[i]=0;
r-=w[i];
i++;
}
while(cw+w[i]<=bestw){//到达叶节点或右子树不满足限界时回溯
i--;//回溯一层
while(i>0&&!x[i]){//当为右子树回溯时
r+w[i];
i--;
}
if(i==0) return bestw;//回溯过根结束
//为左子树回溯时,进入未搜索过的右子树
x[i]=0;
r-=w[i];
i++;
}
}
5.3基于回溯的0-1背包问题
子集树:找n个元素种满足某性质的元素子集
void Backtrack(int t)//t为当前搜索所处的层数
{
if(t>n) Output(x);//到达叶节点,输出可行解x[],从根到叶路径
else
for(int i=1;i>=0;i--){//每次扩展有两种选择,0或1
x[t]=i;//记录当前位置路径取值
if(Constraint(t)&&Bound(t)) Backtrack(t+1);
}//Constraint(t)是否满足约束条件,Bound(t)能否得到更优解,条件成立时递归到t+1层,不成立时继续扩展未搜索的右子树。递归返回则相当于在树中向上一层回溯。
}
定义一个当前节点理论上可能的最优值为上届,最大装包价值
**限界函数设计:**bound(t)>当前已求得的最优值
(1)当前扩展结点背包戒指+剩余物品价值是否大于已找到可行解种最高价值(上界过高,剪枝少)
(2)当前扩展结点背包价值+用动态规划方法求得剩余物品0-1背包问题的最优值是否大于已找到的可行解中的最高价值(上界准确,代价过高)
(3)当前扩展结点背包价值+按贪心策略对剩余物品的背包问题求得的最优值是否大于已找到的可行解中的最高价值(近似最优上界,代价可接受)
!对剩余物品按单位重量价值排序
!依次装入物品直至某物品不能装入
!装入该物品的一部分使得背包装满
public void BackTrack(int t){
//已经搜索到根节点
if(t>n-1){
if(tempValue>maxValue){
maxValue=tempValue;
for(int i=0;i<n;i++) bestWay[i]=way[i];
}
return ;
}
//搜索左边节点,不需用限界函数
if(tempWeight+weight[t]<=capacity){
tempWeight+=weight[t];
tempValue+=value[t];
way[t]=1;
BackTrack(t+1);
tempWeight-=weight[t];
tempValue-=value[t];
way[t]=0;
} //不装如这个物品,直接搜索右边的节点,不许用约束函数
if(greedyBound(t+1)>=maxValue){
BackTrack(t+1);
}
}
public double greedyBound(int k){
double maxLeft=tempValue;
int leftWeight=capacity-tempWeight;
//依照单位重量价值次序装剩余的物品
while(k<=n-1&&leftWeight>=weight[k]){
leftWeight-=weight[k];
maxLeft+=value[k];
k++;
}
//不能装时,用下一个物品的单位重量价值折算到剩余空间
if(k<=n-1){
maxLeft+=value[k]/weight[k]*leftWeight;
}
return maxLeft;
}
回溯法解决的问题
组合优化问题
1.存在目标函数(极大化或极小化)
2.具有约束条件(解需要满足的条件)
3.问题解空间:问题所有形式上可能的解
4.可行解:问题搜索空间中满足约束条件的解
5.最优解:使得目标函数达到极大或极小的可行解
回溯:具有剪枝函数(约束函数+限界函数)的深度优先搜索方法
!!!回溯法的基本思想
(1)针对所给问题,定义问题的解空间
(2)确定易于搜索的解空间结构和节点的扩展规则
(3)以深度优先搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
两个常用的剪枝函数:
(1)约束函数:在扩展结点处减去不满足约束的子树
(2)界限函数:减去得不到最优解的子树
解题步骤:1)针对所给问题,定义问题的解空间
2)确定约束条件
3)以深度优先方式搜索解空间
算法模式:
递归回溯
void Backtrack(int t)
{
if(t>n) Output(x);
else for(int i=f(n,t);i<=g(n,t);i++)
x[t]=h(i);//f和g为可扩展的待选节点范围
if(Constraint(t)&&Bound(t)) Backtrack(t+1);
}
迭代回溯
void IterativeBacktrack(void)
{
int t=1;
while(t>0){
if(f(n,t)<=g(n,t))//非叶节点层
for(int i=f(n,t);i<=g(n,t);i++) x[t]=h(i);
if(Constraint(t)&&Bound(t)){
if(Solution(t)) Output(x);
else t++;
}
else t--;//t--回溯,t++迭代扩展
}
}
在任何时刻,算法往往只需保存从根节点到当前扩展节点的路径。如果解空间树中从根节点到叶节点的最长路径的长度为h(n),则回溯法所需的空间通常为O(h(n))。而显式地存储整个解空间则需要O(2^h(n))或O(h(n)!)内存空间。
5.4批处理作业调度
n个作业要在由2台机器M1和M2组成的加工线上完成处理。每个作业有两项任务,加工顺序都是先在M1上加工任务1,然后在M2上加工任务2。M1和M2加工作业i所需的时间分别为ti1和ti2,批处理作业调度问题确定这n个作业的最优加工顺序(最佳调度方案),使得n个作业(在机器2上)加工完成时间的和最小。
作业i在机器2上的加工完成时间为ti1+ti2+该作业的等待时间。
理解模型
第一种:A->B->C 19S=3+6+10
第二种:A->C->B 18S=3+7+8
第三种:B->A->C 20S=4+6+10
第四种:B->C->A 21S=4+8+9
第五种:C->A->B 19S=5+6+8
第六种:C->B->A 19S=5+6+8
第七种:A->C->B 18S=3+7+8
第八种:C->A->B 19S=5+6+8
有三种调度情况所有作业在机器2的最终完成时间一样(流水作业调度,求最短作业完成时间,适合动态规划法求解)
批作业调度的目标是所有作业完成时间和。因为,一批作业同时到达,需同时考虑所有作业用户的平均满意度(平均等待时间短)和提高资源利用率(资源空闲时间短)。
1.回溯法算法设计
1)问题的简单的解决方法就是在搜索不同作业调度排列的同时,不断更新最优解,最后找到问题的解。算法框架用数组x(初值为1,2,3,…,n)模拟不同的排列,在不同排列下计算各种排列下的加工耗时情况。
这3个作业的6种可能的调度方案是:
ABC;ACB;BAC;BCA;CAB;CBA;它们所相应的完成时间和分别是19,18,20,21,19,19。易见,最佳调度方案是A,C,B,其完成时间和为18。
算法思路用排列树表示解空间E={x1,…,xn},xi属于{1,…,n},
约束条件:当i≠j,xi≠xj(元素不能重复选取)
限界函数:bestx:求已求得的最小完成时间和
x:当前扩展结点的完成时间和
r:加上待扩展作业后的完成时间和
当x+r>=bestx时,将x对应的子树剪去
2)当前作业a(第i-1个作业)的带扩展作业b(第i个作业)完成时间r:(机器2上加工完成时间记为f2[i],另机器1上加工完成时间为f1[i])
机器M2存在积压(如图1)或空闲(如图2)两种情况。当机器M2有积压情况出现时,f2[i]=f2[i-1]+M2[x[i]];当机器M2有空闲时,f2[i]=f1[i]+M2[x[i]].机器M1进行顺序不间断加工,其上加工时间是固定的,f1[i]=f1[i-1]+M1[x[i]]。
2.数据结构设计
1)用二维数组job(n,2)存储作业在M1,M2上的加工时间。
2)由于f1在计算中,只需要当前值(作业在机器1上顺序执行),所以用变量存储即可,不必用数组;而f2在计算中,还依赖前一个作业数据判断机器2空闲和积压两种不同情况,所以有必要用数组存储。
3)考虑到回溯过程的需要,用变量f存储当前加工所需要的全部时间。
3.算法实现
int job[n][2],x[n],f1=0,f=0,f2[n]=0;
main()//x[]:当前作业调度排列
{//job[][2]:各作业在两机器上的执行时间
int j;//f:已执行作业在机器2的完成时间和
input(n);//f1:当前执行作业在机器1的完成时间
for(i=1;i<=2;i++)//f2[i]:作业i在机器2的完成时间
for(j=1;j<=n;j++)
input(job[j][i]);
try();
}
backtrackBatch(int i)
{
int j;
if(i>n){
for(j=1;j<=n;j++)//到达叶节点,记录该排列方式x[]
bestx[j]=x[j];
bestf=f;//该排列方式所对应的时间
}else//job[x[j][i]代表当前作业调度x排列中的第j个作业在第i台机器上的处理时间
{
for(j=i;j<=n;j++)
{
f1=f1+job[x[j]][1];//记录第一台机器加工所花费的时间
if(f2[i-1]>f1) f2[i]=f2[i-1]+job[x[j]][2];//M2有积压的情况
else f2[i]=f1+job[x[j]][2];//M2有空闲的情况
f=f+f2[i];
if(f<bestf)
{
swap(x[i],x[j]);
backtrackBatch(i+1);
swap(x[i],x[j]);
//交换作业顺序排列,swap记录在第i层选择的是第j号作业
f1=f1-job[x[j]][1];
f=f-f2[i];
//回溯后恢复f1和f的值
}
}
}
}
3)一个最优调度应使机器M1没有空闲时间,且机器M2的空闲时间最少。在一般情况下,当作业按在机器M1上时间由小到大排列后,机器M2的空闲时间较少,当然最少情况一定还与M2上的加工时间有关,所以还需要对解空间进行搜索。但排序后对问题可以尽快地问题可以尽快地找到接近最优的解,再加入下一步界限操作就能加速搜索速度。(让第一次到达的叶节点解接近最优值,从而优化bestf值)