子集和算法

时间:2010-12-04 21:34:18

标签: algorithm dynamic-programming subset-sum

我正在解决这个问题:

  

Subset Sum问题将X = {x1, x2 ,…, xn}个整数n和另一个整数K作为输入。问题是检查是否存在X'的{​​{1}}子集,其元素总和为X并找到子集(如果有)。例如,如果KX = {5, 3, 11, 8, 2},则答案为K = 16,因为子集YES的总和为X' = {5, 11}。为子集Sum实现一个算法,其运行时间至少为16

注意复杂性O(nK)。我认为动态编程可能有所帮助。

我找到了一个指数时间算法,但它无济于事。

有人可以帮我解决这个问题吗?

12 个答案:

答案 0 :(得分:40)

这个问题被观看了36000多次,但我没有看到足够的答案,用逻辑详细解释算法。所以我想我会尝试这样做。

<强>假设:

为简单起见,我首先假设输入集X仅包含正整数,k为正数。但是,我们可以调整算法来处理负整数以及k为负的情况。

<强>逻辑:

此算法的关键或真正 任何DP问题都是打破问题并从基本情况开始。 然后我们可以使用基础案例构建我们知道的一些知识:

  1. 我们知道如果集合X为空,那么我们就无法求和k的任何值。
  2. 如果一组X包含k,那么它的子集总和为k
  3. 我们知道,如果x1的子集X的一部分k1总和为X,那么k1将有一个总和为x1的子集即X = {x1, x1, x3, ......., xn, xn+1}
  4. 我们有一套k1。如果x1 = {x1, x1, x3, ......., xn}的子集总和为k - k1,我们知道它有X = {4}的子集和。
  5. 举例说明1,2,3,4:

    1. 很容易。如果你有一个空集{}。你不能拥有一个子集 你不能有任何子集。
    2. 集合x1 = {1,3,5}的子集总数为4,因为4它本身就是集合的一部分

    3. 假设您有一组X = {1,3,5,2,8},他是集x1的子集。如果k1 = 8的子集总和为X,则表示x1的子集总和为8​​,因为XX = {1,3,5,2,19}的子集

    4. 说你有一个集合x1 = {1,3,5,2},我们想要知道它是否有一个20的子集和。它确实有一种方法可以知道m是否可以求和(20 - 19) )= 1.由于x1的子集和为1,所以当我们将19加到集合x1时 我们可以使用新的数字1 + 19 = 20来创建我们想要的总和20.
    5. 动态构建矩阵 凉!现在让我们利用上述四个逻辑并从基础案例开始构建。我们将构建一个矩阵m。我们定义:

      • 矩阵i+1k + 1行和true列。

      • 矩阵的每个单元格都有falsei

      • m [i] [s]返回true或false以指示此问题的答案:&#34;使用数组中的第一个s项,我们可以找到{{{ 1}? &#34; m[i][s]返回true表示是,false表示否

      (注意维基百科的答案或大多数人构建函数m(i,s)但我认为矩阵是一种理解动态编程的简单方法。当我们在集合或数组中只有正数时,它很有效。但是函数路径更好,因为你不必处理索引超出范围,匹配数组的索引和总和到矩阵.....)

      让我们使用示例构建矩阵:

      X = {1,3,5,2,8}
      k = 9
      

      我们将逐行构建矩阵。我们最终想知道m [n] [k]包含truefalse的单元格。

      第一行: 逻辑1.告诉我们矩阵的第一行应该都是false

         0 1 2 3 4 5 6 7 8 9
         _ _ _ _ _ _ _ _ _ _
      0| F F F F F F F F F F
      1|
      2|
      3|
      4|
      5|
      

      第二行及以上: 然后对于第二行或更高的行,我们可以使用逻辑2,3,4来帮助我们填充矩阵。

      • 逻辑2告诉我们m[i][s] = (X[i-1] == s) rememebr m [i]指的是X中的第i项,即X [i-1]
      • 逻辑3告诉我们m[i][s] = (m[i-1][s])这是在查看上面的单元格。
      • 逻辑4告诉我们m[i][s] = (m[i-1][s - X[i-1]])这是在查看X [i-1]单元格上方和左侧的行。

      如果其中任何一项为true,则m[i][s]true,否则为false。所以我们可以将2,3,4重写为m[i][s] = (m[i-1][s] || a[i-1] == s || m[i-1][s - a[i-1]])

      使用以上逻辑填充矩阵m。在我们的示例中,它看起来像这样。

         0 1 2 3 4 5 6 7 8 9
         _ _ _ _ _ _ _ _ _ _
      0| F F F F F F F F F F
      1| F T F F F F F F F F
      2| F T F T T F F F F F 
      3| F T F T T T T F T T
      4| F T T T T T T T T T 
      5| F T T T T T T T T T
      

      现在使用矩阵回答您的问题:

      查看m[5][9]这是原始问题。使用前5项(这是所有项目)我们可以找到9(k)的子集和?并且该单元表示答案为true

      以下是代码:

      import java.util.*;
      
      public class SubSetSum {
      
          public static boolean subSetSum(int[] a, int k){
      
              if(a == null){
                  return false;
              }
      
              //n items in the list
              int n = a.length; 
              //create matrix m
              boolean[][] m = new boolean[n + 1][k + 1]; //n + 1 to include 0, k + 1 to include 0 
      
              //set first row of matrix to false. This also prevent array index out of bounds: -1
              for(int s = 0; s <= k; s++){
                  m[0][s] = false;
              }
      
              //populate matrix m
              for(int i = 1; i <= n; i++){
                  for(int s = 0; s <= k; s++){    
                      if(s - a[i-1] >= 0){ //when it goes left we don't want it to go out of bounds. (logic 4)
                          m[i][s] = (m[i-1][s] || a[i-1] == s || m[i-1][s - a[i-1]]); 
                      } else {
                          m[i][s] = (m[i-1][s] || a[i-1] == s);
                      }       
      
                  }
              }
      
              //print matrix
              print(m);
      
              return m[n][k];
      
          }
      
          private static void print(boolean[][] m){
              for(int i = 0; i < m.length; i++){
                  for(int j = 0; j < m[i].length; j++){
                      if(m[i][j]){
                          System.out.print("T");
                      } else {
                          System.out.print("F");
                      }           
                  }
                  System.out.print("\n");
              }
          }
      
          public static void main(String[] args){
              int[] array = {1,3,5,2,8};
              int k = 9;
      
              System.out.println(subSetSum(array,k));
      
          }
      }
      

      构建矩阵m取O((n + 1)(k + 1)),即O(nk)。它似乎应该是多项式但它不是!它实际上是伪多项式。阅读它here

      同样,这仅在输入仅包含正数时才有效。您可以轻松调整它以使用负数。矩阵仍然有n + 1行但B - A + 1列。其中B是上限,A是下限(+1包括零)。矩阵仍然是你必须用下限抵消s

      从头到尾很难解释文本上的DP问题。但我希望这可以帮助那些试图理解这个问题的人。

答案 1 :(得分:18)

由于看起来你的所有数字都是正数,你可以使用动态编程来解决这个问题:

Start将是一个大小为K + 1的布尔数组possible,第一个值为true,其余为false。第i个值将表示是否可以实现i的子集和。对于集合中的每个数字n,循环遍历possible数组,如果第i个值为真,则将第i + n值设置为true。

最后,如果possible中的第k个值为真,那么您可以形成k的子集和。 O(NK)时间问题解决了。

Wikipedia's page on the subset sum problem详细解释了此算法适用于不保证为正的整数集。

答案 2 :(得分:8)

我建议阅读Wiki的算法。那里存在算法,参见{{1>}解的伪多项式时间动态规划解,解不是多项式时间,是(p,n)中的多项式,但它不是n中的多项式+ log P(输入的大小),因为O(P*n)可能非常大,如2 ^ n,解P * n =(2 ^ n)* n一般不是多项式时间解,但是当p是由n的一些多项式函数限定的是多项式时间算法。

这个问题是NPC,但它有一个P算法,属于weakly NP-Complete个问题,还有Strongly NP-Complete个问题,这意味着你找不到任何问题Pseudo polynomial time算法除非P = NP,否则这个问题不在这个问题范围内,所以不知何故很容易。

我说这个尽可能简单,但它不是强NP完全或弱NP完全问题的确切定义。

有关详细信息,请参阅Garey and Johnson第4章。

答案 3 :(得分:3)

在一般情况下,没有已知的子集和算法,其运行小于O(2 ^(n / 2))。

答案 4 :(得分:3)

看来我晚会晚了,这是我的两分钱。我们将创建一个boolean[] solution[n+1][k+1],使solution[i][j]true,如果使用前i个项(索引0i-1)我们可以得出总和{ {1}};其他j。最后,我们将返回false

我们可以推断出以下几点:

  1. 如果sum为零,则对于任何数量的元素始终是一个可能的答案(空集)。一切如此。
  2. 如果set为空,我们将没有任何子集,因此无法获取任何K。因此,永远不可能给出答案。都是假的。
  3. 如果子集X1(X的子集,X中没有最后一个元素)具有k的子集和,则X也具有X1。例如。对于X1 = {1,3,5}和k = 8,如果X1具有子集和,则X = {1,3,5,7}也具有子集和
  4. 对于i / p,设置X = {1,3,5,7,19}且k = 20,如果X想知道20个子集和的可能性,则x1 = {1,3,5 ,7}可以具有20-19的子集和,即1。仅在k> = 19(即X中的最后一个元素)时适用。

基于以上几点,我们可以轻松编写如下算法。

solution[k][n]

答案 5 :(得分:2)

void subsetSum (int arr[], int size, int target) {
  int i, j ;
  int **table ;
  table = (int **) malloc (sizeof(int*) * (size+1)) ;
  for ( i = 0 ; i <= size ; i ++ ) {
    table[i] = (int *) malloc (sizeof(int) * (target+1)) ;
    table[i][0] = 1 ;
  }
  for ( j = 1 ; j <= target ; j ++ )
    table[0][j] = 0 ;
  for ( i = 1 ; i <= size ; i ++ ) {
    for ( j = 1 ; j <= target ; j ++ )
      table[i][j] = table[i-1][j] || (arr[i-1] <= j && table[i-1][j-arr[i-1]] ) ;
  } 
  if ( table[size][target] == 1 )
    printf ( "\ntarget sum found\n" ) ; 
  else printf ( "\nTarget sum do not found!\n" ) ;
  free (table) ;
}

答案 6 :(得分:0)

具有一维阵列的DP解决方案(DP阵列处理顺序在这里很重要)。

bool subsetsum_dp(vector<int>& v, int sum)
{
    int n = v.size();
    const int MAX_ELEMENT = 100;
    const int MAX_ELEMENT_VALUE = 1000;
    static int dp[MAX_ELEMENT*MAX_ELEMENT_VALUE + 1]; memset(dp, 0, sizeof(dp));

    dp[0] = 1;

    for (int i = 0; i < n; i++)
    {
        for (int j = MAX_ELEMENT*MAX_ELEMENT_VALUE; j >= 0; j--)
        {
            if (j - v[i] < 0) continue;
            if (dp[j - v[i]]) dp[j] = 1; 
        }
    }

    return dp[sum] ? true : false;
}

答案 7 :(得分:0)

让M为所有元素的总和。 注意,K <= M

let m be a Boolean array [0...M]
set all elements of m to be False
m[0]=1
for all numbers in the set let a[i] be the ith number
    for j = M to a[i]
        m[j] = m[j] | m[j-a[i]];

然后简单地测试m [k]

答案 8 :(得分:0)

boolean hasSubset(int arr[],int remSum,int lastElem){
    if(remSum==0) return true;
    else if(remSum!=0 && lastElem<0) return false;

    if(arr[lastElem]>remSum) return hasSubset(arr, remSum, lastElem-1);
    else return (hasSubset(arr, remSum, lastElem-1) ||hasSubset(arr, remSum-arr[lastElem], lastElem-1));
}

考虑第i个元素。要么它将为子集总和贡献,要么不会。如果它对总和有贡献,那么“和的值”减少等于第i个元素的值。如果它没有贡献,那么我们需要在剩余元素中搜索“和值”。

答案 9 :(得分:0)

上述答案都很棒但是并没有真正最全面地概述这样的事情对正数和负数都有效。

给定一组有序的整数,定义两个变量X和Y,使其

X =负面元素的总和

Y =正元素之和

并对初始集进行操作,就好像通过按顺序应用这些规则来递归二叉树一样

  1. 如果最右边的元素等于您要检查的总和 for return true
  2. 如果这样做不会留空,请向左递 set,从排序数组中删除最右边的元素
  3. 如果您的集合中还有一个元素,并且它不是并且返回false
  4. 不是向右递归,而是检查中所有元素的总和 数组q,如果X&lt; = B&lt; = Y则返回true,否则返回false
  5. 如果左子树或右'recursion'返回true,则返回true到父
  6. 上面的答案更加详细和准确,但是对于如何发挥这一点的广泛观点绘制了一棵二叉树。关于运行时间的长度是多少?

答案 10 :(得分:0)

function subsetsum(a, n) {
    var r = [];
    for (var i = parseInt(a.map(function() { return 1 }).join(''), 2); i; i--) {
        var b = i.toString(2).split('').reverse().map(function(v, i) {
            return Number(v) * a[i]
        }).filter(Boolean);
        if (eval(b.join('+')) == n) r.push(b);
    }
    return r;
}

var a = [5, 3, 11, 8, 2];
var n = 16;
console.log(subsetsum(a, n)); // -> [[3, 11, 2], [5, 3, 8], [5, 11]]

蛮力-忘记排序,尝试每一个组合,评估器就会击败Array.reduce(它也适用于负数)。

答案 11 :(得分:0)

时间复杂度为n ^ 2的递归解决方案

public void solveSubsetSum(){
    int set[] = {2,6,6,4,5};
            int sum = 9;
            int n = set.length;

            // check for each element if it is a part of subset whose sum is equal to given sum
            for (int i=0; i<n;i++){
                if (isSubsetSum(set, sum, i, n)){
                    Log.d("isSubset:", "true") ;
                    break;
                }
                else{
                    Log.d("isSubset:", "false") ;
                }
                k=0; // to print time complexity pattern
            }
        }

private boolean isSubsetSum(int[] set, int sum, int i, int n) {

            for (int l=0;l<k; l++){
            System.out.print("*"); 
            // to print no of time is subset call for each element
        }
        k++;
        System.out.println();     
        if (sum == 0){
            return true;
        }

        if (i>=n){
            return false;
        }

        if (set[i] <= sum){ 
        // current element is less than required sum then we have to check if rest of the elements make a subset such that its sum is equal to the left sum(sum-current element)
            return isSubsetSum(set, sum-set[i], ++i, n);
        }
        else { //if current element is greater than required sum
            return isSubsetSum(set, sum, ++i, n);
        }
   }

最坏情况的复杂度:O(n ^ 2)

最佳情况:O(n),即;如果第一个元素构成一个子集,其总和等于给定的总和。

如果我在这里计算时间复杂度有误,请纠正我。