查找加起来为给定总和的最小子数组(允许重复)

时间:2019-04-10 20:46:16

标签: java arrays algorithm

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

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

对于我来说,我想只找到 个最小的子数组,并允许从输入数组中复制数据。

例如,给定[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]。 br /> 我了解为什么我的算法错误,但是不确定如何解决。

4 个答案:

答案 0 :(得分:1)

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

答案 1 :(得分:1)

术语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

答案 2 :(得分:1)

由于您使用的是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是中间可能的总数。

提示:

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

答案 3 :(得分:1)

广度优先(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的列表也是最短的长度(因为队列中的所有列表都是按长度的升序评估的)。您可以通过添加启发式方法来选择首先评估哪个相同长度列表(例如,最接近总和的那个)来提高运行时间。但是,这仅适用于特定输入,并且最坏情况下的复杂性将保持不变。