背包分组问题的解法

背包分组问题的解法

作者: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,

前两组完全匹配,最后一组近似匹配。

原文地址:https://www.cnblogs.com/eaglet/p/1555645.html