算法 & 數據結構——收納箱算法???
. 最近工作上有一個需求,需要將圖片打包成圖集,以便於讓資源更緊湊,利用率更高,提升性能,遊戲行內的同誌應該很熟練這個操作.通常我們需要用一個app來完成這項工作,最出名的莫過於Texture Packer。
Texture Packer官方示意圖
. 最早接觸到這一概念的時候,我還是一個學生,當時玩的《暗黑魔破壞神2》,它有一個背包系統,這個背包系統跟現在大多數遊戲都不一樣,它的道具存放並非等大小,比如長劍比匕首長,斧頭比長劍寬,因此在擺放道具時,不能亂放,不然背包就不夠用。
暗黑破壞神2背包截圖
. 這是一個很有意思的設計,但現在基本上絕跡了,以至於我想到遊戲中的背包就會不禁想起暗黑破壞神2的背包系統,我在《天龍八部》遊戲中,第一次發現了背包自動整理這一項功能,但是它的背包道具都是等大小的,所以平平無奇,但卻不由讓我想到《暗黑破壞神2》的背包自動整理,它必然會涉及到一個最優排列組合算法。這個疑惑一直困擾了我很多年,它到底是怎麽實現的?直到上周,我重新打開了《暗黑破壞神2》這款遊戲,發現它根本沒有這個功能,童年幻想破滅~
. 雖然童年幻想破滅了,但成年夢想得跟上,正好工作有這麽一個讓我去實現的機會,於是我嘗試去思考算法,起初想到經典的《背包算法》,仔細研究後發現不太適用,於是放棄。不得不說,廁所是一個很適合思考的地方,因為後來的實現方案是我在蹲廁所的時候突然就想出來了。有沒有科學上的解釋?
收納箱算法?
. 這個算法跟我們日常使用收納箱的思路很相似,起初收納箱是空的,它只有一個存放空間,但是很大.之後我們逐個物品往裏面放,每次放進去一個物品,原來的空間都會被拆分成兩個,最後空間越來越小,直到不能裝下剩下的物品,或者物品放完了,這一過程就結束了.
收納箱示意圖
. 上圖清晰描述了收納箱的存放過程,綠色是收納箱,黃色是物品,起初收納箱是空的,只有一個空間,放進物品後,空間被拆分了,這一步是關鍵,因為放進一個物品會有兩種拆分策略,紅色線和棕色線分表分表顯示了兩種拆分策略,如果按紅色線拆分,則會產生一個很小的空間和一個很大的空間,如果按棕色線拆分,則產生兩個大小均勻的空間。這兩種策略對應不同的物品有奇效。每次放進去一個物品,收納箱的一處空間就會被占用,而算法只考慮沒有被占用的空間即可。
. 接下來就是逐個物品往收納箱裏放,這一步也是關鍵,因為每次放進一個物品,空間都會被劃分,這個劃分依據是物品的大小,如果先放進去一個很小的物品,比如1x1大小的物品,那麽空間無論怎麽分,都會產生一個很狹窄的空間,這個狹窄的空間很可能存不下後續的任何物品,那麽這個空間就浪費了,因此在此之前,先對所有物品進行一個排序,這個排序可以按物品的面積,物品的寬度,物品的高度,物品的最長的邊,等等。這些排列策略也會影響最終的排列組合。
改進
空間被浪費
. 上圖是一個空間被浪費的例子,因為上文描述的算法需要已知收納箱大小,比如已知收納箱512x512大小,如果物品總面積超出這個大小,則裝不下,如果物品遠小於這個大小,則浪費空間,所以算法能動態計算大小,那就再好不過了。不過謝天謝地,很容易就可以搞定這個問題,我們只需要提前計算出單個物品最大需要的大小,用這個大小作為收納箱的大小就可以了。
自動適應
. 優化過後,已經提高了不少空間利用率,第一個收納箱裝進了最大的物品,後面的收納箱采用較小的尺寸繼續裝剩下的物品,直到全部裝完為止。但是這裏產生了一個新問題,會生成多個收納箱,如果多個收納箱都很小,那資源復用率就下降了,所以還需要進一步優化,盡可能提高資源復用率。比如兩個128x128可以合並成一個256x256,兩個256x256可以合並成一個512x512。
. 思路是這樣的,設定一個打包級別,當這個級別的打包數量達到一個值,就將這個級別提升一級,再重新打包,直到所有級別的收納箱都沒有超出限制。比如,128x128 2,256x256 2,512x512 3 分別代表三個不同的級別,他們是遞增關系,當128x128的收納箱達到2個的時候,說明它需要提升一個級別重新打包,於是級別提升到256x256,依次類推。
最終版本
以上結果是采用按棕色線拆分空間,按物品最長邊排序,及以下打包級別:
{ 128, 128, 1 },
{ 256, 256, 2 },
{ 512, 512, 3 },
{ 1024, 1024, 4 },
{ 2048, 2048, 100 },
動態圖
// storage_box.h
#pragma once
#include <list>
#include <tuple>
#include <array>
#include <vector>
#include <cassert>
#include <algorithm>
using iint = int;
using uint = unsigned int;
class StorageBox {
public:
struct Item {
uint i;
uint w;
uint h;
uint GetV() const
{
return std::max(w, h);
}
};
struct ResultItem {
uint i;
uint x;
uint y;
uint w;
uint h;
ResultItem(): i((uint)~0)
{ }
uint GetV() const
{
return w * h;
}
bool IsReady() const
{
return i != (uint)~0;
}
bool IsContains(const Item & item) const
{
return w >= item.w && h >= item.h;
}
bool AddItem(const Item & item, ResultItem * out0, ResultItem * out1)
{
if (!IsContains(item))
{
return false;
}
auto nx = x + item.w;
auto ny = y + item.h;
auto s0 = (w - item.w) * item.h;
auto s1 = (h - item.h) * w;
auto s2 = (w - item.w) * h;
auto s3 = (h - item.h) * item.w;
// 兩種切分策略:
// 按最大面積切分
// 按均勻面積切分
//if (std::max(s0, s1) > std::max(s2, s3))
if (std::max(s0, s1) - std::min(s0, s1) < std::max(s2, s3) - std::min(s2, s3))
{
out0->x = nx;
out0->y = y;
out0->w = w - item.w;
out0->h = item.h;
out1->x = x;
out1->y = ny;
out1->w = w;
out1->h = h - item.h;
}
else
{
out0->x = nx;
out0->y = y;
out0->w = w - item.w;
out0->h = h;
out1->x = x;
out1->y = ny;
out1->w = item.w;
out1->h = h - item.h;
}
w = item.w;
h = item.h;
i = item.i;
return true;
}
};
struct ResultBox {
uint level;
std::vector<ResultItem> items;
};
// 打包級別
static constexpr iint PACK_LEVEL[][3] = {
{ 128, 128, 1 },
{ 256, 256, 2 },
{ 512, 512, 3 },
{ 1024, 1024, 4 },
{ 2048, 2048, 100 },
};
std::vector<ResultBox> Pack(std::vector<Item> items);
private:
// 確定使用哪個級別打包圖集
uint CheckLevel(const Item & item);
uint CheckLevel(const std::vector<Item> & items);
// 根據圖片的V值進行排序
void SortItems(std::vector<Item> & items);
void SortItems(std::vector<ResultItem> & items);
uint CheckLimit(
std::vector<ResultBox>::iterator cur,
std::vector<ResultBox>::iterator end);
// 打包
ResultBox PackBox(
std::vector<Item> & items, uint level);
void PackBox(
std::vector<Item> & items, uint level, std::vector<ResultBox> & retBoxs);
// 解包
void UnpackBox(std::vector<Item> & items,
std::vector<ResultBox>::iterator cur,
std::vector<ResultBox>::iterator end);
};
// storage_box.cpp
#include "storage_box.h"
std::vector<StorageBox::ResultBox> StorageBox::Pack(std::vector<Item> items)
{
std::vector<ResultBox> retBoxs;
PackBox(items, 0, retBoxs);
for (auto it = retBoxs.begin(); it != retBoxs.end();)
{
auto level = it->level;
auto limit = StorageBox::PACK_LEVEL[level][2];
auto count = CheckLimit(it, retBoxs.end());
if (count > limit)
{
UnpackBox(items, it, retBoxs.end());
retBoxs.erase(it, retBoxs.end());
PackBox(items, level+1, retBoxs);
it = retBoxs.begin();
}
else
{
++it;
}
}
return retBoxs;
}
uint StorageBox::CheckLevel(const Item & item)
{
for (auto i = 0; i != sizeof(PACK_LEVEL) / sizeof(PACK_LEVEL[0]); ++i)
{
if ((uint)PACK_LEVEL[i][0] >= item.w &&
(uint)PACK_LEVEL[i][1] >= item.h)
{
return i;
}
}
return (uint)~0;
}
uint StorageBox::CheckLevel(const std::vector<Item>& items)
{
uint level = 0;
for (auto & item : items)
{
auto i = CheckLevel(item);
assert((uint)~0 != i);
if (i > level) { level = i; }
}
return level;
}
void StorageBox::SortItems(std::vector<Item>& items)
{
std::sort(items.begin(), items.end(), [](const Item & item0, const Item & item1)
{
return item0.GetV() > item1.GetV();
});
}
void StorageBox::SortItems(std::vector<ResultItem> & items)
{
std::sort(items.begin(), items.end(), [](const ResultItem & item0, const ResultItem & item1)
{
return item0.GetV() < item1.GetV();
});
}
uint StorageBox::CheckLimit(std::vector<ResultBox>::iterator cur, std::vector<ResultBox>::iterator end)
{
uint count = 0;
uint level = cur->level;
cur = std::next(cur);
while (cur != end && cur->level == level)
{
++cur; ++count;
}
return count;
}
StorageBox::ResultBox StorageBox::PackBox(std::vector<Item> & items, uint level)
{
ResultBox retBox;
retBox.level = level;
std::vector<ResultItem> retItems;
ResultItem retItem;
retItem.i = (uint)~0;
retItem.x = 0;
retItem.y = 0;
retItem.w = PACK_LEVEL[level][0];
retItem.h = PACK_LEVEL[level][1];
retItems.push_back(retItem);
auto itemIndex = 0u;
ResultItem retItem0;
ResultItem retItem1;
while (itemIndex != items.size())
{
auto isNewItem = false;
for (auto it = retItems.begin(); it != retItems.end(); ++it)
{
if (it->AddItem(items.at(itemIndex), &retItem0, &retItem1))
{
isNewItem = true;
// 添加到收納箱
retBox.items.push_back(*it);
retItems.erase(it);
// 新增2個新收納箱
retItems.push_back(retItem0);
retItems.push_back(retItem1);
SortItems(retItems);
// 刪除物品
items.erase(items.begin() + itemIndex);
break;
}
}
if (!isNewItem) { ++itemIndex; }
}
return retBox;
}
void StorageBox::PackBox(std::vector<Item>& items, uint level, std::vector<ResultBox>& retBoxs)
{
SortItems(items);
while (!items.empty())
{
retBoxs.push_back(PackBox(items, level == 0? CheckLevel(items): level));
level = 0;
}
}
void StorageBox::UnpackBox(std::vector<Item> & items, std::vector<ResultBox>::iterator cur, std::vector<ResultBox>::iterator end)
{
for (; cur != end; ++cur)
{
for (auto & retItem : cur->items)
{
if (retItem.IsReady())
{
Item item;
item.i = retItem.i;
item.w = retItem.w;
item.h = retItem.h;
items.push_back(item);
}
}
}
}
這個算法並不能得到最優排列組合,但是這個算法簡單而且在大多數情況下都夠用。
算法 & 數據結構——收納箱算法???