子集和算法效率

时间:2016-08-05 12:23:08

标签: c# algorithm performance subset-sum

我们每天都会收到一些付款(Transaction)。每个Transaction都有IDAmount。我们要求将大量这些交易与特定金额进行匹配。例如:

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

如果我们想要找到总计600的交易,你可以拥有多套(1,2,3),(2,4),(1,5)。

我找到了一个我已经改编的算法,其工作方式如下所述。 30次交易需要15ms。但交易数量平均约为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;
    }
}

4 个答案:

答案 0 :(得分: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}作为可能的总和。
  5. 但是你还需要导致目标总和的子集,为此我们只记得采用哪个元素来实现当前的子总和。此版本返回一个子集,该子集产生目标总和,否则为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)是否有可能实现目标总和。但我想说标准的动态编程在这里不太容易出错。

答案 1 :(得分:0)

我目前无法提供完整的代码,但是在找到匹配项(O平方)之前,不要迭代每个事务列表两次,请尝试以下概念:

  1. 使用现有交易金额作为条目设置哈希表,以及每组两笔交易的总和,假设每个值最多由两笔交易组成(周末信用卡处理)。
  2. 对于每个总计,对哈希表的引用 - 该槽中的事务集是匹配事务的列表。
  3. 而不是O ^ 2,你可以将它降低到4 * O,这会在速度上产生明显的差异。

    祝你好运!

答案 2 :(得分:0)

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

Knapsack problem学习。 对于这个问题,我们可以为pre i事务定义子集的数量,加起来总和:dp [i] [sum]。 等式:

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

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

答案 3 :(得分:0)

你在这里有一些实际的假设,可以通过聪明的分支修剪来实现蛮力:

  • 项目是唯一的,因此您不会得到有效子集的组合爆炸(即(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输入运行它,产生大约1k的结果导致25毫安以下,所以它应该能够轻松处理你的740案例。