一、回溯算法
以深度优先的方式系统地搜索问题的解的方法称为回溯法。
可以系统地搜索一个问题的所有解或任意解。
有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。
回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。
二、0-1背包问题
给定一个物品集合s={1,2,3,…,n},物品i的重量是wi,其价值是vi,背包的容量为W,即最大载重量不超过W。在限定的总重量W内,我们如何选择物品,才能使得物品的总价值最大。
假设背包容量C=30,w={16,15,15},v={45,25,25}
三、旅行商问题
是指一销售商从n个城市中的某一城市出发,不重复地走完其余n-1个城市并回到原出发点,在所有可能的路径中求出路径长度最短的一条。本题假定该旅行商从第1个城市出发。
输入
对每个测试例,第1行有两个整数:n(4≤n≤10)和m(4≤m≤20 ) ,n是结点数,m是边数。接下来m行,描述边的关系,每行3个整数:(i,j),length,表示结点i到结点j的长度是length。
当n=0时,表示输入结束。
输出
对每个测试例,输出最短路径长度所经历的结点,最短的长度。
旅行商问题的解空间是一棵排列树。
开始时,x={1,2,…,n},相应的排列树由x的全排列构成。
回溯算法的数据结构:
#define NUM 100
int n; //图G的顶点数量
int m; //图G的边数
int a[NUM][NUM]; //图G的邻接矩阵
int x[NUM]; //当前解
int bestx[NUM]; //当前最优解向量
int cc; //当前费用
int bestc; //当前最优值
int NoEdge = -1; //无边标记
在构造邻接矩阵a时,其初始值应为NoEdge:
for (i=0; i<NUM; i++)
for (j=1; j<NUM; j++)
a[i][j] = NoEdge;
最优值和向量x的初始化数值如下:
bestc = NoEdge;
for(i=1; i<=n; i++)
x[i] = i;
代码实现
void Backtrack(int t){
//到达叶子结点的父结点。旅行路径上的倒数第二个城市。最后一个城市是出发点
if(t==n) {
if(a[x[n-1]][x[n]]!= NoEdge && a[x[n]][1]!= NoEdge &&
(cc + a[x[n-1]][x[n]]+a[x[n]][1]<bestc||bestc== NoEdge))
{
for(int i=1; i<=n; i++)
bestx[i] = x[i];
bestc = cc + a[x[n-1]][x[n]] + a[x[n]][1];
}
return;
}
else {
for(int i=t; i<=n; i++)
{
if(a[x[t-1]][x[i]]!= NoEdge &&
(cc + a[x[t-1]][x[i]]< bestc||bestc == NoEdge))
{
swap(x[t],x[i]); // 先交换。此时,X[t]的值发变化。
cc += a[x[t-1]][x[t]]; //再计算。不能交换顺序。
Backtrack(t+1); // 如果要改变顺序,应该为cc += a[x[t-1]][x[i]]。
cc -= a[x[t-1]][x[t]];
swap(x[t],x[i]);
}
}
}
}
四、(流水)批作业处理问题
给定n个作业的集合j={j1,j2,…,jn}。
每一个作业ji都有两道工序,分别在两台机器上完成。一台机器一次只能处理一道工序,并且工序一旦开始,就必须进行下去直到完成。
每一个作业必须先由机器1处理,然后由机器2处理。
作业ji需要机器j的处理时间为t[i][j],其中i= 1, 2, …, n ,j=1, 2。对于一个确定的作业调度,设f[i][j]是作业i在机器j上的完成处理的时间(等待时间+处理时间),所有作业在机器2上完成处理的时间之和定义如下:
称为该作业调度的完成时间之和。
由于只有两台机器,作业的处理顺序极大地影响结束时间f。流水作业调度问题要求对于给定的n个作业,制定最佳作业调度方案,使其完成时间和(等待服务时间)达到最小。
作业ji需要机器j的处理时间为t[i][j],其中i= 1, 2, …, n ,j=1, 2。对于一个确定的作业调度,
设f[i][j]是作业i在机器j上的完成处理的时间,
根据要求,每个作业的第一道工序执行顺序的特点:
依次串行处理。
因此
前i件作业在机器1上完成处理的时间定义如下:
f[i][1]=sum(t[j][1]) (1<=j<=i)
前i件作业在中的每一道作业的第二道工序何时执行?
本作业的第一道工序执行结束,并且
前一道作业的第二道工序也执行结束,
因此,第i道作业的第二道工序执行时间为
f[i][2]=max(f[i-1][2],f[i][1])+t[i][2]
目标:
f=sum(f[i][2]) (1<=i<=n)
以调度顺序为1,2,3为例:
作业1在机器1上完成的时间为2,在机器2上完成的时间为3(2+1)
作业2在机器1上完成的时间为5(2+3),在机器2上完成的时间为6(5+1)
作业3在机器1上完成的时间为7(5+2),在机器2上完成的时间为10(7+3)
3+6+10=19,所以是19
调度顺序为1,3,2
作业1在机器1上完成的时间为2, 在机器2上完成的时间为3
作业3在机器1上完成的时间为4,在机器2上完成的时间为7
作业2在机器1上完成的时间为7,在机器2上完成的时间为8
3+7+8=18,所以时间和为18
由于每个作业都要处理,只是在机器上的处理顺序不同,因此流水作业调度问题的一个候选解是n个作业的一个排列。
设解向量为X ( x1, x2, …, xn) ,就是确定最优解是n个作业( 1, 2, …, n )的哪一个排列,因此解空间是一棵排列树。
代码实现:
#include <iostream>
using namespace std;
class jobs{
int m1,m2;//每到工序所需要的时间
int index; //作业的编号
public:
jobs(int a=0, int b=0):m1(a),m2(b){}
int get_m1(){
return m1;
}
int get_m2(){
return m2;
}
void set(int a, int b ){
m1=a;
m2=b;
}
};
void arrange(jobs*job, int n,int t,int f1,int *f2,int &f,int sum,int x[],int best[]){
if(t>n){ //已经遍历到叶子结点,进行最优值的更新
if(sum<f){
f=sum;
for(int i=1;i<=n;i++) best[i]=x[i];
}
}
else{
for(int j=t;j<=n;j++){
f1=f1+job[x[j]].get_m1();
f2[t]=(f2[t-1]>f1?f2[t-1]:f1)+job[x[j]].get_m2();
sum=sum+f2[t];
swap(x[t],x[j]);
arrange(job,n,t+1,f1,f2,f,sum,x,best);
swap(x[t],x[j]);
f1=f1-job[x[j]].get_m1();
sum=sum-f2[t];
}
}
}