一、实验目的
1、了解回溯法和分支限界法的基本思想。
2、运用回溯法和分支限界法解决最小重量机器设计问题。
二、实验要求
最小重量机器设计问题:设某一机器由n个部件组成,每一种部件可以从m个不同的供应商处购得。设wij是从供应商j处购得的部件i的重量,cij是相应的价格。试设计一个算法,给出总价格不超过c的最小重量机器设计。分别用回溯法和分支限界法实现问题的算法。
三、算法思想和实现
1、回溯法
(1)此问题是一棵排列树,设开始时bestx=[-1,-1,……,-1]则相应的排列树由x[0:n-1]的所有排列构成。
(2)找最小重量机器设计的回溯算法Backtrack是类machine的公有成员。私有数据成员整型数组Savex保存搜索过的路径,到达叶节点后将数据赋值给数组bestx。成员bestw记录当前最小重量,cc表示当前花费,cw表示当前的重量。
(3)在递归函数Backtrack中,在保证总花费不超过c的情况下:
当i=n时,当前扩展结点是排列树的叶节点。此时搜索到一个解,判断此时的最小重量是否小于当前最小重量,若小于则更新bestw,并得到搜索路径bestx。
当i<n时,当前扩展结点位于排列树的第i-1层。当x[0:i]的花费小于给定最小花费时,算法进入排列树的第i层,否则将减去相应的子树。算法用变量cc记录当前路径x[0:i]的费用。
#include<iostream>
using namespace std;
#define N 3
#define M 3
int w[N][M]={{1,2,3},{3,2,1},{2,2,2}};
int c[N][M]={{1,2,3},{3,2,1},{2,2,2}};
class machine
{public:
void InitMachine(int C);
void Backtrack(int i);
void printsave();
private:
int COST;//题目中的C
int cw;//当前的重量
int cc;//当前花费
int bestw;//当前最小重量
int bestx[N];//最优解
int savex[N];//savex[i]如果为j,表示第i个零件应该选用第j个人供应的
};
void machine::InitMachine(int C){
COST=C;
cw=cc;
bestw=-1;//初值为-1,标记最小重量是否变化
for(int j=0;j<N;j++)
bestx[j]=-1;
}
void machine::Backtrack(int i)
{ if(i>=N)//达到叶子节点
{if(cw<bestw||bestw==-1)//当前能找到的最小重量<以前最小重量,保留当前的最小重量
{
bestw=cw;
cout<<"************************************"<<endl;
cout<<"搜索路径的bestw:"<<bestw<<endl<<"供应商搜索路径:";
for(int k=0;k<N;k++)//把当前搜过的路径记下来
{
cout<<bestx[k]<<"";
savex[k]=bestx[k];
}
cout<<endl;
}
return;
}
for(int j=0;j<M;j++)//依次递归尝试这三个供应商
{
if((cc+c[i][j]<COST)&&(w[i][j]>0))
{cc+=c[i][j];
cw+=w[i][j];
bestx[i]=j;
Backtrack(i+1);
bestx[i]=-1;
cc-c[i][j];
cw-=w[i][j];
}
}
void machine::printsave(){
if(bestw==-1) { cout<<"输入的总价格太小!"<<endl;
}
else {
cout<<"************************"<<endl;
cout<<"最优重量(bestw )是:"<<bestw<<endl;
for(int k=0;k<N;k++)
cout<<"第"<<k+1<<"个部件由第"<<save[k]+1<<"供应商供应"<<endl;
cout<<endl;
}
}
int main(){
mach Mach;
int C;
cout<<"输入总价格不超过的C:";
cin>>C;//输入最小花费
Mach.InitMachine(C);
Mach.Backtrack(0);
Mach.printsave();
return 0;
}
2、分支限界法
解空间为一棵排列树,采用优先队列式分支限界法找出所给的最小重量机器设计,开始时,将排列树的根节点置为当前扩展结点。在初始扩展结点处还设有选定部件是哪个供应商提供的,故cv=0,cw=0,position=0,peer=0,1≤i≤n。x[1:n]记录供应商的选择while完成对排列树内部结点的有序扩展。循环体内依次从活结点优先队列中取出具有最小重量的结点,依次为当前扩展结点。并且花费不超过c并加以扩展,队列为空时则结束循环。当peer=n时,此时扩展结点是叶节点,得到当前的最小重量和最优解数组x。若peer<n时,算法依次产生当前扩展结点的所有儿子节点。对于当前扩展结点的一个儿子结点,计算出当前的重量,当小于当前的最小重量时,将儿子结点插入到活结点优先队列中,而当不下于当前最小重量时,以当前这个结点为根的子树中不可能有比当前最小重量部件选择更好的解,故可将此结点舍去。若在一结点所计算的花费大于所给的最小花费,则剪去以此节点为根的子树。
#include "fstream.h"
#include "iostream.h"
struct nodetype {
int peer;
struct nodetype *parent;
int position;
double cw;
double cv;
double r; };
struct nodetype *queues[100000000];
/小根堆///
void insert(struct nodetype *x, int oldlast) //x是要插入的数
{ //oldlast是目前堆的元素数目
int last = oldlast+1;
queues[last]=x;
int i=last;
while((i > 1)&&(queues[i]->r < queues[i/2]->r)) {
struct nodetype *temp;
temp=queues[i];
queues[i]=queues[i/2];
queues[i/2]=temp;
i = i/2;
}
} //last是当前堆的元素个数,执行该函数后
struct nodetype * deletemin(int last,struct nodetype *a[])
{ //返回堆的第一个元素(即最小元素)
struct nodetype *temp;
temp=a[1];
a[1]=a[last];
last --;
int i = 1;
int j=0;
while(i <= last/2)
{ if((a[2*i]->r < a[2*i+1]->r)||(2*i == last)) j = 2*i;
elsej=2*i+1; if(a[i]->r > a[j]->r) {
struct nodetype *temp;
temp=a[i];
a[i]=a[j];
a[j]=temp;
i = j;
}
else return(temp);
}
return(temp);
}
void main() /小根堆///
{
ifstream fin("input.txt");
ofstream fout("output.txt");
int n,m,c;
fin>>n;fin>>m;fin>>c;
double **w=new double*[n+1];
double **cc=new double*[n+1];
for(int i=1;i<=n;i++){
w[i]=new double[m+1];
cc[i]=new double[m+1];
}
for(i=1;i<=n;i++)
for(int j=1;j<=m;j++)
fin>>cc[i][j];
for(i=1;i<=n;i++)
for(int j=1;j<=m;j++)
fin>>w[i][j];
double *cmin=new double[n+1];
double *wmin=new double[n+1];
for(i=1;i<=n;i++) {
cmin[i]=cc[i][1];
wmin[i]=w[i][1];
for(int j=2;j<=m;j++) {
if(cmin[i]>cc[i][j]) cmin[i]=cc[i][j];
if(wmin[i]>w[i][j]) wmin[i]=w[i][j];
}
}
double *rc=new double[n+1];//剩余部件最小价格和
double *rw=new double[n+1];//剩余部件最小重量和
rc[n]=0;rw[n]=0;
for(i=n-1;i>=1;i--) {
rc[i]=rc[i+1]+cmin[i+1];
rw[i]=rw[i+1]+wmin[i+1];
}
struct nodetype *node=new struct nodetype;
node->peer=0;
node->cv=0;
node->cw=0;
node->position=0;
node->r=rw[1]+wmin[1];
insert(node,0);
int cpeer=0;
int q_len=1;
bool isend=false;
while(!isend&&q_len>0)
{ node=deletemin(q_len,queues);
q_len--;
if(node->peer==n) {
isend=true;
fout<<node->cw<<endl; int *x=new int[n+1];
for(int k=n;k>=1;k--) {
x[k]=node->position;
node=node->parent;
}
for(k=1;k<=n;k++) fout<<x[k]<<" ";
fout<<endl; return; }
for(int j=1;j<=m;j++)
{ if(node->cv+cc[node->peer+1][j]+rc[node->peer+1]<=c)
{
cpeer=node->peer+1;
struct nodetype *node_add=new struct nodetype ;
node_add->peer=cpeer;
node_add->cv=node->cv+cc[cpeer][j];
node_add->cw=node->cw+w[cpeer][j];
node_add->r=node_add->cw+rw[cpeer];
node_add->position=j;
node_add->parent=node;
insert(node_add,q_len);
q_len++;
}
}
}
if(q_len<=0) { fout<<"No Solution!"<<endl;
return;}
}
四、结果
input.txt
3 3 4
1 2 3
3 2 1
2 2 2
1 2 3
3 2 1
2 2 2
output.txt
4
1 3 1
五、总结
分支限界法与回溯法对当前扩展结点所采用的扩展方式不同。在分支限界法中,每一个结点只有一次机会成为扩展结点。而在回溯法中,每一个活结点有多次机会成为扩展结点。