1. 求解Bin Packing问题的近似算法
对于本文所研究的基于近似算法的装箱问题,给出First fit、Next fit、Best fit和Worse fit这四种策略,下面将对它们进行详细阐述。
1.1 First fit策略
First fit策略的精髓在于寻找当前物品所能存放的第一个箱子。First fit以物品为视角,在处理当前物品i时,首先寻找第一个还没有装满的箱子j。若箱子j可以装下物品i,则将物品i装入箱子j后,继续处理下一个物品i+1;若箱子j装不下物品i,则考虑下一个箱子j+1,直至为物品i找到能存放的箱子。后续物品依上述描述继续处理。当然,若某个箱子已经装满,则该箱子后续就不能再装入任何物品,即将其彻底关闭;若当前已经装入物品的箱子都不能装下物品i,则会将物品i放入一个未被使用过的新箱子。
算法1. BinPackingFirstFit(S,n,B)
输入:物品集合S,物品个数n,箱子集合B
输出:使用的箱子个数
1. initialize all b[j] as 0
2. for i←1 to n do
3. for j←1 to n do
4. if b[j]+s[i]≤1 then
5. B[i]←j
6. b[j]←b[j]+ s[i]
7. break
8 j←1
9. while(b[j] != 0) do
10. j←j+1
11 return j-1
其中,s[i]表示第i个物品的大小,b[j]表示箱子j所装入物品的容量,B[i]表示第i个物品应该装入的箱子号。
在算法1中,步骤1用于将所有箱子当前装入物品的体积初始化为0,即每个箱子都还没有装入任何物品。步骤2~8是该算法的核心部分,以物品为视角开始逐个处理物品,将它们放入当前所能放入的编号最靠前的箱子。步骤9~11用于获取所有物品装入完毕后,用了多少个箱子。因为步骤1的时间复杂度为O(n),步骤2~7的时间复杂度为O(n²),步骤8~10的时间复杂度为O(n),所以算法1的时间复杂度为O(n²)。
1.2 Next fit策略
Next fit策略的精髓在于为当前待装入物品的箱子寻找可以装入的物品。不同于其它三种策略,Next fit策略以箱子为视角,在处理箱子j时,首先判断当前待装入的物品i能否装入箱子j。若能装入,则继续处理下一个物品i+1;若不能装入,则关闭箱子j,转而打开箱子j+1。后续物品依上述描述继续处理。很明显,Next fit策略的速度极快,它省去了很多判断的步骤,但是它的性能同时也是极差的。
算法2. BinPackingNextFit(S,n,B)
输入:物品集合S,物品个数n,箱子集合B
输出:使用的箱子个数
1. initialize all b[j] as 0,m←1
2. for i←1 to n do
3. if b[m]+s[i]≤1 then
4. B[i]←m
5. b[m]←b[m]+ s[i]
6. else
7. m←m+1
8 B[i]←m
9. b[m]←s[i]
10. return m
其中,m表示当前打开的箱子号,s[i]表示第i个物品的大小,b[j]表示箱子j所装入物品的容量,B[i]表示第i个物品应该装入的箱子号。
在算法2中,步骤1用于将所有箱子当前装入物品的体积初始化为0,即每个箱子都还没有装入任何物品。步骤2~9是该算法的核心部分,以箱子为视角开始逐个处理物品,每个箱子在不能装入当前物品后就关闭掉不再打开,转而用新箱子继续装入物品。因为步骤1的时间复杂度为O(n),步骤2~9的时间复杂度为O(n),所以算法2的时间复杂度为O(n)。
1.3 Best fit策略
Best fit策略的精髓在于从能存放当前物品的箱子中挑选剩余容量最小的那个箱子。Best fit策略以物品为视角,在处理当前物品i时,首先在所有已经装入物品但还没有装满的箱子中寻找能装入物品i的箱子。若存在上述的箱子,则从这些箱子中寻找剩余容量最小的那个箱子,将物品放入其中;若不存在上述的箱子,则将物品i放入一个新箱子中。后续物品依上述描述继续处理。很明显,Best fit策略看起来更加合理,它使得每个箱子都尽可能地发挥最大的作用。
算法3. BinPackingBestFit(S,n,B)
输入:物品集合S,物品个数n,箱子集合B
输出:使用的箱子个数
1. initialize all b[j] as 0,m←1
2. for i←1 to n do
3. left←2,a←1
4. for j←1 to m do
5. if b[j]+s[i]≤1 then
6. if 1-b[j]≤left then
7. left←1-b[j]
8 a←j
9. if left==2 then
10. m←m+1
11. b[m]←s[i]
12. B[i]←m
13. else
14 b[a]←b[a]+s[i]
15. B[i]←a
16 return m
其中,m表示当前已经开启的箱子总数,a表示当前选择的将i号物品放入的箱子的编号,left表示1到m号箱子中能装下i号物品的那个箱子的最大剩余容量,s[i]表示第i个物品的大小,b[j]表示箱子j所装入物品的容量,B[i]表示第i个物品应该装入的箱子号。
在算法3中,步骤1用于将所有箱子当前装入物品的体积初始化为0,即每个箱子都还没有装入任何物品。步骤2~15是该算法的核心部分,以物品为视角,为当前待存放的物品寻找能放入且剩余容量最小的箱子。
1.4 Worse fit策略
Worse fit策略的精髓在于从能存放当前物品的箱子中挑选剩余容量最大的那个箱子。Worse fit策略以物品为视角,在处理当前物品i时,首先在所有已经装入物品但还没有装满的箱子中寻找能装入物品i的箱子。若存在上述的箱子,则从这些箱子中寻找剩余容量最大的那个箱子,将物品放入其中;若不存在上述的箱子,则将物品i放入一个新箱子中。后续物品依上述描述继续处理。很明显,Worse fit策略在能装入物品的箱子中选择箱子时,与Best fit的选择标准恰好相反。
算法4. BinPackingWorseFit(S,n,B)
输入:物品集合S,物品个数n,箱子集合B
输出:使用的箱子个数
1. initialize all b[j] as 0,m←1
2. for i←1 to n do
3. left←-1,a←1
4. for j←1 to m do
5. if b[j]+s[i]≤1 then
6. if 1-b[j]≥left then
7. left←1-b[j]
8 a←j
9. if left==-1 then
10. m←m+1
11. b[m]←s[i]
12. B[i]←m
13. else
14 b[a]←b[a]+s[i]
15. B[i]←a
16 return m
其中,m表示当前已经开启的箱子总数,a表示当前选择的将i号物品放入的箱子的编号,left表示1到m号箱子中能装下i号物品的那个箱子的最小剩余容量,s[i]表示第i个物品的大小,b[j]表示箱子j所装入物品的容量,B[i]表示第i个物品应该装入的箱子号。
在算法4中,步骤1用于将所有箱子当前装入物品的体积初始化为0,即每个箱子都还没有装入任何物品。步骤2~15是该算法的核心部分,以物品为视角,为当前待存放的物品寻找能放入且剩余容量最大的箱子。
2. 实例计算与分析
本文用到的实例来自课本,其中n=8,S={0.4,0.2,0.5,0.8,0.2,0.2,0.4,0.3}。根据代码运行所得结果,并自行模拟装箱过程,将给出四种算法对应代码的图解过程。
2.1 First fit策略
算法1对应代码的图解过程如图2.1和2.2所示,算法1对应代码的运行结果图如图2.3所示。
在编写算法1对应的代码时,由于C语言中数组的下标是从0开始的,所以在描述物品大小的数组s[]中,将下标为0处的数据设定为固定值0保持不变,转而从下标为1处开始计数。同时还要注意数组s[]的大小要比实际物品的数量大1,用以防止产生数组下标的越界问题。并且,在后续的for循环中,要将变量i和j的起始值设定为1,终止值设定为n。而在打印输出时,对浮点数进行特殊处理,只保留一位小数,这样在代码运行结果图中,可以从数值上更加直观且清晰地表现出完整详细的装箱过程。
2.2 Next fit策略
算法2对应代码的图解过程如图2.4和2.5所示,算法2对应代码的运行结果图如图2.6所示。
在编写算法2对应的代码时,除了要注意算法1中涉及到的关于数组下标越界和for循环变量初始值和终止值设定的问题,还需要注意无需再设计一个while循环用以获取使用的箱子数,程序中定义的整型变量m在经过for循环和if-else的分支选择变换后,最终会等于实际使用的箱子数。
2.3 Best fit策略
算法3对应代码的图解过程如图2.7和图2.8所示,算法3对应代码的运行结果图如图2.9所示。
在编写算法3对应的代码时,为了替物品i寻找到当前能够装入物品且剩余容量最小的箱子,定义一个名为left的浮点型变量,并将其初始值设定为2(此处只要设定left的初始值大于或等于1,并将其放在外层for循环与内层for循环之间进行。这样就可以保证每次进入外层for循环的时候,left值被重置为初始值,而在每次进入内层for循环后,保证内部逻辑设想正常实现)。而在后续通过判断left的值是否改变来选择物品应该放入的箱子编号。
2.4 Worse fit策略
算法4对应代码的图解过程如图2.10和图2.11所示,算法4对应代码的运行结果图如图2.12所示。
在编写算法4对应的代码时,大体结构上与算法3对应的代码相同,只需要注意将浮点型变量left的初始值设定为-1(原因同算法3),且注意后续1-b[j]与left的值比较时将大于等于改为小于等于(算法3是为物品寻找能放下且剩余容量最小的箱子,而算法4是为物品寻找能放下且剩余容量最大的箱子),还有通过判断left的值是否改变来选择物品应该放入的箱子编号时,将判断的值从2改成-1即可即可。
3 结论与展望
本文研究利用近似算法求解装箱问题,给出了First fit、Next fit、Best fit和Worse fit这四种策略。不难看出,它们都用到了贪心算法的思想,即从局部来思考问题,做出从当前看来最好的选择,而非从整体上考虑全局的最优解。
对于First fit策略,它的特点是优先考虑序号靠前的且能放入当前物品的箱子,只要能找到可以存放当前的物品的箱子,就立即装入,而不考虑后续情况。对于Next fit策略,它的特点是速度快,为箱子找物品。只要当前箱子无法存入当前待存入的物品,就会打开新的箱子,但这样显然会容易造成空间浪费的问题。对于Best fit策略,它是将当前物品放入优先放入能存入的且剩余容量最小的箱子,它看起对于空间的利用看起来是更加合理的。对于Worse fit策略,与Best fit策略相反,它是将当前物品优先放入能存入的且剩余容量最大的箱子,这样也容易造成空间浪费的问题。
本文在对四种算法对应的代码进行编写时,需要关注以下几个方面:(1)数组下标的越界问题;(2)for循环中变量的初始值和终止值的选择问题;(3)由于实例涉及到小数,所以还要关注浮点数计算时的误差问题。此外,在编写算法3和算法4对应的代码时,由于一开始将变量left的类型定义为int,所以运行的结果是错的,最终经过调试、对比和逐个排查后才发现应该将其定义为float类型,这样后续变量left的值才能成功被改写。
对于本文涉及到的四种算法,都可以首先对待装入的物品的体积进行排序,然后再进行相应的后续步骤,可以得到更优质的解。当然,增加这一排序的步骤势必会增加算法的运行时间。诚然,本文对于装箱问题的研究还仅局限于一维层面,即只考虑物品体积这一个因素。为此,希望通过对这四种策略在解决一维装箱问题上的的研究,后续有机会进一步研究二维、三维甚至多维装箱问题。而对于求解装箱问题的算法,还有的学者提出了调和算法、并行算法、随机算法、遗传算法和混合启发式算法等诸多算法,本文在撰写时也只是对上述算法的相关论文进行了了解性的阅读,限于作者本身的水平原因,并未对它们进行深入且具体的研究,希望后续有机会对上述众多算法进行深入研究,并对比它们各自的优劣。
4.源代码
本文代码链接为:https://github.com/qigezongdui/suanfa 选择BinPacking.c文件即可。