本文专栏:研究生课程 点击查看系列文章
1. 问题描述
设有n个物品S和若干个容量为C的箱子B,n个物品的体积分别为{s1 ,s2 ,…, sn } ,且有 si ≤ C (1≤i≤n) ,装箱问题(packing problem)把所有物品分别装入箱子,求占用箱子数最少的装箱方案。
应用场景
装箱问题是大量实际问题的抽象。 例如,有若干货物要装入集装箱后运输,则在装入所有货物的条件下,占用集装箱最少的装箱方案就是装箱问题。类似的还有码头运输货物的装船方案等。
2. 问题分析
最优装箱方案可以通过把 n个物品划分为若干子集,每个子集的体积和小于C,然后取子集中元素个数最少的划分方案。
但是,这种划分可能的方案数有 2n 种,在多项式时间内不能够保证找到最优装箱方案。
大多数装箱问题的近似算法采用贪心策略,即在每个物品装箱时规定一种局部选择方法。
下面介绍4种不同的求解装箱问题的近似算法。
(1)首次适宜法: 首先将所有的箱子初始化为空,然后依次取每一个物品,将该物品装入第一个能容纳它的箱子中 。
(2)最适宜法: 首先将所有的箱子初始化为空,然后依次取每一个物品,将该物品装到能够容纳它并且目前最满的箱子中,使得该箱子装入物品后闲置空间最小。
(3)首次适宜降序法:将物品按体积从大到小排序,然后用首次适宜法装箱。
(4)最适宜降序法:将物品按体积从大到小排序,然后用最适宜法装箱。
3. 本文算法设计思想与算法描述
在本文中,采用随机适应算法(RF)来解决此问题。
算法设计思想:
该算法的大致思想是,首先根据物品的大小总和 Sum,除以每个箱子的容量,求出理论上的最优方案所用的箱子个数 B(即物品可切割),然后第一轮将所有的物品 S 随机分配到这 B 个箱子中。当某个物品 S[i] 无法装进当前选中的箱子,并且物品 S[i] 的体积大于箱子已用的体积,则将二者替换,即将 S[i] 箱子放入,将已放入的物品取出。直到任何物品都装不进任何一个箱子为止,第1轮结束。此时,第1轮分配的箱子所用的体积都大于容量的0.5倍(至多有1个箱子剩余空间小于容量的0.5倍)。然后第二轮继续执行此操作,将所有未放入箱子的物品求和,计算第2轮要用到的箱子个数B2 。
算法描述:
输入:物品个数n,每个物体的体积Si,箱子的容量C
输出:每个物品所放置的箱子编号,以及每个箱子已用的空间
输入数据
do{
遍历未装入箱子中的物品,令该类物品的体积之和S=sum(Si)
取第k轮箱子个数B=S/C
for( j=1;i<=B;j++ ){//遍历所有的物品
for() 二重循环,箱子增加
if 当前箱子的剩余空间大于物品体积
放入
else if 当前箱子空间小于物品体积,但是物品体积大于箱子的已用空间
将二者置换(或者将已放入的取出来,把当前遍历节点放入)
else if 如果箱子还有剩余
将当前节点放入下一个箱子中
}
}
while(C>S)
最后,剩下的物品,可以放入一个箱子中。
4. 算法复杂性分析与算法近似比
根据算法描述,可以看出本随机适应算法的时间代价主要由两部分组成:对长度为n的物品序列 S=( S1, S2 , … , Sn) 进行求和,需要 O(n) 阶时间代价;装箱过程需要由两层for循环实现,即使在最坏的情况下,物品都执行到最后一个箱子才放下,时间代价为O(n2 ) 。但是,虽然外层有个while循环,但该循环,大多数情况下往往只执行一、二轮循环,即可将所有物品装箱了。综合分析,算法时间复杂度为O(n2 )。
已知每个箱子的容量为C。对于n个物品 ( S1, S2 , … , Sn) ,假设物品体积求和为S1 ,则第一轮所用的基本箱子个数为B1=S1/C(向上取整)。则第一轮结束,保证了B1 个箱子中都有物品,并且其空闲空间<0.5C(因为空闲空间>0.5C的箱子所装的物品都被替换出来了),已用空间used>0.5C。如果巧了,恰好第一轮全部装完,则可能至多有一个箱子的空闲空间超过0.5C。
第二轮装箱之前,需要先求剩余物品之和:
即 S2 等于 S1 减去所有箱子已用的空间总和。而前面已经分析出,每个箱子已用空间used>0.5C,所以:
所以S2<S1 - 0.5S1 = 0.5S1 ,所以第二轮所用箱子数B2 =S2 / C < 0.5S1 /C=0.5 B1。最终,每轮所分配的基本箱子数,都小于前一轮的一半,即:B1=S1/C,B2<0.5 B1,B3<0.5 B2,……。最终,该算法所用的总箱子个数Bn = B1 + B2 + B3 +……≈2S1。所以所用箱子总数:
Bn < 2B1 (1)
假设最优装箱所用箱子数为m,显然有:
(2)
由(1)(2)求得算法近似比:
(3)
所以即使在最坏的情况下,该算法可以保证2的近似比。
然而,在多次试验中,往往经过一、二轮即可操作完成。假设经过三轮迭代,则Bn = B1 + B2 + B3 < B1 + 0.5B1 + 0.25B1 =1.75B1 ,所以一般情况下,近似比小于1.75。
5. 算法实现
根据前文的算法描述,采用C++高级语言实现了该算法。
// 装箱问题.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
#include "pch.h"
#include <iostream>
using namespace std;
/*初始数据定义*/
//全局箱子和物品个数定义
const int g_num = 100;
//定义物品Goods
struct Goods {
double volume = 0; //物品的体积
int flag = 0; //物品是否已放入箱子中:1-已放入 0-未放入
int boxNum = -1; //物品放入的箱子编号
}Goods[g_num];
//定义箱子Box
struct Box {
double volume = 0; //箱子的容量
double usedVolume = 0; //箱子已用容量
int goodsNum = 0; //当前箱子存放的物品个数
int goods[100] = { 0 }; //记录存放的箱子编号
}Box[g_num];
//输入箱子的初始数据
void initBox() {
double volume;
cout << "请输入箱子的容量:" ;
cin >> volume;
for (int i = 0; i < g_num; i++)
{
Box[i].volume = volume;
}
}
//打印箱子的数据
void printBox() {
int num = 0;
cout << "你想打印前多少个箱子的数据?(小于 " << g_num << " ):";
cin >> num;
cout << "编号\t\t容量\t\t已用容量\t物品个数\t物品编号(大小)" << endl;
for (int i = 0; i < num; i++)
{
//打印基本信息
cout << i << "\t\t" << Box[i].volume << "\t\t" << Box[i].usedVolume << "\t\t" << Box[i].goodsNum << "\t\t";
for (int j = 0; j < Box[i].goodsNum; j++)
{
//打印当前箱子的物品编号信息
cout << Box[i].goods[j] << "(" << Goods[Box[i].goods[j]].volume << ");";
}
//最后来个换行
cout << endl;
}
cout << endl;
}
//只打印已用的箱子
void printBoxUsed() {
cout << "已用箱子的详细信息如下:" << endl;
cout << "编号\t\t容量\t\t已用容量\t物品个数\t物品编号(大小)" << endl;
for (int i = 0; i < g_num; i++)
{
if (Box[i].usedVolume>0) {
//打印基本信息
cout << i << "\t\t" << Box[i].volume << "\t\t" << Box[i].usedVolume << "\t\t" << Box[i].goodsNum << "\t\t";
for (int j = 0; j < Box[i].goodsNum; j++)
{
//打印当前箱子的物品编号信息
cout << Box[i].goods[j] << "(" << Goods[Box[i].goods[j]].volume << ");";
}
//最后来个换行
cout << endl;
}
}
cout << endl;
}
// 输入物品的数据
void initGoods() {
int num;
cout << "请输入物品的个数(小于 " << g_num << " ):";
cin >> num;
cout << "请输入 " << num << " 个物品的大小(以空格间隔):" << endl;
for (int i = 0; i < num; i++)
{
cin >> Goods[i].volume;
}
}
// 打印物品的数据
void printGoods() {
cout << "所有物品的信息如下:" << endl;
cout << "编号\t\t大小\t\t已入箱?\t\t箱子编号" << endl;
for (int i = 0; Goods[i].volume > 0; i++)
{
cout << i << "\t\t" << Goods[i].volume << "\t\t" << Goods[i].flag << "\t\t" << Goods[i].boxNum << endl;
}
cout << endl;
}
/**
bNum 当前指定的分类的箱子个数(上限)
i 当前物品的编号
**/
void fenpei(int bNum,int i) {
//只有未放入箱子中的节点才算在内
if (Goods[i].flag == 0) {
for (int j = 0; j < bNum; j++)
{
//如果当前箱子剩余空间(总量减去已用)大于物品体积,可放下
if (Box[j].volume - Box[j].usedVolume >= Goods[i].volume) {
//物品状态修改
Goods[i].flag = 1;
Goods[i].boxNum = j;
//箱子容量增加、物品个数增加、物品数组增加
Box[j].usedVolume += Goods[i].volume;
Box[j].goods[Box[j].goodsNum] = i;
Box[j].goodsNum++;
//跳出内层循环
return;
}
//否则,当前箱子空间小于物品体积,如果:物品体积大于箱子的已用空间
else if (Goods[i].volume > Box[j].usedVolume) {
//置换
//新物品状态修改
Goods[i].flag = 1;
Goods[i].boxNum = j;
//取出物品状态修改
//设置几个临时值
int tempGoodsNum = Box[j].goodsNum;//当前阶段换出来多少个物品
int tempGoods[g_num] = { 0 };//存放这些换出来的物品的序号
for (int t = 0; t < Box[j].goodsNum; t++)
{
//cout <<"换出第:" << Box[j].goods[t] <<"号物品" << endl;
//替换的前面的节点,重新分配一下
tempGoods[t] = Box[j].goods[t];
//修改原物品的状态
Goods[Box[j].goods[t]].flag = 0;
Goods[Box[j].goods[t]].boxNum = -1;
//重置箱子中存放的物品数组编号
Box[j].goods[t] = 0;
}
//箱子容量、物品个数、物品数组修改
Box[j].goodsNum = 1;
Box[j].usedVolume = Goods[i].volume;
Box[j].goods[0] = i;
//替换出来的,进行迭代,重新分配箱子
for (int k = 0; k < tempGoodsNum; k++)
{
//cout << "将换出的:" << tempGoods[k] << "递归" << endl;
fenpei(bNum, tempGoods[k]);
}
//跳出内层循环
return;
}
}
}
return;
}
// 执行操作
void zhuangxiang() {
//数据初始化
double sum = 0;
int bNum = 0;//所用的箱子总数
//求和,遍历所有未装入箱子中的结点
for (int i=0; Goods[i].volume > 0; i++)
{
if (Goods[i].flag == 0)
sum += Goods[i].volume;
}
//开始循环
do
{
//设置本轮所用箱子个数,向上取整
int bnum = ceil(sum / Box[0].volume);
bNum = bNum + bnum;
//cout << "本轮需要用箱子个数:"<< bnum <<",截止目前,已用箱子:"<< bNum << endl;
//核心算法部分
//遍历所有的未放入箱子中的物品
for (int i = 0; Goods[i].volume > 0; i++)
{
//只有未放入箱子中的节点才算在内
if (Goods[i].flag == 0) {
fenpei(bNum, i);
}
}
sum = 0;
//求和,遍历所有未装入箱子中的结点
for (int i = 0; Goods[i].volume > 0; i++)
{
if (Goods[i].flag == 0)
sum += Goods[i].volume;
}
//打印测试
//printBoxUsed();
//printGoods();
} while (sum>Box[0].volume);
//如果还有剩余,剩下的只能放进一个箱子里,增加一个箱子,为其分配
if (sum != 0) {
++bNum;
for (int i = 0; Goods[i].volume > 0; i++)
{
//只有未放入箱子中的节点才算在内
if (Goods[i].flag == 0) {
fenpei(bNum, i);
}
}
}
cout << "\n装箱结束。用的箱子总数:" << bNum << endl;
//打印测试
printBoxUsed();
printGoods();
}
int main()
{
while (true)
{
initBox();
initGoods();
zhuangxiang();
}
}
6. 测试结果
输入:
1 2 3 7 8 9 1 2 3 5
输出:
输入:
1 2 3 4 5 6 7 8 9 9 8 7 6 5 4 3 2 1 1 2 3 4 5 6 7
输出:
以上。