子集和(硬币变化)

时间:2012-09-21 14:48:35

标签: c++ algorithm

我的问题是,我需要计算整数数组的总和与W总和的数量。“

让我们说:

int array[] = {1,2,3,4,5};

我的算法只是找到从1W / minimum(array)的所有长度组合,它等于W,因为最小值为1。 如果每个组合的总和等于W,则检查每个组合然后增加一个计数器N

任何其他算法来解决这个问题?应该更快:))

更新 好吧,子集问题和背包问题都很好,但我的问题是数组的组合重复元素,如下所示:

1,1,1 -> the 1st combination
1,1,2
1,1,3
1,1,4
1,1,5
1,2,2 -> this combination is 1,2,2, not 1,2,1 because we already have 1,1,2. 
1,2,3
1,2,4
1,2,5
1,3,3 -> this combination is 1,3,3, not 1,3,1 because we already have 1,1,3. 
1,3,4
.
.
1,5,5
2,2,2 -> this combination is 2,2,2, not 2,1,1 because we already have 1,1,2. 
2,2,3
2,2,4
2,2,5
2,3,3 -> this combination is 2,3,3, not 2,3,1 because we already have 1,2,3.
.
.
5,5,5 -> Last combination

这些都是长度为3的{1,2,3,4,5}的组合。子集求和问题提供了另一种我不感兴趣的组合。

所以汇总到W的组合,可以说是W = 7

2,5
1,1,5
1,3,3
2,2,3
1,1,2,3
1,2,2,2
1,1,1,1,3
1,1,1,2,2
1,1,1,1,1,2
1,1,1,1,1,1,1

更新 真实问题是重复的元素1,1,1是必要的,并且生成的组合的顺序并不重要,因此1,2,11,1,22,1,1相同。

4 个答案:

答案 0 :(得分:9)

目前还没有有效的算法,也许永远不会(NP完全问题)。

这是({3}}的变体。

答案 1 :(得分:3)

这是coin change problem。它可以通过动态编程来解决,并且具有合理的W和设置大小

的限制

答案 2 :(得分:0)

以下是Go中解决此问题的代码。我相信它在O(W / min(A))时间运行。评论应该足以看出它是如何工作的。重要的细节是它可以多次使用A中的元素,但是一旦它停止使用该元素,它就不会再使用它。这避免了重复计算[1,2,1]和[1,1,2]等事情。

package main

import (
  "fmt"
  "sort"
)

// This is just to keep track of how many times we hit ninjaHelper
var hits int = 0

// This is our way of indexing into our memo, so that we don't redo any
// calculations.
type memoPos struct {
  pos, sum int
}

func ninjaHelper(a []int, pos, sum, w int, memo map[memoPos]int64) int64 {
  // Count how many times we call this function.
  hits++

  // Check to see if we've already done this computation.
  if r, ok := memo[memoPos{pos, sum}]; ok {
    return r
  }

  // We got it, and we can't get more than one match this way, so return now.
  if sum == w {
    return 1
  }

  // Once we're over w we can't possibly succeed, so just bail out now.
  if sum > w {
    return 0
  }

  var ret int64 = 0
  // By only checking values at this position or later in the array we make
  // sure that we don't repeat ourselves.
  for i := pos; i < len(a); i++ {
    ret += ninjaHelper(a, i, sum+a[i], w, memo)
  }

  // Write down our answer in the memo so we don't have to do it later.
  memo[memoPos{pos, sum}] = ret
  return ret
}

func ninja(a []int, w int) int64 {
  // We reverse sort the array.  This doesn't change the complexity of
  // the algorithm, but by counting the larger numbers first we can hit our
  // target faster in a lot of cases, avoid a bit of work.
  sort.Ints(a)
  for i := 0; i < len(a)/2; i++ {
    a[i], a[len(a)-i-1] = a[len(a)-i-1], a[i]
  }
  return ninjaHelper(a, 0, 0, w, make(map[memoPos]int64))
}

func main() {
  a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
  w := 1000
  fmt.Printf("%v, w=%d: %d\n", a, w, ninja(a, w))
  fmt.Printf("Hits: %v\n", hits)
}

答案 3 :(得分:0)

只是把它放到床上,这里是递归和(非常简单)动态编程解决这个问题。您可以通过使用更复杂的终止条件来减少递归解决方案的运行时间(但不是时间复杂度),但其主要目的是显示逻辑。

我见过的许多动态编程解决方案都保留了整个N x | c |结果数组,但这不是必需的,因为第i行可以从第i-1行生成,而且它可以按从左到右的顺序生成,因此不需要进行复制。

我希望这些评论有助于解释逻辑。 dp解决方案速度足够快,以至于我无法找到一个没有超长时间溢出的测试用例,这个时间超过了几毫秒;例如:

$ time ./coins dp 1000000 1 2 3 4 5 6 7
3563762607322787603

real    0m0.024s
user    0m0.012s
sys     0m0.012s

// Return the number of ways of generating the sum n from the
// elements of a container of positive integers.
// Note: This function will overflow the stack if an element
//       of the container is <= 0.
template<typename ITER>
long long count(int n, ITER begin, ITER end) {
  if (n == 0) return 1;
  else if (begin == end || n < 0) return 0;
  else return
      // combinations which don't use *begin
    count(n, begin + 1, end) +
      // use one (more) *begin.
    count(n - *begin, begin, end);
}

// Same thing, but uses O(n) storage and runs in O(n*|c|) time, 
// where |c| is the length of the container. This implementation falls
// directly out of the recursive one above, but processes the items
// in the reverse order; each time through the outer loop computes
// the combinations (for all possible sums <= n) for sum prefix of
// the container.
template<typename ITER>
long long count1(int n, ITER begin, ITER end) {
  std::vector<long long> v(n + 1, 0);
  v[0] = 1;
  // Initial state of v: v[0] is 1; v[i] is 0 for 1 <= i <= n.
  // Corresponds to the termination condition of the recursion.

  auto vbegin = v.begin();
  auto vend = v.end();
  for (auto here = begin; here != end; ++here) {
    int a = *here;
    if (a > 0 && a <= n) {
      auto in = vbegin;
      auto out = vbegin + a;
      // *in is count(n - a, begin, here).
      // *out is count(n, begin, here - 1).
      do *out++ += *in++; while (out != vend);
    }
  } 
  return v[n];
}