使用C#通过求和或精确单个值从列表中获取最接近的值

时间:2016-03-24 07:25:14

标签: c#

我想找到最接近的交易金额(应该是> =交易金额)或等于给定数字的单笔交易金额,但它应该是最小金额。会有很多数据组合,这些数据是> =给定的数字但是我想要最小的交易数量。

假设我给出金额为100,并且交易金额数字如下

场景1:85,35,25,45,16,100

情景2:55,75,26,55,99

情景3:99,15,66,75,85,88,5

上述情景的预期输出如下

场景1:100

情景2:75,26(即75 + 26 = 101)

情景3:85,15(即85 + 15 = 100)

我当前的代码输出如下

情景1:85,25

情景2:55,26,55

情景3:99,5

这是我的代码

class Program
{
    static void Main(string[] args)
    {
        string input;
        decimal transactionAmount;
        decimal element;

        do
        {
            Console.WriteLine("Please enter the transaction amount:");
            input = Console.ReadLine();
        }
        while (!decimal.TryParse(input, out transactionAmount));

        Console.WriteLine("Please enter the claim amount (separated by spaces)");
        input = Console.ReadLine();

        string[] elementsText = input.Split(' ');
        List<decimal> claimAmountList = new List<decimal>();
        foreach (string elementText in elementsText)
        {
            if (decimal.TryParse(elementText, out element))
            {
                claimAmountList.Add(element);
            }
        }

        Solver solver = new Solver();
        List<List<decimal>> results = solver.Solve(transactionAmount, claimAmountList.ToArray());
        foreach (List<decimal> result in results)
        {
            foreach (decimal value in result)
            {
                Console.Write("{0}\t", value);
            }
            Console.WriteLine();
        }


        Console.ReadLine();

    }
    public class Solver
    {

        private List<List<decimal>> mResults;
        private decimal minimumTransactionAmount = 0;
        public List<List<decimal>> Solve(decimal transactionAmount, decimal[] elements)

        {

            mResults = new List<List<decimal>>();
            RecursiveSolve(transactionAmount, 0.0m,
                new List<decimal>(), new List<decimal>(elements), 0);
            return mResults;
        }

        private void RecursiveSolve(decimal transactionAmount, decimal currentSum,
            List<decimal> included, List<decimal> notIncluded, int startIndex)
        {
            decimal a = 0;
            for (int index = startIndex; index < notIncluded.Count; index++)
            {

                decimal nextValue = notIncluded[index];


                if (currentSum + nextValue >= transactionAmount)
                {
                    if (a >= currentSum + nextValue)
                    {
                        if (minimumTransactionAmount < currentSum + nextValue)
                        {
                            minimumTransactionAmount = currentSum + nextValue;
                            List<decimal> newResult = new List<decimal>(included);
                            newResult.Add(nextValue);
                            mResults.Add(newResult);
                        }
                        a = currentSum + nextValue;
                    }
                    if (a == 0)
                    {
                        a = currentSum + nextValue;    
                    }

                }
                else if (currentSum + nextValue < transactionAmount)
                {
                    List<decimal> nextIncluded = new List<decimal>(included);
                    nextIncluded.Add(nextValue);
                    List<decimal> nextNotIncluded = new List<decimal>(notIncluded);
                    nextNotIncluded.Remove(nextValue);
                    RecursiveSolve(transactionAmount, currentSum + nextValue,
                        nextIncluded, nextNotIncluded, startIndex++);
                }
            }
        }
    }

}

5 个答案:

答案 0 :(得分:3)

好的,在这里我会尝试为答案提出一些建议。基本上,我认为最好制作一些中间类和方法来帮助您解决问题。这个想法如下:

  1. 您可以创建一个包含两个元素TotalValueCombinations的自定义类,以存储方案中每个数字组合的总值和组合。像这样的东西

    public class CombinationAndValue {
        public int TotalValue;
        public List<int> Combinations;
    }
    
  2. 接下来,您可以制作一个自定义方法,该方法会将值列表(即您的数字集)作为输入并生成所有可能的CombinationAndValue类&#39;实例

    public List<CombinationAndValue> comVals(List<int> vals) {
        List<CombinationAndValue> coms = new List<CombinationAndValue>();
        //... logic to generate all possible combinations
        return coms;
    }
    

    要从一组项目中创建所有可能的组合,请考虑此link或其他资源的答案。

  3. 一旦你有这两个项目,你可以做简单的LINQ来获得解决方案:

    List<int> vals = new List<int>() { 55, 75, 26, 55, 99 };
    int target = 100;
    CombinationAndValue sol = comVals(target, vals)
                                .Where(x => x.TotalValue >= 100) //take everything that has TotalValue >= 100
                                .OrderBy(x => x.TotalValue) //order by TotalValue from the smallest
                                .ThenBy(x => x.Combinations.Count) //then by the number of combined elements
                                .FirstOrDefault(); //get first or default
    

答案 1 :(得分:1)

你可能不得不蛮力这个。这是一种方法:

编写提供所有可能组合的方法

这是使用uint中设置的位的标准方法。请注意,此实现仅支持最多包含31个元素的数组。此外,为简洁起见,省略了错误处理。

public static IEnumerable<IEnumerable<T>> Combinations<T>(T[] array)
{
    uint max = 1u << array.Length;

    for (uint i = 1; i < max; ++i)
        yield return select(array, i, max);
}

static IEnumerable<T> select<T>(T[] array, uint bits, uint max)
{
    for (int i = 0, bit = 1; bit < max; bit <<= 1, ++i)
        if ((bits & bit) != 0)
            yield return array[i];
}

编写方法以获取序列的“最大”元素

为此,我们可以使用Jon Skeet等人的“MaxBy”,为了方便我在这里重现(但它是available via NuGet)。

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> selector)
{
    return source.MinBy(selector, Comparer<TKey>.Default);
}

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> selector, IComparer<TKey> comparer)
{
    using (IEnumerator<TSource> sourceIterator = source.GetEnumerator())
    {
        if (!sourceIterator.MoveNext())
            throw new InvalidOperationException("Sequence was empty");

        TSource min = sourceIterator.Current;
        TKey minKey = selector(min);

        while (sourceIterator.MoveNext())
        {
            TSource candidate = sourceIterator.Current;
            TKey candidateProjected = selector(candidate);

            if (comparer.Compare(candidateProjected, minKey) < 0)
            {
                min = candidate;
                minKey = candidateProjected;
            }
        }

        return min;
    }
}

编写算法

将样板代码排除在外,我们现在可以编写算法来确定最接近的匹配:

public static IEnumerable<int> FindClosest(int[] array, int target)
{
    var result = Combinations(array).MinBy(c => {
        int s = c.Sum();
        return s >= target ? s : int.MaxValue; });

    return result.Sum() >= target ? result : Enumerable.Empty<int>();
}

(请注意,此算法会多次枚举序列,这对于数组来说很好,但对于一般IEnumerable<T>则不好。)

Compilable Demo

将它完全放入可编辑的演示控制台应用程序中:

using System;
using System.Collections.Generic;
using System.Linq;

namespace Demo
{
    static class Program
    {
        static void Main()
        {
            int target = 100;

            test(85, 35, 25, 45, 16, 100);   // Prints 100: 100
            test(55, 75, 26, 55, 99);        // Prints 101: 75, 26
            test(99, 15, 66, 75, 85, 88, 5); // Prints 100: 15, 85
            test(1, 1, 1, 1, 1);             // Prints 0: 
        }

        static void test(params int[] a)
        {
            var result = FindClosest(a, 100);
            Console.WriteLine(result.Sum() + ": " + string.Join(", ", result));
        }

        public static IEnumerable<int> FindClosest(int[] array, int target)
        {
            var result = Combinations(array).MinBy(c => {
                int s = c.Sum();
                return s >= target ? s : int.MaxValue; });

            return result.Sum() >= target ? result : Enumerable.Empty<int>();
        }

        public static IEnumerable<IEnumerable<T>> Combinations<T>(T[] array)
        {
            uint max = 1u << array.Length;

            for (uint i = 1; i < max; ++i)
                yield return select(array, i, max);
        }

        static IEnumerable<T> select<T>(T[] array, uint bits, uint max)
        {
            for (int i = 0, bit = 1; bit < max; bit <<= 1, ++i)
                if ((bits & bit) != 0)
                    yield return array[i];
        }

        public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> selector)
        {
            return source.MinBy(selector, Comparer<TKey>.Default);
        }

        public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> selector, IComparer<TKey> comparer)
        {
            using (IEnumerator<TSource> sourceIterator = source.GetEnumerator())
            {
                if (!sourceIterator.MoveNext())
                    throw new InvalidOperationException("Sequence was empty");

                TSource min = sourceIterator.Current;
                TKey minKey = selector(min);

                while (sourceIterator.MoveNext())
                {
                    TSource candidate = sourceIterator.Current;
                    TKey candidateProjected = selector(candidate);

                    if (comparer.Compare(candidateProjected, minKey) < 0)
                    {
                        min = candidate;
                        minKey = candidateProjected;
                    }
                }

                return min;
            }
        }
    }
}

使用MinByOrDefault

的替代方法

由于MinBy不适用于空序列,因此上述解决方案变得复杂。我们可以略微更改它,并将其重命名为MinByOrDefault

通过此更改,特殊情况代码将从FindClosest()消失,如果未找到匹配项,则会返回null

public static IEnumerable<int> FindClosest(int[] array, int target)
{
    return Combinations(array)
        .Where(c => c.Sum() >= target)
        .MinByOrDefault(c => c.Sum());
}

我认为这看起来相当优雅 - 但可能会编写更快,更高效(但更复杂)的实现。特别要注意的是,每种组合的总和计算两次。这可以避免。

这是更新的可编译演示程序。我更喜欢这个版本:

using System;
using System.Collections.Generic;
using System.Linq;

namespace Demo
{
    static class Program
    {
        static void Main()
        {
            int target = 100;

            test(85, 35, 25, 45, 16, 100);   // Prints 100: 100
            test(55, 75, 26, 55, 99);        // Prints 101: 75, 26
            test(99, 15, 66, 75, 85, 88, 5); // Prints 100: 15, 85
            test(1, 1, 1, 1, 1);             // Prints 0: 
        }

        static void test(params int[] a)
        {
            var result = FindClosest(a, 100);

            if (result != null)
                Console.WriteLine(result.Sum() + ": " + string.Join(", ", result));
            else
                Console.WriteLine("No result found for: " + string.Join(", ", a));
        }

        public static IEnumerable<int> FindClosest(int[] array, int target)
        {
            return Combinations(array)
                .Where(c => c.Sum() >= target)
                .MinByOrDefault(c => c.Sum());
        }

        public static IEnumerable<IEnumerable<T>> Combinations<T>(T[] array)
        {
            uint max = 1u << array.Length;

            for (uint i = 1; i < max; ++i)
                yield return select(array, i, max);
        }

        static IEnumerable<T> select<T>(T[] array, uint bits, uint max)
        {
            for (int i = 0, bit = 1; bit < max; bit <<= 1, ++i)
                if ((bits & bit) != 0)
                    yield return array[i];
        }

        public static TSource MinByOrDefault<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> selector)
        {
            return source.MinByOrDefault(selector, Comparer<TKey>.Default);
        }

        public static TSource MinByOrDefault<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> selector, IComparer<TKey> comparer)
        {
            using (IEnumerator<TSource> sourceIterator = source.GetEnumerator())
            {
                if (!sourceIterator.MoveNext())
                    return default(TSource);

                TSource min = sourceIterator.Current;
                TKey minKey = selector(min);

                while (sourceIterator.MoveNext())
                {
                    TSource candidate = sourceIterator.Current;
                    TKey candidateProjected = selector(candidate);

                    if (comparer.Compare(candidateProjected, minKey) < 0)
                    {
                        min = candidate;
                        minKey = candidateProjected;
                    }
                }

                return min;
            }
        }
    }
}

<强>附录

这是FindClosest()的版本,它不会计算两次总和。它不是那么优雅,但可能会更快:

public static IEnumerable<int> FindClosest(int[] array, int target)
{
    return Combinations(array)
        .Select(c => new {S = c.Sum(), C = c})
        .Where(c => c.S >= target)
        .MinByOrDefault(x => x.S)
        ?.C;
}

超过31项的组合

这个版本的Combinations()最多可以使用63个项目:

public static IEnumerable<IEnumerable<T>> Combinations<T>(T[] array)
{
    ulong max = 1ul << array.Length;

    for (ulong i = 1; i != max; ++i)
        yield return selectComb(array, i);
}

static IEnumerable<T> selectComb<T>(T[] array, ulong bits)
{
    ulong bit = 1;

    for (int i = 0; i < array.Length; bit <<= 1, ++i)
        if ((bits & bit) != 0)
            yield return array[i];
}

我认为您不太可能想要生成超过63项的所有组合。

毕竟,63项有2 ^ 63种组合,或9,223,372,036,854,775,808种组合。

即使你可以每秒处理十亿次,也需要250多年才能完成......

答案 2 :(得分:1)

正如评论中所指出的,这是the knapsack problem的变体。但是,如问题Variation on knapsack - minimum total value exceeding 'W'中所述,您的问题在某种意义上是背包问题的相反问题,因为您希望最小化受限制的项目的权重,即总权重应超过最小值。在背包问题中,您希望最大化重量,但要限制总重量不能超过最大值。

幸运的是,accepted answer演示了一个&#34;逆转背包问题&#34;可以通过将物品(交易价值)放入背包来解决。无法放入背包的物品(交易价值)是解决问题的最佳方案。

Google's Operations Research tools为算法提供.NET绑定以解决背包问题。从算法输入开始:

var amounts = new[] { 55, 75, 26, 55, 99 };
var targetAmount = 100;

创建一个背包解算器:

const String name = "https://stackoverflow.com/questions/36195053/";
var solver = new KnapsackSolver(
  KnapsackSolver.KNAPSACK_MULTIDIMENSION_BRANCH_AND_BOUND_SOLVER,
  name
);

此处选择算法类型,并且随着输入的大小增加,某些算法可能具有更好的性能,因此您可能需要在此处进行调整,特别是在强力算法开始执行不当时。 (解决背包问题是NP hard。)

将输入转换为背包解算器使用的值:

var profits = amounts.Select(amount => (Int64) amount).ToArray();
var weights = new Int64[1, profits.Length];
for (var i = 0; i < profits.Length; i += 1)
  weights[0, i] = profits[i];
var capacities = new Int64[] { amounts.Sum() - targetAmount };

注意如amit所述,将容量设置为所有权重之和减去目标值。

执行求解器:

solver.Init(profits, weights, capacities);
solver.Solve();
var solution = profits
  .Where((profit, index) => !solver.BestSolutionContains(index))
  .Select(profit => (Int32) profit)
  .ToArray();

的数量使其成为背包的是解决方案值。在这种情况下,75, 26符合预期。

答案 3 :(得分:0)

假设优化不是问题,像这样的问题总是最容易用蛮力。在这种情况下,只需尝试数字对的每个组合,找到一个给你最低结果的组合。

Heres与我想出了:

    public List<List<decimal>> Solve(decimal transactionAmount, decimal[] elements)
{
    int combinations = Convert.ToInt32(Math.Pow(2.0, elements.Length));
    List<List<decimal>> results = new List<List<decimal>>();
    List<decimal> result = new List<decimal>();
    decimal bestResult = elements.Sum();
    for (int j = 0; j < combinations; j++)
    {
        string bitArray = Convert.ToString(j, 2).PadLeft(elements.Length, '0');
        decimal sum = 0;
        for (int i = 0; i < elements.Length; i++)
        {
            sum += bitArray[i].Equals('1') ? elements[i] : 0;
            if (sum > bestResult)
                break;
        }

        if (sum > bestResult || sum < transactionAmount)
            continue;

        result.Clear();
        result.AddRange(elements.Where((t, i) => bitArray[i].Equals('1')));
        bestResult = result.Sum();

        //Perfect result
        if (sum == transactionAmount)
            results.Add(new List<decimal>(result));
    }

    // Get last solution
    if (results.All(x => result.Except(x).ToList().Count != 0))
        results.Add(new List<decimal>(result));

    return results;
}

它只是将数字的总和跟踪为二进制数,告诉它要么添加要么不添加。如果找到比当前解决方案更好的解决方案,则会更新它。否则只需循环以尝试下一个组合。

我在大学时实际上遇到了类似的问题,所以我知道对这种特殊问题有一些优化。我把我记得的唯一一个,如果它已经比你的最佳结果更差,就不需要继续计算总和。

编辑:刚修改它以返回多个解决方案,因为我发现你有一个List List。如果您想知道new List,它会使List获得一个副本而不是指向结果的指针(正在改变)。

EDIT2:我意识到你可以得到重复的解决方案(比如你有50,50,50,50)。如果你想避免这些,你可以这样做:

public List<List<decimal>> Solve(decimal transactionAmount, decimal[] elements)
{
    // ....
    for (int j = 0; j < combinations; j++)
    {
        // ....
        //Perfect result
        if (sum == transactionAmount)
            results.Add(new List<decimal>(result.OrderBy(t => t)));
    }
    results.Add(new List<decimal>(result.OrderBy(t => t)));

    return results.Distinct(new ListDecimalEquality()).ToList();
}

public class ListDecimalEquality : IEqualityComparer<List<decimal>>
{
    public bool Equals(List<decimal> x, List<decimal> y)
    {
        return x.SequenceEqual(y);
    }

    public int GetHashCode(List<decimal> obj)
    {
        int hashCode = 0;

        for (int index = 0; index < obj.Count; index++)
        {
            hashCode ^= new { Index = index, Item = obj[index] }.GetHashCode();
        }

        return hashCode;
    }
}

答案 4 :(得分:0)

您可以尝试这种简单的方法:

int[] A = {99, 15, 66, 75, 80, 5, 88, 5};
List<Tuple<string, int>> list = new List<Tuple<string, int>>();
list.Add(new Tuple<string, int>(A[0].ToString(),A[0]));
for(int i = 1; i < A.Length; i++)
{
    var newlist = new List<Tuple<string, int>>();
    list.ForEach(l=>newlist.Add(new Tuple<string, int>(l.Item1 + " " + A[i],l.Item2 + A[i])));
    list.Add(new Tuple<string, int>(A[i].ToString(),A[i]));
    list.AddRange(newlist);
}

Tuple<string, int> solution = list.Where(l =>l.Item2 >= 100).OrderBy(o=>o.Item2).First();