在Java中,我应该如何找到一个数组元素的最接近(或相等)可能的总和到一个特定的值K?
例如,对于数组 {19,23,41,5,40,36} 和 K=44,最接近的可能和是 23+19=42。我已经为此苦苦挣扎了几个小时;我对动态编程几乎一无所知。顺便说一句,数组只包含正数。
在Java中,我应该如何找到一个数组元素的最接近(或相等)可能的总和到一个特定的值K?
例如,对于数组 {19,23,41,5,40,36} 和 K=44,最接近的可能和是 23+19=42。我已经为此苦苦挣扎了几个小时;我对动态编程几乎一无所知。顺便说一句,数组只包含正数。
对于此类问题,您通常会使用动态编程。但是,这基本上归结为保留一组可能的总和并逐个添加输入值,如下面的代码所示,并且具有相同的渐近运行时间O(n K)
:.n
K
然而,下面版本中的常量可能更大,但我认为代码比动态编程版本更容易理解。
public class Test {
public static void main(String[] args) {
int K = 44;
List<Integer> inputs = Arrays.asList(19,23,41,5,40,36);
int opt = 0; // optimal solution so far
Set<Integer> sums = new HashSet<>();
sums.add(opt);
// loop over all input values
for (Integer input : inputs) {
Set<Integer> newSums = new HashSet<>();
// loop over all sums so far
for (Integer sum : sums) {
int newSum = sum + input;
// ignore too big sums
if (newSum <= K) {
newSums.add(newSum);
// update optimum
if (newSum > opt) {
opt = newSum;
}
}
}
sums.addAll(newSums);
}
System.out.println(opt);
}
}
编辑
关于运行时间的简短说明可能很有用,因为我只是O(n K)
毫无理由地声称。
显然,初始化和打印结果只需要恒定的时间,所以我们应该分析双循环。
外循环遍历所有输入,因此它的主体是执行n
次数。
到目前为止,内部循环遍历所有总和,理论上这可能是一个指数数。但是,我们使用 的上限K
,因此 中的所有值sums
都在范围内[0, K]
。由于sums
是一个集合,它最多包含K+1
元素。
内部循环内的所有计算都需要恒定的时间,因此整个循环需要O(K)
. 出于同样的原因,该集合newSums
也最多包含K+1
元素,因此addAll
最终O(K)
也需要。
总结:外循环执行n
次数。循环体采用O(K)
. 因此,算法在O(n K)
.
编辑 2
根据要求如何找到导致最佳总和的元素:
而不是跟踪单个整数 - 子列表的总和 - 您还应该跟踪子列表本身。如果您创建一个新类型,这相对简单(没有 getter/setter 以保持示例简洁):
public class SubList {
public int size;
public List<Integer> subList;
public SubList() {
this(0, new ArrayList<>());
}
public SubList(int size, List<Integer> subList) {
this.size = size;
this.subList = subList;
}
}
初始化现在变为:
SubList opt = new SubList();
Set<SubList> sums = new HashSet<>();
sums.add(opt);
内部循环sums
也需要一些小的调整:
for (Integer input : inputs) {
Set<SubList> newSums = new HashSet<>();
// loop over all sums so far
for (SubList sum : sums) {
List<Integer> newSubList = new ArrayList<>(sum.subList);
newSubList.add(input);
SubList newSum = new SubList(sum.size + input, newSubList);
// ignore too big sums
if (newSum.size <= K) {
newSums.add(newSum);
// update optimum
if (newSum.size > opt) {
opt = newSum;
}
}
}
sums.addAll(newSums);
}
我会说首先对数组进行排序。那么您的示例将是:arr = {5, 19, 23, 36, 40, 41}。然后: 1) 取 arr[0] 和 arr[i],其中 i = arr.Size。求和并记录总和与 K 的差,如果总和小于 K。 2) 如果 sum > K,执行步骤 1,但不要使用 arr[i],而是使用 arr[i-1],因为我们想要降低我们的总和。如果 sum < K,则执行第 1 步,但不要使用 arr[0],而是使用 arr[1],因为我们想要增加总和。不断重复第 2 步,增加或减少索引,直到两个元素的索引相等。然后,我们知道导致总和和 K 之间差异最小的元素对。
----------------针对解决方案中任意数量的元素进行了编辑----------------
我相信你可能需要一棵树。这就是我的想法:
1)选择一个数字作为您的顶级节点。
2)对于集合中的每个数字,创建一个子节点,对于创建的每个分支,计算该分支的总和。
3) 如果总和小于 K,我们再次分支,为集合中的所有元素创建子节点。如果总和大于 K,我们停止,保持总和与 K 之间的差(如果总和 < K)。如果我们找到一个总和更好的分支,那么我们保留那个分支。重复此过程,直到所有分支都完成分支。
使用不同的顶部节点执行步骤 1-3。
private int closestSum(int[] a, int num){
int k=a.length-1;
int sum=0;
Arrays.sort(a);
while(a[k]>num){
k--;
}
for(int i=0;i<k;i++){
for(int j=i+1;j<=k;j++){
if(a[i]+a[j]<=num && a[i]+a[j]>sum)
sum = a[i]+a[j];
}
}
return sum;
}