背包分组问题的解法
作者:eaglet
今天在博问中看到这样一个问题 按记录总值比例分组记录 ,这个问题本质上是一个背包分组的问题。eaglet 花了2小时时间写了一个C#的实现,时间仓促,感觉还有很多值得改进的地方,不管怎么样,功能是实现了,贴出来给大家讨论吧。
我先把原题的意思按照我的理解再描述一遍:
有数组A 假设为 int[] goods = {25,15,10,3,1, 5, 14, 16, 5, 6};
我们希望将这些goods 按下面给出的分组规则来分组。
我们有数组B 假设为 int[] sizes = {50, 30, 20}; 我们希望把数组A分成三组,并且使每组的和与数组B对应的值最匹配。
本题的答案是
Group1 : {10,3,1,14,16,6}
Group2 : {25,5}
Group3 : {15,5}
数组长度小的时候,用手算就可以分组,但如果长度大,分组数量多,则手算就很难了,需要寻求计算机的帮助。
我的解决思路是:
第一步用整个的goods 数组分别按 50, 30 ,20 计算最优组合,得到三组最优组合(注意这时这三组组合很可能有重复的记录)
第二步从这三组组合中取出最优的一组,也就是总和和对应的大小之差最小的一组,保留这组记录。
第三步从goods数组中将刚刚选中的那组数据剔除掉,然后用新的goods 数组重复第一步,运算时不再运算已选出的组合,直到全部匹配或者只剩下最后一组。
第四步如果还剩下最后一组,则把剩余的goods 全部给这一组,并输出。
下面给出代码
/// <summary>
/// 背包分组
/// </summary>
public class BackpackGroup
{
/// <summary>
/// 找到最匹配的那个组别
/// </summary>
/// <param name="sizes"></param>
/// <param name="result"></param>
/// <returns></returns>
private int GetMostMatchedIndex(int[] sizes, List<int>[] result)
{
int min = int.MaxValue;
int index = -1;
for (int i = 0; i < sizes.Length; i++)
{
if (result[i] != null)
{
int sum = 0;
foreach (int value in result[i])
{
sum += value;
}
if (min >= sizes[i] - sum)
{
index = i;
min = sizes[i] - sum;
}
}
}
return index;
}
/// <summary>
/// 得到剩余的goods
/// </summary>
/// <param name="select"></param>
/// <param name="goods"></param>
/// <returns></returns>
private int[] GetLeftGoods(List<int> select, int[] goods)
{
List<int> result = new List<int>();
int?[] tempSelect = new int?[select.Count];
for (int i = 0; i < select.Count; i++)
{
tempSelect[i] = select[i];
}
foreach (int value in goods)
{
bool throwaway = false;
for (int i = 0; i < select.Count; i++)
{
if (tempSelect[i] == null)
{
continue;
}
if (tempSelect[i] == value)
{
throwaway = true;
tempSelect[i] = null;
break;
}
}
if (!throwaway)
{
result.Add(value);
}
}
return result.ToArray();
}
/// <summary>
/// 递归方式内部分组
/// </summary>
/// <param name="goods"></param>
/// <param name="sizes"></param>
/// <param name="result"></param>
private void InnerGroup(int[] goods, int[] sizes, List<int>[] result)
{
List<int>[] temp = new List<int>[result.Length];
result.CopyTo(temp, 0);
for (int i = 0; i < sizes.Length; i++)
{
if (temp[i] == null)
{
Backpack backpack = new Backpack();
temp[i] = backpack.Match(goods, sizes[i]);
}
else
{
temp[i] = null;
}
}
int index = GetMostMatchedIndex(sizes, temp);
if (index < 0)
{
return;
}
result[index] = temp[index];
goods = GetLeftGoods(temp[index], goods);
int left = 0;
int lastIndex = -1;
for(int i = 0; i < result.Length; i++)
{
if (result[i] == null)
{
lastIndex = i;
left++;
}
}
if (left == 1)
{
result[lastIndex] = new List<int>(goods);
return;
}
InnerGroup(goods, sizes, result);
}
public List<int>[] Group(int[] goods, int[] sizes)
{
List<int>[] result = new List<int>[sizes.Length];
InnerGroup(goods, sizes, result);
return result;
}
}
/// <summary>
/// 背包算法
/// </summary>
public class Backpack
{
private List<int> _MatchGoods = new List<int>();
private List<int> _TmpMatchGoods = new List<int>();
private int[] _Goods;
private int _Size;
private int _Max = 0;
private bool findMatch = false;
private int _PreSum = 0;
private bool _CatchLast = false;
private void Init(int[] goods, int size)
{
_MatchGoods = new List<int>();
_TmpMatchGoods = new List<int>();
_Goods = goods;
_Size = size;
_Max = 0;
findMatch = false;
_PreSum = 0;
_CatchLast = false;
}
/// <summary>
/// 递归计算从第start个元素开始的之后的最匹配结果
/// </summary>
/// <param name="start"></param>
/// <param name="floor"></param>
private void Match(int start, int floor)
{
if (start >= _Goods.Length)
{
_CatchLast = true;
return;
}
if (_PreSum + _Goods[start] > _Size)
{
return;
}
_PreSum += _Goods[start];
_TmpMatchGoods.Add(_Goods[start]);
if (start + 1 >= _Goods.Length)
{
_CatchLast = true;
return;
}
for (int i = start + 1; i < _Goods.Length; i++)
{
Match(i, floor + 1);
if (floor == 0)
{
if (_PreSum == _Size)
{
findMatch = true;
_MatchGoods = _TmpMatchGoods;
}
else
{
if (_Max < _PreSum)
{
_Max = _PreSum;
_MatchGoods = _TmpMatchGoods;
}
}
if (_CatchLast)
{
return;
}
_TmpMatchGoods = new List<int>(_Goods.Length);
_PreSum = 0;
_PreSum += _Goods[start];
_TmpMatchGoods.Add(_Goods[start]);
}
}
}
public List<int> Match(int[] goods, int size)
{
Init(goods, size);
//以此计算各个元素的组合,找到第一个最匹配的结果
for (int i = 0; i < goods.Length; i++)
{
_PreSum = 0;
_CatchLast = false;
_TmpMatchGoods = new List<int>(_Goods.Length);
Match(i, 0);
if (findMatch)
{
return _MatchGoods;
}
}
return _MatchGoods;
}
}
这个算法有个问题,就是背包算法只给出了一组最匹配的记录,如果最匹配的记录有多个(并列的),则情况会更复杂一些,不过这个相对简单的算法最后分组的效果已经不错。
另外背包算法感觉写的并不简洁,应该还有更好的写法。
测试代码
Backpack backpack = new Backpack();
int[] goods = {25,15,10,3,1, 5, 14, 16, 5, 6};
int[] sizes = {35, 45, 20};
BackpackGroup backGroup = new BackpackGroup();
List<int>[] groups = backGroup.Group(goods, sizes);
foreach (List<int> matchGoods in groups)
{
foreach (int mg in matchGoods)
{
Console.Write(mg);
Console.Write(",");
}
Console.WriteLine();
}
Console.ReadKey();
下面给出几个不同的分组的结果
int[] sizes = {50, 30, 20};
10,3,1,14,16,6,
25,5,
15,5,
int[] sizes = {70, 10, 20};
25,3,1,14,16,5,6,
10,
15,5,
int[] sizes = {35, 45, 20};
10,3,1,16,6,
25,14,5,
15,5,
前两组完全匹配,最后一组近似匹配。