【课程·研】算法 | 装箱问题(近似算法)

本文专栏:研究生课程  点击查看系列文章

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。

第二轮装箱之前,需要先求剩余物品之和:

image-20210216160141563

即 S2 等于 S1 减去所有箱子已用的空间总和。而前面已经分析出,每个箱子已用空间used>0.5C,所以:

image-20210216160230954

所以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,显然有:

image-20210216160434267(2)

由(1)(2)求得算法近似比:

image-20210216160503584(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

输出:

image-20201222171002081

输入:

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

输出:

image-20201222171124642

以上。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拾年之璐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值