【问题标题】:Finding the smallest sub-array that adds up to a given sum (Duplicates are allowed)找到加起来等于给定总和的最小子数组(允许重复)
【发布时间】:2019-04-10 20:46:16
【问题描述】:

我想找到与给定目标相加的最小子数组。
我的输入是输入 array 和目标 sum.

我知道这个问题已经被问过很多次了,但在大多数情况下,人们试图找到每一个可能的组合,这些组合加起来就是他们的目标,或者他们的解决方案不允许重复。

就我而言,我想只找到最小的子数组,并且允许从输入数组复制数据。

例如,给定输入数组[1,4,10,20,35] 和目标17,我希望输出数组[10,4,1,1,1]
因此,我的算法可以在查找输出时复制输入数组中的任何值。

这是我目前所拥有的:

public static ArrayList<Integer> representNumberAsSumOfArrayElements(ArrayList<Integer> inputs, int target)
{
    ArrayList<Integer> result = new ArrayList<>();

    //First sort the input array in descending order
    Collections.sort(inputs, Collections.reverseOrder());
    int i = 0;

    //Iterate through the input array and break down the target into a sum of the input array
    while (i < inputs.size() && target > 0) {
        if(target >= inputs.get(i) ) {
            result.add(inputs.get(i));
            target = target - inputs.get(i);
        } else {
            i++;
        }
    }
    return result;
}

只需一个简单的驱动程序即可在 100 个目标上测试代码:

public static void main(String[] args) {
    ArrayList<Integer> inputs = new ArrayList<>(Arrays.asList( 1, 4, 10, 20, 35, 56, 84));
    int n = 100;
    ArrayList<Integer> sumArray = new ArrayList<>();
    for (int i = 0; i <= n; i++)
    {
        sumArray = representNumberAsSumOfArrayElements(inputs, i); // O(n)
        System.out.println(i + " written as a sum of array elements " + sumArray);
    }
}

我已经实现了适用于大多数值的 O(n) 算法,但在某些情况下,我得到了错误的结果。
例如,当我以[1,4,10,20,35,56,84] 的输入和69 的目标总和运行我的代码时,正确的输出将是[35,20,10,4],但我的算法输出[56,10,1,1,1]
我明白为什么我的算法是错误的,但我不知道如何解决它。

【问题讨论】:

  • 允许重复的部分使它有点复杂。 I have done this in JS。我想这是一个非常有效的算法,很容易转换成 Java。唯一的事情是它给了你所有的可能性。你只剩下过滤最短的了。
  • @Aaron 我不认为该算法允许多次使用一个项目。事实上,它甚至不允许你使用不连续的项目。
  • 我刚刚测试了我上面评论中提到的代码,对你给定的集合 [1,4,10,20,35,56,84] 和目标是 69 。只需使用result.reduce((p,c) =&gt; p.length &lt;= c.length ? p : c); 过滤结果集,它会在 32 毫秒内返回[4, 10, 20, 35]。再次抱歉,我没有任何 Java 知识来展示它只是 JavaScript。
  • @redu 这很有帮助。我精通 JS,所以我会尝试用 Java 重写它。

标签: java arrays algorithm


【解决方案1】:

首先创建一个前缀和数组,使得 prefSum[i] 给出给定数组从索引 0 到 i(包括)的所有元素的总和。如果你的数组包含所有正整数,则 prefSum 数组被排序,你可以做二分查找。 因此,从 0 到 length 扫描 prefSum 数组,并在 0 到 (i-1) 之间进行二进制搜索 如果您当前的索引是 i 并尝试找到最大的 j 其中 j 在 0 到 i-1 之间 这样 prefSum[i]-prefSum[j]=给定目标。 总体复杂度为 nlogn。

【讨论】:

  • 因此,[1,4,10,20,35] 的 prefSum 将是 [2,5,11,21,36,8,14,24,39,29,39,45, 40,55] 还是我不理解您的回复?
  • prefSum 将是 [1,5,15,35,70]
  • j的值怎么办?
  • j 用于在 prefSum 的帮助下得到子数组 arr[j+1] 到 arr[i] 的总和
  • 我的复杂度应该是 c * n,其中 c 是输入数组的大小,n 是目标?
【解决方案2】:

术语subarray 通常假定数组是连续的(这就是为什么有些回答者意味着另一个问题),但是您的排序告诉我们情况并非如此,您需要以任意顺序的最小项目列表来解决子集总和重复的问题。

您可以通过 动态编程 使用表格方法解决当前问题(而您的代码利用贪婪方法 - 在一般情况下不适用)。要获得最小的子集,您只需要选择具有较短值列表的子问题解决方案。

似乎这个 Python 代码工作正常(没有经过很好的测试)。

def best(lst, summ):
    lst = sorted(lst, reverse = True)
    a = [[] for _ in range(summ + 1)]  # list of lists
    a[0].append(-1)             # fake value to provide  valid zero entry
    for l in lst:
        for i in range(summ + 1 - l):
            t = len(a[i])
            if t:
                if (len(a[i + l]) == 0) or (t < len(a[i + l])):
                    a[i + l] = a[i] +[l]   # concatenate lists
    return a[summ][1:]   #remove fake -1

 for s in range(55, 71):
     print(s, best([1,4,10,20,35,56,84], s))

55 [35, 20]
56 [56]
57 [56, 1]
58 [56, 1, 1]
59 [35, 20, 4]
60 [56, 4]
61 [56, 4, 1]
62 [56, 4, 1, 1]
63 [35, 20, 4, 4]
64 [56, 4, 4]
65 [35, 20, 10]
66 [56, 10]
67 [56, 10, 1]
68 [56, 10, 1, 1]
69 [35, 20, 10, 4]
70 [35, 35]

请注意,我们不需要自己存储列表 - 我添加它们是为了调试和简化。我们只需要存储给定总和中的最后一个附加值和项目计数。
展开列表的解决方案:

def best1(lst, summ):
    a = [(0,0)] * (summ + 1)   # list contains tuples (value, bestcount)
    a[0] = (-1,1)
    for l in lst:
        for i in range(summ + 1 - l):
            t = a[i][1]
            if t:
                if (a[i + l][1] == 0) or (t < a[i + l][1]):
                    a[i + l] = (l, t + 1)
    res = []
    t = summ
    while a[t][1] > 1:
        res.append(a[t][0])
        t = t - a[t][0]

    return res

【讨论】:

    【解决方案3】:

    广度优先 (BFS) 方法的复杂度为 O(n*k),其中 n 是数组中唯一元素的数量,k 是最短答案的长度。伪代码如下:

    1. remove duplicates from the input array, A:
       can be done by copying it into a set in O(|A|)
    2. build a queue of lists Q;
       store the sum of elements as its 0th element, 
       and add an initially-empty list with a sum of 0
    3. while Q is not empty, 
         extract the first list of Q, L
         for each element e in A,
           if L[0] + e == sum, 
               you have found your answer: the elements of L with e
           if L[0] + e < sum, 
               insert a new list (L[0] + e, elements of L, e) at the end of Q
    4. if you reach this point, there is no way to add up to the sum with elements of A
    

    不使用列表的第 0 个元素作为总和会产生重新计算其元素总和的成本。从这个意义上说,存储总和是一种动态编程形式(= 重复使用先前的答案以避免重新计算它们)。

    这保证了添加到sum 的第一个列表也是可能的最短长度(因为队列中的所有列表都按长度升序进行评估)。您可以通过添加启发式方法来选择首先评估哪个相同长度的列表(例如,哪个最接近总和)来改善运行时间;但是,这仅适用于特定输入,并且最坏情况的复杂性将保持不变。

    【讨论】:

      【解决方案4】:

      由于您使用的是Java,我将在Java 中添加一个实现,使用动态编程 (在这种情况下是递归)


      代码

      SubSumDupComb.java:

      import java.util.*;
      
      public class SubSumDupComb {
          /**
           * Find shortest combination add to given sum.
           *
           * @param arr input array,
           * @param sum target sum,
           * @return
           */
          public static int[] find(int[] arr, int sum) {
              // System.out.printf("input arr: %s, sum: %d\n", Arrays.toString(arr), sum);
              List<Integer> list = find(arr, 0, sum, new ArrayList<>());
              // System.out.printf("result: %s\n", list);
      
              return listToArray(list);
          }
      
          /**
           * Find shortest combination add to given sum, start from given index.
           *
           * @param arr        input array,
           * @param start      start index, for further search,
           * @param sum        remain sum,
           * @param prefixList prefix list,
           * @return
           */
          private static List<Integer> find(int[] arr, int start, int sum, List<Integer> prefixList) {
              if (sum == 0) return prefixList; // base case,
              if (start >= arr.length || sum < 0) return null; // bad case,
      
              // exclude current index,
              List<Integer> shortestExcludeList = find(arr, start + 1, sum, prefixList);
      
              // include current index,
              List<Integer> includePrefixList = new ArrayList<>(prefixList);
              includePrefixList.add(arr[start]);
              List<Integer> shortestIncludeList = find(arr, start, sum - arr[start], includePrefixList);
      
              if (shortestIncludeList == null && shortestExcludeList == null) return null; // both null,
              if (shortestIncludeList != null && shortestExcludeList != null) // both non-null,
                  return shortestIncludeList.size() < shortestExcludeList.size() ? shortestIncludeList : shortestExcludeList; // prefer to include elements with larger index,
              else return shortestIncludeList == null ? shortestExcludeList : shortestIncludeList; // exactly one null,
          }
      
          /**
           * Find shortest combination add to given sum, with cache.
           *
           * @param arr input array,
           * @param sum target sum,
           * @return
           */
          public static int[] findWithCache(int[] arr, int sum) {
              // System.out.printf("input arr: %s, sum: %d\n", Arrays.toString(arr), sum);
              List<Integer> list = findWithCache(arr, 0, sum, new ArrayList<>(), new HashMap<>());
              // System.out.printf("result: %s\n", list);
      
              return listToArray(list);
          }
      
          /**
           * Find shortest combination add to given sum, start from given index, with cache.
           *
           * @param arr        input array,
           * @param start      start index, for further search,
           * @param sum        remain sum,
           * @param prefixList prefix list,
           * @return
           */
          private static List<Integer> findWithCache(int[] arr, int start, int sum, List<Integer> prefixList, Map<Integer, Map<Integer, List<Integer>>> cache) {
              if (sum == 0) return prefixList; // base case,
              if (start >= arr.length || sum < 0) return null; // bad case,
      
              // check cache,
              Map<Integer, List<Integer>> cacheAtStart;
              if ((cacheAtStart = cache.get(start)) != null && cacheAtStart.containsKey(sum)) { // cache hit, tips: the cashed list could be null, which indicate no result,
                  // System.out.printf("hit cache: start = %d, sum = %d, cached list: %s\n", start, sum, cacheAtStart.get(sum));
                  return cacheAtStart.get(sum);
              }
      
              // exclude current index, tips: should call this first,
              List<Integer> shortestExcludeList = findWithCache(arr, start + 1, sum, prefixList, cache);
      
              // include current index,
              List<Integer> includePrefixList = new ArrayList<>(prefixList);
              includePrefixList.add(arr[start]);
              List<Integer> shortestIncludeList = findWithCache(arr, start, sum - arr[start], includePrefixList, cache);
      
              List<Integer> resultList;
      
              if (shortestIncludeList == null && shortestExcludeList == null) resultList = null; // both null,
              else if (shortestIncludeList != null && shortestExcludeList != null) // both non-null,
                  resultList = shortestIncludeList.size() < shortestExcludeList.size() ? shortestIncludeList : shortestExcludeList; // prefer to include elements with larger index,
              else
                  resultList = (shortestIncludeList == null ? shortestExcludeList : shortestIncludeList); // exactly one null,
      
              // add to cache,
              if (cacheAtStart == null) { // init cache at given start,
                  cacheAtStart = new HashMap<>();
                  cache.put(start, cacheAtStart);
              }
              cacheAtStart.put(sum, resultList == null ? null : resultList); // add this result to cache,
              // System.out.printf("add cache: start = %d, sum = %d, list: %s\n", start, sum, resultList);
      
              return resultList;
          }
      
          /**
           * List to array.
           *
           * @param list
           * @return
           */
          private static int[] listToArray(List<Integer> list) {
              if (list == null) return null; // no solution,
      
              // list to array,
              int[] result = new int[list.size()];
              for (int i = 0; i < list.size(); i++) {
                  result[i] = list.get(i);
              }
      
              return result;
          }
      }
      

      SubSumDupCombTest.java:
      (测试用例,通过TestNG

      import org.testng.Assert;
      import org.testng.annotations.BeforeClass;
      import org.testng.annotations.Test;
      
      import java.util.Arrays;
      
      public class SubSumDupCombTest {
          private int[] arr;
          private int sum;
          private int[] expectedResultArr;
      
          private int[] arr2;
          private int sum2;
          private int sum2NoSolution;
          private int[] expectedResultArr2;
      
          @BeforeClass
          public void setUp() {
              // init - arr,
              arr = new int[]{1, 4, 10, 20, 35};
              sum = 17;
              expectedResultArr = new int[]{1, 4, 4, 4, 4};
              Arrays.sort(expectedResultArr);
      
              // init - arr2,
              arr2 = new int[]{14, 6, 10};
              sum2 = 40;
              sum2NoSolution = 17;
              expectedResultArr2 = new int[]{10, 10, 10, 10};
              Arrays.sort(expectedResultArr2);
          }
      
          @Test
          public void test_find() {
              // arr
              int[] resultArr = SubSumDupComb.find(arr, sum);
              Arrays.sort(resultArr);
              Assert.assertTrue(Arrays.equals(resultArr, expectedResultArr));
      
              // arr2
              int[] resultArr2 = SubSumDupComb.find(arr2, sum2);
              Arrays.sort(resultArr2);
              Assert.assertTrue(Arrays.equals(resultArr2, expectedResultArr2));
          }
      
          @Test
          public void test_find_noSolution() {
              Assert.assertNull(SubSumDupComb.find(arr2, sum2NoSolution));
          }
      
          @Test
          public void test_findWithCache() {
              // arr
              int[] resultArr = SubSumDupComb.findWithCache(arr, sum);
              Arrays.sort(resultArr);
              Assert.assertTrue(Arrays.equals(resultArr, expectedResultArr));
      
              // arr2
              int[] resultArr2 = SubSumDupComb.findWithCache(arr2, sum2);
              Arrays.sort(resultArr2);
              Assert.assertTrue(Arrays.equals(resultArr2, expectedResultArr2));
          }
      
          @Test
          public void test_findWithCache_noSolution() {
              Assert.assertNull(SubSumDupComb.findWithCache(arr2, sum2NoSolution));
          }
      }
      

      说明:

      • find()
        这是纯粹的递归。
        复杂性:

        • 时间:O(t^n) // 在最坏的情况下,
        • 空格O(n) // 被递归方法栈使用,

        地点:

        • t,是包含一个元素的平均时间。
      • findWithCache()
        为每对 (start, sum) 使用缓存。
        复杂性:

        • 时间:O(n * s)
        • 空格O(n * s) // 被缓存和递归方法栈使用,

        地点:

        • s,是中间可能总和的计数。

      提示:

      • 当有多个可能的最短结果时,结果更喜欢具有较大索引的数字。

      【讨论】:

        猜你喜欢
        • 2022-10-12
        • 2020-06-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-04-26
        • 1970-01-01
        • 2016-05-27
        相关资源
        最近更新 更多