【问题标题】:Subset sum problem子集和问题
【发布时间】:2023-03-19 06:14:01
【问题描述】:

我遇到了计数问题,这是this 问题的延续。我不是一个真正的数学人,所以我很难弄清楚这个建议为解决方案的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)

        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;
            }
        }

基本上,我根据事务类型、事务 ID 和编号从数组中添加和删除内容。我正在向 ArrayList 添加数字,例如 156、340(当它是 TransferIn 或 Buy 时)等,然后我删除它们像 156、340(当它是 TransferOut、Sell 时)一样。我的解决方案可以毫无问题地解决这个问题。我遇到的问题是,对于一些旧数据,员工输入的总和是 1500 而不是 500+400+100+500。我将如何更改它,以便当有 Sell/TransferOutBuy/Transfer In 并且 ArrayList 内部没有匹配项时,它应该尝试从 thatArrayList 添加多个项目并找到组合成聚合的元素。

在我的代码中,我尝试在没有匹配项时通过简单的求和所有内容来解决这个问题(索引 == 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);
                            }
                        }
                    }

但它只有在满足某些条件时才有效,其余的则失败。

编辑:由于你们中的一些人对我的波兰变量名称感到非常惊讶(和蒙蔽),为了简单和可见,我将它们全部翻译成英文。希望这能帮助我获得一些帮助:-)

【问题讨论】:

  • 您选择的标识符令人难以置信......
  • switch 使用不当,两个 if 就足够了..
  • @Joren:波兰语可能更有意义。
  • @Srinivas -> 我已经剪掉了这个开关的其他部分以使其更容易。在这个 switch 中有 6 个案例,其中 3 个我已经删除以简化问题。
  • @Joren, Mark -> 是的,它在波兰语中更有意义,我也将一些变量翻译成英语(更难的变量,如购买然后“Papiery wartościowe - Kupno”)也以简化事情。我一个人编写这个代码,它特定于波兰文化,所以用英语保留标识符是没有意义的。

标签: c# sql-server algorithm .net-3.5


【解决方案1】:

您应该如何做到这一点取决于一些重要的事情:您将拥有多少个数字以及它们有多大?另外,据我了解,您的数据可以更改(添加/删除数字等),对吧?您需要多久进行一次这些查询?

我将介绍两种解决方案。我建议你使用第二种,因为我怀疑它更适合你的需要,而且更容易理解。

解决方案 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.

要获得构成总和的实际数字,您应该保留另一个向量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;
            }
        }
    }

也许您应该进行一些错误检查,并且可能将最后一个总和存储在类中,以免调用 GetLastResult 时的总和与上次调用 Find 时使用的总和不同。无论如何,这就是想法。

解决方案 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

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

我认为这个算法应该可以满足您的需求。它可以很好地处理对数据集的更改,并且适用于大量计数。它也不取决于数字有多大,如果您有大数字,这非常有用。

如果您有任何问题,请发布。

【讨论】:

  • 所以我在想:如果你将问题中的所有数字都乘以 10,那么它仍然是同一个问题,所以没有理由让算法需要更长的时间来解决它。但是,您的 DP 算法确实需要更长的时间。我认为您可以通过使用哈希表而不是数组来解决此问题,并以另一种方式填充它。
  • 这是一个针对该想法的 python 解决方案:pastebin.com/mBirSUeZ 如果您调用subsetsum([1,2,3,4],6),它的速度与subsetsum([100,200,300,400],600) 相同。
  • 这个问题没有多项式时间解,DP解是伪多项式的,所以它确实取决于数字的值。当然,这两个速度相同,它们的输入都非常小且相似。另外,你的算法和我的有点不同。您只尝试给出所请求总和的答案,而我尝试建立一个包含所有总和答案的表格。当然,对于这个问题实例,你的更好,但具有相同的复杂性。考虑一个电话sub([1, 1, 1, 1, ...], &lt;num ones&gt;)。对于很多人来说,这应该会让它变得非常缓慢。
  • 它应该执行大约一百万次操作,只需 1000 次。此外,我无法运行您的 python 代码来使用它。我在@memoize 行收到一个错误,是否应该以某种方式手动删除或实施?我不是蟒蛇人。
  • 好的,我稍微改变了实现。我放弃了递归解决方案,因为即使使用元组它也很慢,并且更难获得所需的索引。现在它是迭代的,即使是 1000 个数字也能立即完成,不容易出现堆栈溢出错误,并且让您可以轻松获得不仅仅是真/假的答案。它应该在 .net 3.5 中编译没有问题。
【解决方案2】:

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

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;
        }
    }
}

这是一个修剪集合的改进版本:

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;
}

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

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

编辑:我刚刚在 Wikipedia 上看到最知名的算法是 O(2^(n/2)*n)。这个是O(2^(n/2))。我能获得图灵奖吗?

【讨论】:

  • 为什么说是O(2^(n/2))?在每次调用Insert 时,您都会遍历整个nums 列表。我认为真的做这样的测试是没有意义的,因为很容易找到一个每个算法(伪多项式或指数)都会失败的测试。您发现一个伪多项式算法失败(需要很长时间):100000。这是您的算法也需要很长时间的一个:10000 个数字:1 2 3 4 ... 10000。搜索 345600。另外,你'只是打印真或假,我认为打印数字也会增加一些开销。无论如何,这似乎确实比 DP 快所以 +1,但是..
  • 但是,如果我们要面对如此高的数字,让我在大学回来后实现我的随机算法:)。我认为如果我们处理非常高的数字会更好。
  • 我说它是 O(2^(n/2)) 因为它本质上将输入列表分成两部分(所以 n/2 个元素),然后为那些 O(2 ^(n/2)),然后从这两个列表中查找目标是否可以生成,这是另一个 O(2^(n/2)) 操作。所以总的来说它是 O(2^(n/2)) =~ O(1.4^n)。之前的算法是O(2^n)。
  • 当然你可以找到一个需要很长时间的输入。关键是其他算法在较小的输入上将花费更长的时间;)您是对的,打印数字会增加大量开销。
  • 哦,在你提到的测试用例上,之前的算法实际上更快。这很奇怪……
猜你喜欢
  • 1970-01-01
  • 2011-01-22
  • 1970-01-01
  • 1970-01-01
  • 2020-03-20
  • 2015-03-24
相关资源
最近更新 更多