1、简单装载问题
[问题描述]
有n个集装箱要装上一艘载重量为W的轮船,其中集装箱i ( 1≤i≤n )的重量为wi。不考虑集装箱的体积
限制,现要这些集装箱中选出若干装上轮船, 使它们的重量之和等于W ,当总重量相同时要求选取的集装箱个数尽可能少。
【求解算法】:
void dfs(int num,int tw,int rw,int op[ ],int i);
num :选择的集装箱个数;
tw : 选择的集装箱重量和
rw : 剩余集装箱的重量和;
op : 表示一个解,即一个选择方案,选择时,op=1,不选时,op=0
i : 表示考虑的集装箱i
对于第i层的某个分枝结点,对应的调用状态是dfs (num, tw, rw, op,i) ,对应的扩展结点有2个:
左分支:
●选择第i个
➢op[i]=1 , num=num+1, tw=tw+w[i], rw=rw-w[i],转到下一个状态dfs(num, tw, rw,op,i+1)
右分支:
●不选择第i个,
➢op[i]=0, num不变, tw不变, rw=rw-w[i] ,转到下一 个状态dfs (num, tw, rw, op,i+1),这个状 态和上一个状态的区别是,该状态中的num,tw没有变
每一次扩展不需要作出所有的扩展,只需要剪枝即可
【剪枝方式】
●左剪枝:
➢tw+w[i]<=w则扩展,否则不扩展(不超过载重量就扩展)
●右剪枝
➢tw+rw-w[i]>=w则扩展,否则不扩展
相当于➢tw+rw-w[i]>=max则扩展,否则不扩展
【剪枝过程】:
初始,num=0,tw=0,rw=所有集装箱的重量和20,括号里我们用来表示选与不选的向量表示(选择时用1表示不选 用0表示),i=1,首先进行左分支,若【选择】第一个集装箱,那么,tw=第一个集装箱的重量5,rw=20-5=15,判断当前是否满足tw+w[i]<=w,5<10满足,则向量里标记为1,num=0+1=1,继续判断下一个左分支(注:每次都默认先拓展左分支,左分支被剪掉再判断右分支,右分支也被剪掉再回溯),i=2,若【选择】第二个集装箱,那么 tw=tw+w[i]=5+2=7,rw=15-2=13,判断tw+w[i]=7<10,向量标记为1,num=2,继续扩展左分支,i=3,若【选择】第三个集装箱, tw=tw+w[i]=7+6=13>10超过了载重,所以剪掉该分支,拓展其右分支,即【不选】第三个集装箱,向量表示为0,tw=7,rw=rw-w[i]=13-6=7, 判断tw+rw-w[i]=7+13-6=14>10,则需要扩展,先扩展左分支,i=4,tw=7+4=11>10,所以不选择,左分支被剪掉,向量用0表示,再拓展右分支,此时是【不选】第4个集装箱,判断tw+rw-w[i]=7+7-4=10=w,所以可以扩展,向量用0表示,继续拓展左分支,i=5,若【选择】第五个集装箱,tw=7+3=10=w, 满足条件,rw=3-3=0, 所以num=3, 向量标记为1,此时暂时认为这个解就是最优解,将其保存,然后再看右分支,tw+rw-w[i]=7+3-3=7<w,所以不拓展,右分支被剪掉,此时所有集装箱都判断完,则回溯,一直回溯到第二个集装箱,(因为i=2没有判断右分支),然后判断tw+rw-w[i]=5+15-2=18>w, 所以拓展,然后一直判断下去直到i=5,然后再回溯到i=1的右分支,继续重复,得到第二个最优解(0,0,1,1,0),此时它的num=2 < 第一个num=3,所以它是最优解
【详细图解参考】:
【代码】
#include <stdio.h>
#include <string.h> //如果用到memset函数记得要加这个头文件!!
#define MAXN 20
//全局变量
int w[]={0,5,2,6,4,3} ;
int n=5,W=10;
int maxw=0; //存放最优解的总重量,初值是0
int x[MAXN]; //存放最优解向量,初值是0
int minnum=999999; //存放最优解的集装箱个数,初值为最大值
void dfs(int num,int tw,int rw,int op[],int i){
if(i>n){ //i>n说明所有的集装箱都判断完了
if(tw==W&&num<minnum){ //满足这个条件即为最优解,则覆盖之前的解
maxw=tw;
minnum=num;
for(int j=1;j<=n;j++){
x[j]=op[j]; //把最优解的值依次赋给最优解向量
}
}
}else{ //没到最后一个集装箱则需要判断当前这个集装箱是选还是不选
op[i]=1; //如果选择
if(tw+w[i]<=W){ //判断剪枝条件
dfs(num+1,tw+w[i],rw-w[i],op,i+1);
}
op[i]=0; //如果不选
if(tw+rw-w[i]>=W){ //判断剪枝条件
dfs(num,tw,rw-w[i],op,i+1);
}
}
}
void displaySolution(int n){
for(int i=1;i<=n;i++){
if(x[i]==1){
printf("选择第%d个集装箱\n",i);
}
}
printf("总重量为%d\n",maxw);
}
int main(){
int op[MAXN]; //op存放所有可行解
int rw;
memset(op,0,sizeof(op)); //初始化为0
for(int i=1;i<=n;i++){
rw=rw+w[i]; //初始剩余量=所有集装箱饿和
}
dfs(0,0,rw,op,1); //从第一个开始
displaySolution(n); //输出解
return 0;
}
#include <string.h> //如果用到memset函数记得要加这个头文件!!
【算法分析】
2、复杂装载问题
【问题描述】:
有一批共n个集装箱要装 上两艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi ,且w1+++..+wn≤c1+c2。装载问题要求确定是否有一个合理的装载方案可将这些集装箱装上这两艘轮
船。如果有,找出一种装载方案。
n=3,c1=c2=50;
w={0,10,40,40};
●例如,当n=3,c1=c2=50 , w={10 , 40 , 40}
将集装箱1和2装到第一艘轮船上,而将集装箱3装到第二艘轮船上。
●例如,当n=3,c1=c2=50,w={20 , 40 , 40}
则无法将这3个集装箱都装上轮船。
【装载方案】:
(1)首先将第一艘轮船尽可能装满 第一艘轮船的最优化(简单装载)问题
(2 )然后将剩余的集装箱装在第二艘轮船上。
【解表示】:
x[0....n]: 存放可行解
tw : 已经装载到第一艘轮船上的集装箱重量之和
rw : 剩余集装箱的重量和
i : 表示考虑的第i个集装箱
maxw: 当前装载方案中,最优方案中的最大装载的集装箱的重量总和
【求解步骤】:
( 1 )将尽可能多的集装箱装到第一 艘船上,得到解向量x
对于第i个集装箱的装载,扩展规则(与简单装载相同)是:
●如果tw+w[i]<=c1,说明第一艘船可以装下,就扩展左分枝, x[i]=1, tw=tw+w[i], rw=rw-w[i]
●如果tw+rw-w[i] >maxw,就扩展右分枝, x[i]=0, rw=rw-w[i]
( 2)累计第一艘装完后剩余的集装箱重量sum
( 3 )若sum<=c2,表示第二艘轮船可以装完,返回true ;否则表示第二艘轮船不能装完,返回false.
【求解算法】:
void dfs(int tw,int rw,int op[ ],int i);
tw : 选择的集装箱重量和
rw : 剩余集装箱的重量和
op :表示一个解,即一个选择方案
i : 表示考虑的集装箱i
【剪枝过程】:
初始,tw=0,rw=10+40+40=90, op为初始状态向量(0,0,0,0)(第一个0不用), i=1,如果第一个集装箱选择,tw=0+10<c1,满足条件可以选择,则rw=90-10=80,op[1]=1, 向量为(0,1,0,0),i=2,如果第二个集装箱选择,tw=10+40=50刚好等于c1,也可以选择,则rw=80-40=40, op[2]=1, 向量为(0,1,1,0),i=3,因为已经等于第一艘船的全部重量了所以肯定不能再继续拓展左分支,开始拓展右分支,即不选择第三个集装箱,那么tw不发生变化为50,rw=rw-w[i]=40-40=0, 向量为(0,1,1,0), 到此我们得到第一个可行解(0,1,1,0),maxw=50,然后回溯到i=2时的右分支,即第二个集装箱不被选择时,tw不发生变化为10,rw=rw-w[i]=80-40=40,向量为(0,1,0,0), i=3, 然后判断这个节点的左分支要不要扩展, tw+w[i]=10+40=50<=c1, 所以可以扩展,到此又得到一个可行解(0,1,0,1) ,此时的最优解最大载重量没有超过上一个可行解,所以不替换,然后回溯到i=3的右分支,此时tw+rw-w[i]=10+40-40=10<maxw, 所以右分支剪去,再回溯到I=1的右分支,即不选择第一个箱子tw=0,rw=rw-w[i]=90-10=80,向量为(0,0,0,0),考虑该节点是否扩展左分支,因为tw+w[i]=0+40=40<c1,所以可以扩展,扩展后,tw=0+40=40,rw=80-40=40,向量为(0,1,0,1),再看是否扩展下一个左分支,此时tw+w[i]=40+40=80>c1,所以剪枝,再看右分支,不被选择时,tw=40,rw=rw-w[i]=40-40=0, tw+rw-w[i]=40+0-40=0<maxw, 所以剪枝,回溯到上一个右分支,tw=0,rw=80-40=40,tw+rw-w[i]=40<maxw, 还是剪枝
【图解参考】
得到最优解后,计算sum
代码
#include <stdio.h>
#include <string.h>
#define MAXN 20
//全局变量
int w[]={0,10,40,40} ; //不用下标0的元素
int n=3,c1=50,c2=50;
int maxw=0; //存放第一艘船最优解的总重量,初值是0
int x[MAXN]; //存放第一艘船最优解向量,初值是0
void dfs(int tw,int rw,int op[],int i){
if(i>n){ //i>n说明所有的集装箱都判断完了
if(tw<=c1&&tw>maxw){ //满足这个条件即为最优解,则覆盖之前的解
maxw=tw;
for(int j=1;j<=n;j++){
x[j]=op[j]; //把最优解的值依次赋给最优解向量
}
}
}else{ //没到最后一个集装箱则需要判断当前这个集装箱是选还是不选
op[i]=1; //如果选择
if(tw+w[i]<=c1){ //判断剪枝条件
dfs(tw+w[i],rw-w[i],op,i+1);
}
op[i]=0; //如果不选
if(tw+rw-w[i]>maxw){ //判断剪枝条件
dfs(tw,rw-w[i],op,i+1);
}
}
}
bool solve(){
int sum=0;
for(int j=1;j<=n;j++){
if(x[j]==0){
sum+=w[j]; //将剩余箱子重量加起来
}
}
if(sum<=c2) return true; //第二艘船可以装下
else return false; //装不下
}
void displaySolution(int n){
for(int i=1;i<=n;i++){
if(x[i]==1){
printf("将第%d个集装箱装上第一艘船\n",i);
}else{
printf("将第%d个集装箱装上第二艘船\n",i);
}
}
}
int main(){
int op[MAXN];
memset(op,0,sizeof(op));
int rw;
for(int i=1;i<=n;i++){
rw+=w[i];
}
dfs(0,rw,op,1); ///求第一艘船的最优解
if(solve()){
printf("最优方案为:\n");
displaySolution(n);
}else{
printf("没有合适的方案");
}
}