问题描述:
有 n 个集装箱要装上一艘载重量为 W 的轮船,其中集装箱 i (1<=i<=n) 的重量,为wi。子啊装在体积不受限制的情况下,将尽可能重的集装箱装上轮船,当重量相同时要求选取的集装箱个数尽可能少,编写一个实验程序采用回溯法求解。要求采用适当的剪枝条件提高效率,左孩子结点剪枝的条件是只装载满足重量要求的集装箱,右孩子结点剪枝的条件是至多要选中 3 个集装箱。
例如,n=5,W=10,w[] = {5,2,6,4,3}时,其最佳装载方案是(0,0,1,1,0),既装载第3、4个集装箱。
问题分析:
看完题目感觉可以用穷举法把所有可能性全列举出来,然后从所有结果中选取一个最优解,然后输出。
but,题目需要回溯法,那么就要考虑用回溯法的思想去解决这个问题。
回溯法:在包含问题的所有解的解空间树中,按照深度搜索优先的策略,从根节点出发搜索解空间树。
下图展示的是程序的大致执行过程。
简单的说就是一直在深度优先搜索结果,查找完当前层的时候需要恢复以前修改数据,保证接下来深度搜索基准数据没有错。
如果只是简单的回溯,其实和穷举法差不多,但是 回溯法中可以写剪枝函数,把那些不满足条件的枝叶剪掉,从而加快搜索速度。
下面我们来模拟一遍回溯的过程,假设现在w[] 这个数组中只有3个元素,分别为 5 、2、6
图中黑色直线标识深度递归到某一层进行计算,蓝色的线条表示当前层计算完毕,需要恢复数据到上一层接着进行寻找。
每个节点右两个值,第一个代表当前层装的数量,第二个代表当前层的重量.
咱们设定取左孩子表示装当前 i 集装箱,取右孩子表示不装当前 i 集装箱
#include <stdio.h>
#define MAXN 20
int maxw=0; //总重量
int min=65535; //装的最少数量
int res[MAXN]; //存放最终结果
int count=0; //测试执行次数
int getMax(int a,int b)
{//获取两个数中最大值
return a>b?a:b;
}
void cpsz(int a[],int b[],int n)
{//复制数组
int i;
for(i=0;i<n;i++){
b[i]=a[i];
}
}
// i 表示层数
// n 表示总层数
// w[] 存放每个物品重量
// W 表示最大重量
// tw 当前层的重量
// tc 当前层的数量
// opt[] 临时存放解
void f(int i,int n,int w[],int W,int tw,int tc,int opt[])
{//主要递归主体
count++;
printf("层数 %d 总层数 %d 最大重量 %d 当前层重量 %d 当前层数量 %d count: %d \n",i,n,W,tw,tc,count);
if(i<n){
//取 i 物品 左剪枝
if(tw+w[i]<=W){
opt[i]=1;
f(i+1,n,w,W,tw+w[i],tc+1,opt);
}
opt[i]=0;
f(i+1,n,w,W,tw,tc,opt);
}else{
if(tw>=maxw){
maxw=tw;
if(tc<min){
cpsz(opt,res,n);
min=tc;
}
}
}
}
void printRes(int w[],int n){
int i;
printf("问题最终解为\n装箱物品: ");
for(i=0;i<n;i++){
if(res[i]==1)
printf("%d ",w[i]);
}
printf("\n装箱最大的容量为:%d",maxw);
printf("\n装箱数量为%d\n",min);
}
int main()
{
int n=5; //n个集装箱
int W=10; //轮船最大载重量
int w[] = {9,4,8,6,1}; //n个物品对应的数量
int opt[MAXN]; //存放临时解
f(0,n,w,W,0,0,opt);
printRes(w,n);
return 0;
}
从执行结果可以看出来左剪枝还是起到一定效果,高度为 5 的满二叉树应该为 31 次,但实际执行了27次.
由于题上给出来右剪枝的条件,所以可以进行右剪枝.下面就是右剪枝的代码
#include <stdio.h>
#define MAXN 20
int maxw=0; //总重量
int min=65535; //装的最少数量
int res[MAXN]; //存放最终结果
int count=0; //测试执行次数
int getMax(int a,int b)
{//获取两个数中最大值
return a>b?a:b;
}
void cpsz(int a[],int b[],int n)
{//复制数组
int i;
for(i=0;i<n;i++){
b[i]=a[i];
}
}
// i 表示层数
// n 表示总层数
// w[] 存放每个物品重量
// W 表示最大重量
// tw 当前层的重量
// tc 当前层的数量
// opt[] 临时存放解
void f(int i,int n,int w[],int W,int tw,int tc,int opt[])
{//主要递归主体
count++;
printf("层数 %d 总层数 %d 最大重量 %d 当前层重量 %d 当前层数量 %d count: %d \n",i,n,W,tw,tc,count);
if(i<n){
//取 i 物品 左剪枝
if(tw+w[i]<=W){
opt[i]=1;
f(i+1,n,w,W,tw+w[i],tc+1,opt);
}
//右剪枝
if(tw>3){
//不取 i 物品
opt[i]=0;
f(i+1,n,w,W,tw,tc,opt);
}
}else{
if(tw>=maxw){
maxw=tw;
if(tc<min){
cpsz(opt,res,n);
min=tc;
}
}
}
}
void printRes(int w[],int n){
int i;
printf("问题最终解为\n装箱物品: ");
for(i=0;i<n;i++){
if(res[i]==1)
printf("%d ",w[i]);
}
printf("\n装箱最大的容量为:%d",maxw);
printf("\n装箱数量为%d\n",min);
}
int main()
{
int n=5; //n个集装箱
int W=10; //轮船最大载重量
int w[] = {9,4,8,6,1}; //n个物品对应的数量
int opt[MAXN]; //存放临时解
f(0,n,w,W,0,0,opt);
printRes(w,n);
return 0;
}
进行右剪枝之后程序只执行了 7 次就出答案了, 看来右剪枝的效果还是很明显的, 但是如果题上没有给右剪枝的条件或者要求,最好不要进行右剪枝,以免程序出错。