一、实验目的: 理解并掌握回溯法与分支限界法的联系与区别,学会构造不同问题的解空间树,用上述两种算法解决装载问题。 | ||||||||
问题描述:有n个集装箱要装上2艘载重量分别为C1和C2的轮船,其中集装箱i的重量为wi,且∑wi≦C1+C2。请分别用回溯法跟分支限界法确定是否有一个合理的装在方案可将这些集装箱装上这2艘轮船。 输入:多组测试数据。每组测试数据包括两行:第一行输入集装箱数目n(n<1000),以及两艘轮船的载重C1和C2;第二行输入n个整数,表示每个集装箱的重量。 输出:如果存在合理装载方案,输出第一艘轮船的最大装载重量;否则输出“NO”。 程序代码:(不允许粘图,重点语句加上注释) 回溯法: #include <iostream> using namespace std; int x[100]; int bestx[100]; int w[100]; int bestw; int r,n,c1,c2,cw; void ShuRu() { cout<<"请输入集装箱的数量:"<<endl; cin>>n; cout<<"请输入轮船1,2的载重量:"<<endl; cin>>c1>>c2; cout<<"请输入每个集装箱的重量:"<<endl; for(int i=1;i<=n;i++) { cin>>w[i]; } } void ChushiH () { r=0; cw=0; bestw=0; for(int i=1;i<=n;i++) {r=r+w[i];} } void HuiSu (int i) { if(i>n) //判断是否达到叶子节点 { if(cw>bestw) { for(int j=1;j<=n;j++) { bestx[j]=x[j]; } bestw=cw; } } r=r-w[i]; if(cw+w[i]<=c1) //判断该集装箱到底放不放 { x[i]=1; cw=cw+w[i]; HuiSu(i+1); //当节点i的子树延伸结束时要返回i节点 x[i]=0; cw=cw-w[i]; } if(cw+r>bestw) //判断先不放该集装箱后是否还有可行解 { x[i]=0; HuiSu(i+1); } r=r+w[i];//当节点i的子树延伸结束时要返回i节点 } void ShuChu () {int i; int c2w=0; for(int i=1;i<=n;i++) { if(bestx[i]==0) { c2w=c2w+w[i]; } } if(c2w>c2) cout<<"装不下啊!"<<endl; else { cout<<"轮船1装入的集装箱为:"; for( i=1;i<=n;i++) { if(bestx[i]==1) cout<<i<<" "; } cout<<endl; cout<<"轮船2装入的集装箱为:"; for(i=1;i<=n;i++) { if(bestx[i]!=1) cout<<i<<" "; } } } int main() { ShuRu(); ChushiH(); HuiSu(1); ShuChu(); return 0; } 分支限界法: #include <stdio.h> #include <stdlib.h> #include<cstring> #include <bits/stdc++.h> using namespace std; typedef struct Q { Q *parent; int lchild; int weight; }Q; int n,c,bestw; int bestx[100],w[100]; void InPut() { scanf("%d %d", &n, &c); for(int i = 1; i <= n; ++i) scanf("%d", &w[i]); for(int i = 1; i <= n; ++i) printf("%d ", w[i]); cout << endl; printf("输入结束\n"); } //Q *&bestE 的原因是 首先bestE是个地址, 其次引用为了赋值使用, 后边for循环中用到 void EnQueue(queue<Q *> &q, int wt, int i, Q *E, Q *&bestE, int ch) { if(i == n) { if(wt == bestw) { bestE = E; bestx[n] = ch; return; } } Q *b; b = new Q; b->weight = wt; b->lchild = ch; b->parent = E; q.push(b); } int MaxLoading() { queue<Q *>q; q.push(0); int i = 1; int Ew = 0, r = 0; bestw = 0; for(int j = 2; j <= n; ++j) r += w[j]; Q *E, *bestE; //bestE的作用是:结束while循环后,bestE指向最优解的叶子节点,然后通过bestE->parent找到装入了哪些物品。 E = new Q; //E这里作为一个中间量,连接parent和child E = 0; //赋0是因为树的根的值是0,while刚开始的时候其代表root while(true) { int wt = Ew + w[i]; if(wt <= c) { if(wt > bestw) //提前更新bestW,注意更新条件 bestw = wt; EnQueue(q, wt, i, E, bestE, 1); } if(Ew + r >= bestw) //右儿子剪枝 { EnQueue(q, Ew, i, E, bestE, 0); } E = q.front(); q.pop(); if(!E) //如果取得的数是0,代表该处理下一层 { if(q.empty()) //如果队列为空,表示该循环结束了 break; q.push(0); //如果队列中还有数据,表示循环还没结束。在该层的末尾加一个0标识符 E = q.front(); q.pop(); i++; //下一层走起 r -= w[i]; //计算剩余的重量 } Ew = E->weight; //不要忘记更新最新节点的值 } for(int j = n - 1; j > 0; --j) { bestx[j] = bestE->lchild; bestE = bestE->parent; } } void OutPut() { printf("最优装载量为 %d\n", bestw); printf("装载的物品为 \n"); for(int i = 1; i <= n; ++i) if(bestx[i] == 1) printf("%d ", i); } int main() { InPut(); MaxLoading(); OutPut(); } 程序测试及运行结果: 1.回溯法 2.分支限界法 | ||||||||
分析与讨论: 1.问题的思路 在问题的边带权的解空间树中进行广度优先搜索再找一个叶结点使其对应路径的权最小(最大)。当搜索到达一个扩展结点时,一次性扩展它的所有孩子然后将满足约束条件且最小耗费函数£目标函数限界的孩子,插入活结点表中最后从活结点表中取下一结点同样扩展,直到找到所需的解或活动结点表为空为止。由此可知,装载问题等价于特殊的0-1背包问题。 2.回溯法和分支限界法的不同 (1)求解目标不同 回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。 (2)搜索方式不同 回溯法以深度优先的方式搜索解空间树,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树。 其中分支限界法包括队列式(FIFO)分支限界法(即将活结点表组织成一个队列,按照先进先出(FIFO)原则选取下一个结点为扩展结点。)和优先队列式分支限界法(即将活结点表组织成一个优先队列,按照规定的优先级选取优先级最高的结点成为当前扩展结点。) |