问题描述
共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且 ∑ i = 1 n w i ≤ c 1 + c 2 \sum^{n}_{i=1}w_i \le c_1+c_2 ∑i=1nwi≤c1+c2。装载问题要求确定是否有一个合理的装载方案可将这些集装箱装上这2艘轮船。如果有,找出一种装载方案
问题分析
最优装载方案
- 首先将第一艘船尽可能装满
- 将再剩余的集装箱装上第二艘船
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最大
故该问题可用子集树解决
以3个集装箱装船为例
3个集装箱重量分别为3,7,5,两艘船的载重分别为8,7
解的三元组表示为(x1,x2,x3),xi∈{0,1}
解空间有8个三元组:111,110,101,011,100,010,001,000
不难看出101为唯一方案,对应第1、3个集装箱放在一号船上,第2个集装箱放在2号船上
子集树如下图所示,走左支意味着将当前结点放入一号轮船,走右支意味着当前结点不放入一号船。
当走到叶子结点时,更新当前最优解
剪枝条件:
- 进入左子树的条件是已装入一号船的重量加上当前结点的重量要小于等于一号轮船的载重
c1
- 进入右子树的条件不需要考虑轮船能否装下,为了更有效的剪枝,可以实时记录当前剩余集装箱的总重量,如果剩余重量加上已有重量仍小于当前最优重量,就不进入右子树
对于上述右子树的剪枝条件,是考虑在当前结点不放入一号船的情况下,让剩余所有集装箱全部放入一号船,如果仍不能达到当前最优解,说明这条路走到底不可能获得最优解,就不需要走了
代码
数据结构:
bestx[]
:记录最优解x[]
:记录当前解r
:记录剩余集装箱的总重量,初始值为所有集装箱的总重量cw
:记录当前已装船的重量bestw
:记录最大重量
代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#define MAX 100
int n, c1, c2;
int w[MAX];//集装箱重量
int bestx[MAX];//记录最优解
int x[MAX];//记录当前解
int r = 0; //记录剩余集装箱的总重量
int cw; //记录当前集装箱重量
int bestw = 0;//能放入一号船的最大重量
void backtrack(int i) {
if (i > n) {
for (int j = 1; j <= n; j++) {
bestx[j] = x[j];
}
bestw = cw;
return;
}
r -= w[i];
if (cw+w[i]<=c1) {//重量不超过船的载重,进入左子树搜索
cw += w[i];
x[i] = 1;
backtrack(i + 1);
cw -= w[i];
x[i] = 0;
}
if (r + cw >= bestw) {//进入右子树
x[i] = 0;
backtrack(i + 1);
}
r += w[i];
}
int main() {
FILE *fin = fopen("input.txt","r");
fscanf(fin, "%d %d %d", &n, &c1, &c2);
for (int i = 1; i <= n; i++) {
fscanf(fin, "%d", &w[i]);
r += w[i];
}
backtrack(1);
FILE *fout = fopen("output.txt","w");
if (bestw == 0) {
fprintf(fout, "no solution!\n");
return 0;
}
fprintf(fout, "%d\n", bestw);
for (int i = 1; i <= n; i++) {
fprintf(fout, "%d ", bestx[i]);
}
fclose(fin);
fclose(fout);
return 0;
}
代码说明
- 从文件中读取n、c1、c2、w[i],向文件中输出两行,第一行是第一艘船的最大装载重量,第二行是最优解;若不存在最优方案,则输出"no solution!"
- 进入下一层结点时需将
r
减去当前集装箱的重量,因为不论这个集装箱是否上船,r是剩余的集装箱的总重量,都不应该包含当前集装箱;退回上一层结点时应该给r
加回当前重量w[i] - 当
i>n
时不需要额外判断是否cw>=bestw
。因为在进入右子树时已经判断过走右支也可以获得最优解(剪枝条件)。
实例
input.txt
output.txt
经过计算,该解是一个最优解