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