【问题标题】:Subset Sum algorithm efficiency子集和算法效率
【发布时间】:2023-04-05 00:33:02
【问题描述】:

我们每天都有许多付款 (Transaction) 进入我们的业务。每个Transaction 都有一个ID 和一个Amount。我们要求将其中一些交易与特定金额相匹配。示例:

Transaction    Amount
1              100
2              200
3              300
4              400
5              500

如果我们想找到总和为 600 的交易,您将有多个集合 (1,2,3),(2,4),(1,5)。

我找到了一个我已经适应的算法,它的工作原理如下。 30 笔交易需要 15 毫秒。但交易数量平均约为 740,最大接近 6000。执行此搜索的效率更高吗?

sum_up(TransactionList, remittanceValue, ref MatchedLists);

private static void sum_up(List<Transaction> transactions, decimal target, ref List<List<Transaction>> matchedLists)
{
    sum_up_recursive(transactions, target, new List<Transaction>(), ref matchedLists);
}

private static void sum_up_recursive(List<Transaction> transactions, decimal target, List<Transaction> partial, ref List<List<Transaction>> matchedLists)
{
    decimal s = 0;
    foreach (Transaction x in partial) s += x.Amount;

    if (s == target)
    {
        matchedLists.Add(partial);
    }

    if (s > target)
        return;

    for (int i = 0; i < transactions.Count; i++)
    {
        List<Transaction> remaining = new List<Transaction>();
        Transaction n = new Transaction(0, transactions[i].ID, transactions[i].Amount);
        for (int j = i + 1; j < transactions.Count; j++) remaining.Add(transactions[j]);

        List<Transaction> partial_rec = new List<Transaction>(partial);
        partial_rec.Add(new Transaction(n.MatchNumber, n.ID, n.Amount));
        sum_up_recursive(remaining, target, partial_rec, ref matchedLists);
    }
}

Transaction 定义为:

class Transaction
{
    public int ID;
    public decimal Amount;
    public int MatchNumber;

    public Transaction(int matchNumber, int id, decimal amount)
    {
        ID = id;
        Amount = amount;
        MatchNumber = matchNumber;
    }
}

【问题讨论】:

  • Wrong site 我想...
  • 列表中是否有很多重复值?
  • 不,所有值都是唯一的,我们目前正在努力缩小我们从中选择的列表,但它可能不会对集合产生太大影响。
  • @Sinatr 我认为这是正确的领域,因为我正在专门研究我拥有的算法的当前 C# 实现。
  • @anothershrubery,codereview - 你有工作代码并想要改进它,programmers - 最佳算法(语言不可知或c#)。如果您有错误(不工作的代码)或遇到问题(性能),Stackoverflow 很好。我不是坚持,但我认为你有更好的算法。另一件事是你没有解释你的,但它看起来像直接的(递归迭代),内存效率很高但性能很差。

标签: c# algorithm performance subset-sum


【解决方案1】:

如前所述,您的问题可以通过 O(n*G) 中的伪多项式算法解决,n - 项目数和 G - 您的目标总和。

第一部分问题:是否有可能达到目标总和G。以下伪/python 代码解决了它(我的机器上没有 C#):

def subsum(values, target):
    reached=[False]*(target+1) # initialize as no sums reached at all
    reached[0]=True # with 0 elements we can only achieve the sum=0
    for val in values:
        for s in reversed(xrange(target+1)): #for target, target-1,...,0
            if reached[s] and s+val<=target: # if subsum=s can be reached, that we can add the current value to this sum and build an new sum 
                reached[s+val]=True
    return reached[target] 

什么想法?让我们考虑值[1,2,3,6] 和目标总和7

  1. 我们从一个空集开始 - 可能的总和显然是 0
  2. 现在我们看看第一个元素1 并且必须选择接受或不接受。剩下的可能是{0,1}
  3. 现在查看下一个元素2:导致可能的集合{0,1}(不接受)+{2,3}(接受)。
  4. 到目前为止,与您的方法没有太大区别,但现在对于元素 3,我们可以设置 a. 用于不采用 {0,1,2,3}b. 用于采用{3,4,5,6} 导致 {0,1,2,3,4,5,6} 作为可能的总和。您的方法的不同之处在于有两种方法可以到达3,并且您的递归将从那里开始两次(这不是必需的)。一遍又一遍地计算基本相同的员工是您的方法的问题,以及为什么提出的算法更好。
    1. 作为最后一步,我们考虑6 并获得{0,1,2,3,4,5,6,7} 作为可能的总和。

但您还需要导致目标总和的子集,为此我们只需记住采用哪个元素来实现当前子总和。此版本返回结果为目标总和的子集,否则返回 None

def subsum(values, target):
    reached=[False]*(target+1)
    val_ids=[-1]*(target+1)
    reached[0]=True # with 0 elements we can only achieve the sum=0

    for (val_id,val) in enumerate(values):
        for s in reversed(xrange(target+1)): #for target, target-1,...,0
            if reached[s] and s+val<=target:
                reached[s+val]=True
                val_ids[s+val]=val_id          

    #reconstruct the subset for target:
    if not reached[target]:
        return None # means not possible
    else:
        result=[]
        current=target
        while current!=0:# search backwards jumping from predecessor to predecessor
           val_id=val_ids[current]
           result.append(val_id)
           current-=values[val_id]
        return result

作为另一种方法,您可以使用memoization 来加快您当前的解决方案,记住状态(subsum, number_of_elements_not considered) 是否有可能达到目标总和。但我想说标准动态规划在这里不太容易出错。

【讨论】:

    【解决方案2】:

    是的。

    目前我无法提供完整的代码,但不要重复每个事务列表两次直到找到匹配项(O 平方),试试这个概念:

    1. 设置一个哈希表,将现有交易金额作为条目,以及每组两笔交易的总和,假设每个值最多由两笔交易组成(周末信用卡处理)。
    2. 对于每个总数,参考哈希表 - 该槽中的交易集是匹配交易的列表。

    您可以将其降低到 4*O,而不是 O^2,这会显着提高速度。

    祝你好运!

    【讨论】:

    • 该值可以由两个以上的交易组成。交易数量没有限制,因此我认为这不会起作用?
    【解决方案3】:

    动态编程可以有效解决这个问题: 假设您有 n 笔交易,最大交易量为 m。 我们可以只用 O(nm) 的复杂度来解决它。

    Knapsack problem 了解它。 对于这个问题,我们可以为前 i 个交易定义子集的数量,加起来为 sum:dp[i][sum]。 等式:

    for i 1 to n:
        dp[i][sum] = dp[i - 1][sum - amount_i]
    

    dp[n][sum] 是您需要的数字,您需要添加一些技巧来获得所有子集。 块引用

    【讨论】:

      【解决方案4】:

      您在这里有几个实际假设可以使智能分支修剪的蛮力变得可行:

      • 项目是独一无二的,因此您不会得到有效子集的组合爆炸(即 (1,1,1,1,1,1,1,1,1,1,1,1,1) 添加最多 3)
      • 如果生成的可行集的数量仍然很大,那么在遇到总运行时问题之前,您将用完收集它们的内存。
      • 排序输入升序将允许一个简单的提前停止检查 - 如果您的剩余总和小于当前元素,那么任何尚未检查的项目都不可能出现在结果中(因为当前和后续项目只会变得更大)
      • 保持运行总和会加快每一步,因为您不会一遍又一遍地重新计算它

      这里有一段代码:

      public static List<T[]> SubsetSums<T>(T[] items, int target, Func<T, int> amountGetter)
          {
              Stack<T> unusedItems = new Stack<T>(items.OrderByDescending(amountGetter));
              Stack<T> usedItems = new Stack<T>();
              List<T[]> results = new List<T[]>();
              SubsetSumsRec(unusedItems, usedItems, target, results, amountGetter);
              return results;
          }
          public static void SubsetSumsRec<T>(Stack<T> unusedItems, Stack<T> usedItems, int targetSum, List<T[]> results, Func<T,int> amountGetter)
          {
              if (targetSum == 0)
                  results.Add(usedItems.ToArray());
              if (targetSum < 0 || unusedItems.Count == 0)
                  return;
              var item = unusedItems.Pop();
              int currentAmount = amountGetter(item);
              if (targetSum >= currentAmount)
              {
                  // case 1: use current element
                  usedItems.Push(item);
                  SubsetSumsRec(unusedItems, usedItems, targetSum - currentAmount, results, amountGetter);
                  usedItems.Pop();
                  // case 2: skip current element
                  SubsetSumsRec(unusedItems, usedItems, targetSum, results, amountGetter);
              }
              unusedItems.Push(item);
          }
      

      我已经针对 100k 输入运行它,在 25 毫秒以下产生大约 1k 的结果,因此它应该能够轻松处理您的 740 案例。

      【讨论】:

      • 不知道是什么问题,但我已经让你的确切代码运行了大约 20 分钟,但仍然没有结果......
      • 得到了OutOfMemoryException
      • 这基本上意味着你得到了太多的结果,无论如何这在实践中是相当无用的。一旦获得 N 个结果并使用这些结果,您可能想要停止。
      • 虽然违背了练习的目的。如果我们不能得到所有结果,最好没有得到,因此我们不会使用那个解决方案。
      • 没错。您可以添加“If (results.Count > maxresults) return;”行到递归方法提前停止
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-05-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多