子集和问题

Mad*_*Boy 5 c# sql-server algorithm .net-3.5

我有一个计数问题,这是这个问题的延续.我不是一个真正的数学家,所以我很难弄清楚subset sum problem这被建议作为解决方案.

我有4个ArrayList我持有数据:alId,alTransaction,alNumber,alPrice

输入| 交易| 号码| 价格
8 | 买| 95.00000000 | 305.00000000
8 | 买| 126.00000000 | 305.00000000
8 | 买| 93.00000000 | 306.00000000
8 | 转出| 221.00000000 | 305.00000000
8 | 转入| 221.00000000 | 305.00000000
8 | 卖| 93.00000000 | 360.00000000
8 | 卖| 95.00000000 | 360.00000000
8 | 卖| 126.00000000 | 360.00000000
8 | 买| 276.00000000 | 380.00000000

最后,我正在尝试为客户留下剩下的东西以及我放入3个数组列表的内容:
- alNewHowMuch(对应于alNumber),
- alNewPrice(对应于alPrice),
- alNewInID(对alID的corrseponds)

        ArrayList alNewHowMuch = new ArrayList();
        ArrayList alNewPrice = new ArrayList();
        ArrayList alNewInID = new ArrayList();
        for (int i = 0; i < alTransaction.Count; i++) {
            string transaction = (string) alTransaction[i];
            string id = (string) alID[i];
            decimal price = (decimal) alPrice[i];
            decimal number = (decimal) alNumber[i];
            switch (transaction) {
                case "Transfer out":
                case "Sell":
                    int index = alNewHowMuch.IndexOf(number);
                    if (index != -1) {
                        alNewHowMuch.RemoveAt(index);
                        alNewPrice.RemoveAt(index);
                        alNewInID.RemoveAt(index);
                    } else {
                        ArrayList alTemp = new ArrayList();
                        decimal sum = 0;
                        for (int j = 0; j < alNewHowMuch.Count; j ++) {
                            string tempid = (string) alNewInID[j];
                            decimal tempPrice = (decimal) alNewPrice[j];
                            decimal tempNumbers = (decimal) alNewHowMuch[j];
                            if (id == tempid && tempPrice == price) {
                                alTemp.Add(j);
                                sum = sum + tempNumbers;
                            }
                        }
                        if (sum == number) {
                            for (int j = alTemp.Count - 1; j >= 0; j --) {
                                int tempIndex = (int) alTemp[j];
                                alNewHowMuch.RemoveAt(tempIndex);
                                alNewPrice.RemoveAt(tempIndex);
                                alNewInID.RemoveAt(tempIndex);
                            }
                        }
                    }
                    break;
                case "Transfer In":
                case "Buy":
                    alNewHowMuch.Add(number);
                    alNewPrice.Add(price);
                    alNewInID.Add(id);
                    break;
            }
        }
Run Code Online (Sandbox Code Playgroud)

基本上我是根据事务类型,事务ID和数字添加和删除Array中的东西.我正在向ArrayList添加数字,如156,340(当它是TransferIn或Buy)等等,然后我删除它们就像156,340那样(当它是TransferOut,卖出时).我的解决方案适用于此而没有问题.我遇到的问题是,对于一些旧数据,员工输入的金额是1500而不是500 + 400 + 100 + 500.我如何更改它,以便当ArrayList中存在Sell/TransferOut或者Buy/Transfer In没有匹配时,它应该尝试从中添加多个项目ArrayList并找到组合成聚合的元素.

在我的代码中,我尝试通过简单的总结来解决这个问题,当没有匹配时(index == 1)

                    int index = alNewHowMuch.IndexOf(number);
                    if (index != -1) {
                        alNewHowMuch.RemoveAt(index);
                        alNewPrice.RemoveAt(index);
                        alNewInID.RemoveAt(index);
                    } else {
                        ArrayList alTemp = new ArrayList();
                        decimal sum = 0;
                        for (int j = 0; j < alNewHowMuch.Count; j ++) {
                            string tempid = (string) alNewInID[j];
                            decimal tempPrice = (decimal) alNewPrice[j];
                            decimal tempNumbers = (decimal) alNewHowMuch[j];
                            if (id == tempid && tempPrice == price) {
                                alTemp.Add(j);
                                sum = sum + tempNumbers;
                            }
                        }
                        if (sum == number) {
                            for (int j = alTemp.Count - 1; j >= 0; j --) {
                                int tempIndex = (int) alTemp[j];
                                alNewHowMuch.RemoveAt(tempIndex);
                                alNewPrice.RemoveAt(tempIndex);
                                alNewInID.RemoveAt(tempIndex);
                            }
                        }
                    }
Run Code Online (Sandbox Code Playgroud)

但它只有在满足某些条件时才有效,而其余条件则无效.

编辑:由于你们中的一些人被我的波兰变量名称所惊讶(并且蒙蔽了眼睛),我将它们全部翻译成英文,以简化和可见.希望这会帮助我得到一些帮助:-)

IVl*_*lad 5

你应该怎么做取决于一些重要的事情:你将拥有多少数字以及它们有多大?另外,据我所知,您的数据可以更改(添加/删除数字等),对吧?您经常需要进行这些查询吗?

我将提出两个解决方案.我建议你使用第二种,因为我怀疑它对你需要的更好,而且更容易理解.

解决方案1 ​​ - 动态编程

S[i] = true if we can make sum i and false otherwise.

S[0] = true // we can always make sum 0: just don't choose any number
S[i] = false for all i != 0
for each number i in your input
    for s = MaxSum downto i
        if ( S[s - i] == true )
            S[s] = true; // if we can make the sum s - i, we can also make the sum s by adding i to the sum s - i.
Run Code Online (Sandbox Code Playgroud)

要获得构成总和的实际数字,您应该保留另一个向量P[i] = the last number that was used to make sum i.您可以在上述if条件下相应地更新.

时间复杂度O(numberOfNumbers * maxSumOfAllNumbers)非常糟糕,特别是因为您必须在数据发生变化时重新运行此算法.只要你的数字非常大并且你可以拥有很多它们,即使是一次运行也很慢.事实上,"很多"都是误导.如果您有100个数字且每个数字可以大到10 000,那么每次数据更改时,您将执行大约100*10 000 = 1 000 000次操作.

知道这是一个很好的解决方案,但在实践中并没有真正有用,或者至少在你的情况下并非如此.

对于我建议的方法,他是一些C#:

   class Program
      {
        static void Main(string[] args)
        {
            List<int> testList = new List<int>();

            for (int i = 0; i < 1000; ++i)
            {
                testList.Add(1);
            }

            Console.WriteLine(SubsetSum.Find(testList, 1000));

            foreach (int index in SubsetSum.GetLastResult(1000))
            {
                Console.WriteLine(index);
            }
        }
    }

    static class SubsetSum
    {
        private static Dictionary<int, bool> memo;
        private static Dictionary<int, KeyValuePair<int, int>> prev;

        static SubsetSum()
        {
            memo = new Dictionary<int, bool>();
            prev = new Dictionary<int, KeyValuePair<int, int>>();
        }

        public static bool Find(List<int> inputArray, int sum)
        {
            memo.Clear();
            prev.Clear();

            memo[0] = true;
            prev[0] = new KeyValuePair<int,int>(-1, 0);

            for (int i = 0; i < inputArray.Count; ++i)
            {
                int num = inputArray[i];
                for (int s = sum; s >= num; --s)
                {
                    if (memo.ContainsKey(s - num) && memo[s - num] == true)
                    {
                        memo[s] = true;

                        if (!prev.ContainsKey(s))
                        {
                            prev[s] = new KeyValuePair<int,int>(i, num);
                        }
                    }
                }
            }

            return memo.ContainsKey(sum) && memo[sum];
        }

        public static IEnumerable<int> GetLastResult(int sum)
        {
            while (prev[sum].Key != -1)
            {
                yield return prev[sum].Key;
                sum -= prev[sum].Value;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

您可能应该进行一些错误检查,并且可能将最后一笔存储在类中,以便不允许使用GetLastResultFind上次调用的总和不同的总和进行调用.无论如何,这是个主意.

解决方案2 - 随机算法

现在,这更容易.保留两个列表:usedNumsunusedNums.还要保留一个变量usedSum,该变量在任何时间点都包含usedNums列表中所有数字的总和.

每当你需要在你的集合中插入一个数字时,也要将它添加到两个列表中的一个(无关紧要,但是随机进行,以便有相对均匀的分布).相应更新usedSum.

每当你需要从你的集合中删除一个数字时,找出它所在的两个列表中的哪一个.只要你没有很多东西,你就可以用线性搜索来做(这一次很多意味着超过10 000,也许在快速计算机上甚至是100 000,并且假设您不经常并且连续快速地执行此操作.无论如何,如果您需要,可以优化线性搜索.).找到号码后,将其从列表中删除.相应更新usedSum.

每当您需要查找集合中是否有与数字相加的数字时S,请使用以下算法:

while S != usedSum
    if S > usedSum // our current usedSum is too small
        move a random number from unusedNums to usedNums and update usedSum
    else // our current usedSum is too big
        move a random number from usedNums to unusedNums and update usedSum
Run Code Online (Sandbox Code Playgroud)

在算法结束时,列表usedNums将为您提供其总和的数字S.

我认为,这个算法应该对你需要的东西有益.它可以很好地处理数据集的更改,并且可以很好地处理大量数据.它也不取决于数字有多大,如果您有大数字,这非常有用.

如果您有任何疑问,请发布.


Jul*_*les 5

这是我的算法.它运行O(2^(n/2))SubsetSum(1000, list-of-1000-ones)在20毫秒内解决.请参阅IVlad帖子末尾的评论.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace SubsetSum
{
    class Program
    {
        static void Main(string[] args)
        {
            var ns = new List<int>();
            for (int i = 0; i < 1000; i++) ns.Add(1);
            var s1 = Stopwatch.StartNew();
            bool result = SubsetSum(ns, 1000);
            s1.Stop();
            Console.WriteLine(result);
            Console.WriteLine(s1.Elapsed);
            Console.Read();
        }

        static bool SubsetSum(ist<int> nums, int targetL)
        {
            var left = new List<int> { 0 };
            var right = new List<int> { 0 };
            foreach (var n in nums)
            {
                if (left.Count < right.Count) left = Insert(n, left);
                else right = Insert(n, right);
            }
            int lefti = 0, righti = right.Count - 1;
            while (lefti < left.Count && righti >= 0)
            {
                int s = left[lefti] + right[righti];
                if (s < target) lefti++;
                else if (s > target) righti--;
                else return true;
            }
            return false;
        }

        static List<int> Insert(int num, List<int> nums)
        {
            var result = new List<int>();
            int lefti = 0, left = nums[0]+num;
            for (var righti = 0; righti < nums.Count; righti++)
            {

                int right = nums[righti];
                while (left < right)
                {
                    result.Add(left);
                    left = nums[++lefti] + num;
                }
                if (right != left) result.Add(right);
            }
            while (lefti < nums.Count) result.Add(nums[lefti++] + num);
            return result;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是一个修改版本的改进版本:

static bool SubsetSum(List<int> nums, int target)
{
    var remainingSum = nums.Sum();
    var left = new List<int> { 0 };
    var right = new List<int> { 0 };
    foreach (var n in nums)
    {
        if (left.Count == 0 || right.Count == 0) return false;
        remainingSum -= n;
        if (left.Count < right.Count) left = Insert(n, left, target - remainingSum - right.Last(), target);
        else right = Insert(n, right, target - remainingSum - left.Last(), target);
    }
    int lefti = 0, righti = right.Count - 1;
    while (lefti < left.Count && righti >= 0)
    {
        int s = left[lefti] + right[righti];
        if (s < target) lefti++;
        else if (s > target) righti--;
        else return true;
    }
    return false;
}

static List<int> Insert(int num, List<int> nums, int min, int max)
{
    var result = new List<int>();
    int lefti = 0, left = nums[0]+num;
    for (var righti = 0; righti < nums.Count; righti++)
    {

        int right = nums[righti];
        while (left < right)
        {
            if (min <= left && left <= max) result.Add(left);
            left = nums[++lefti] + num;
        }
        if (right != left && min <= right && right <= max) result.Add(right);
    }
    while (lefti < nums.Count)
    {
        left = nums[lefti++] + num;
        if (min <= left && left <= max) result.Add(left);
    } 
    return result;
}
Run Code Online (Sandbox Code Playgroud)

最后一个在大约5毫秒内解决了100000个问题(但这是算法的最佳情况,现实世界数据会慢一些).

为了您的使用,这个算法可能足够快(我没有看到任何明显的改进).如果您输入10,000个产品,其随机价格在0到20之间,并且您的目标是总和为500,那么在我的笔记本电脑上可以在0.04秒内解决.

编辑:我刚刚在维基百科上读到了最着名的算法O(2^(n/2)*n).这个是O(2^(n/2)).我获得图灵奖吗?