背包分组问题的解法
作者: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,
前两组完全匹配,最后一组近似匹配。