public IList <ItemInfo> Calculate(int sourceItemLength, IDictionary <int, int> targetItemCountsByLengths)
        {
            foreach (var pair in targetItemCountsByLengths)
            {
                var targetItemLength = pair.Key;
                if (targetItemLength > sourceItemLength)
                {
                    throw new ArgumentException();
                }
            }

            CalculationState calculationState = new CalculationState();

            FindMinimum(sourceItemLength, targetItemCountsByLengths, new List <ItemInfo>(), calculationState);
            return(calculationState.MinimumItemInfos);
        }
        private static void FindMinimum(int sourceItemLength,
                                        IDictionary <int, int> targetItemCountsByLengths,
                                        IList <ItemInfo> actualItemInfos,
                                        CalculationState calculationState)
        {
            if (targetItemCountsByLengths.Count == 0)
            {
                var isActualItemInfosMinimum = actualItemInfos.Count < calculationState.ItemCount;
                if (isActualItemInfosMinimum)
                {
                    calculationState.MinimumItemInfos = actualItemInfos;
                }

                return;
            }

            var stateHash      = ItemInfoHelper.GetHash(actualItemInfos);
            var isStateVisited = calculationState.VisitedStateHashes.Contains(stateHash);

            if (isStateVisited)
            {
                return;
            }
            calculationState.VisitedStateHashes.Add(stateHash);

            foreach (var pair in targetItemCountsByLengths)
            {
                var currentItemLength = pair.Key;

                var copiedItemCountsByLengths = new Dictionary <int, int>(targetItemCountsByLengths);
                copiedItemCountsByLengths[currentItemLength]--;
                if (copiedItemCountsByLengths[currentItemLength] == 0)
                {
                    copiedItemCountsByLengths.Remove(currentItemLength);
                }

                foreach (var currentItemInfo in actualItemInfos)
                {
                    var hasFreeLength = ItemInfoHelper.GetFreeLength(currentItemInfo) >= currentItemLength;
                    if (!hasFreeLength)
                    {
                        continue;
                    }

                    var dictionary      = actualItemInfos.ToDictionary(x => x, x => new ItemInfo(x));
                    var copiedItemInfos = new List <ItemInfo>(dictionary.Values);
                    dictionary[currentItemInfo].TargetItemLengths.Add(currentItemLength);

                    FindMinimum(sourceItemLength, copiedItemCountsByLengths, copiedItemInfos, calculationState);
                }
                {
                    var dictionary      = actualItemInfos.ToDictionary(x => x, x => new ItemInfo(x));
                    var copiedItemInfos = new List <ItemInfo>(dictionary.Values)
                    {
                        new ItemInfo(sourceItemLength, currentItemLength)
                    };

                    FindMinimum(sourceItemLength, copiedItemCountsByLengths, copiedItemInfos, calculationState);
                }
            }
        }