给定我有一个数字数组,例如[14,6,10]-如何找到可以加到给定目标值的可能组合/对。
例如,我有 [14,6,10] ,我正在寻找目标值为 40 我的预期输出将是
10 + 10 + 6 + 14
14 + 14 + 6 + 6
10 + 10 + 10 + 10
*顺序并不重要
话虽如此,这是我到目前为止所尝试的:
function Sum(numbers, target, partial) {
var s, n, remaining;
partial = partial || [];
s = partial.reduce(function (a, b) {
return a + b;
}, 0);
if (s === target) {
console.log("%s", partial.join("+"))
}
for (var i = 0; i < numbers.length; i++) {
n = numbers[i];
remaining = numbers.slice(i + 1);
Sum(remaining, target, partial.concat([n]));
}
}
>>> Sum([14,6,10],40);
// returns nothing
>>> Sum([14,6,10],24);
// return 14+10
实际上是无用的,因为只有在该数字只能使用一次求和时才会返回。
那怎么办?
答案 0 :(得分:5)
只要总和小于所需总和,就可以添加实际索引的值,或者继续下一个索引。
function getSum(array, sum) {
function iter(index, temp) {
var s = temp.reduce((a, b) => a + b, 0);
if (s === sum) result.push(temp);
if (s >= sum || index >= array.length) return;
iter(index, temp.concat(array[index]));
iter(index + 1, temp);
}
var result = [];
iter(0, []);
return result;
}
console.log(getSum([14, 6, 10], 40));
.as-console-wrapper { max-height: 100% !important; top: 0; }
要获得有限的结果集,可以指定长度并在退出条件下进行检查。
function getSum(array, sum, limit) {
function iter(index, temp) {
var s = temp.reduce((a, b) => a + b, 0);
if (s === sum) result.push(temp);
if (s >= sum || index >= array.length || temp.length >= limit) return;
iter(index, temp.concat(array[index]));
iter(index + 1, temp);
}
var result = [];
iter(0, []);
return result;
}
console.log(getSum([14, 6, 10], 40, 5));
.as-console-wrapper { max-height: 100% !important; top: 0; }
答案 1 :(得分:4)
TL&DR:跳至第二部分了解真实情况
@Nina Scholz对这个基本问题的回答只是向我们展示了算法的美好表现。老实说,这让我很困惑,有两个原因
[14,6,10,7,3]
尝试500
时,它对iter
函数进行了36,783,575次调用,而不会破坏调用堆栈。但是内存根本没有显示任何重要用途。因此,我搁置了解决方案,而是开始在开发工具上进一步研究她的代码,发现它既漂亮又有缺点。
首先,我相信这种算法方法(其中包括对递归的非常巧妙的使用)可能值得称呼。这是非常高效的内存,并且仅对构造的结果集使用内存。堆栈会动态地不断增长和缩小,直到达到极限为止。
问题是,尽管效率很高,但仍会进行大量的冗余调用。因此,经过稍微修改后,可以将对iter
的36,783,575个调用缩减为20,254,744个……类似于45%,这将产生更快的代码。关键是输入数组必须升序排列。
因此,这是Nina算法的修改版本。 (请耐心等待。完成过程大约需要25秒)
function getSum(array, sum) {
function iter(index, temp) {cnt++ // counting iter calls -- remove in production code
var s = temp.reduce((a, b) => a + b, 0);
sum - s >= array[index] && iter(index, temp.concat(array[index]));
sum - s >= array[index+1] && iter(index + 1, temp);
s === sum && result.push(temp);
return;
}
var result = [];
array.sort((x,y) => x-y); // this is a very cheap operation considering the size of the inpout array should be small for reasonable output.
iter(0, []);
return result;
}
var cnt = 0,
arr = [14,6,10,7,3],
tgt = 500,
res;
console.time("combos");
res = getSum(arr,tgt);
console.timeEnd("combos");
console.log(`source numbers are ${arr}
found ${res.length} unique ways to sum up to ${tgt}
iter function has been called ${cnt} times`);
尽管我对性能印象深刻,但是我对上述解决方案感到不满意,但没有明确的理由可以提及。它对副作用的工作方式以及很难理解的双重递归,都让我感到困扰。
因此,我来回答这个问题。尽管我要在JS中使用函数,但与公认的解决方案相比,这效率要高出许多倍。我们仍然有空间用丑陋的命令性方法使其更快一点。
区别是;
给出数字:[14,6,10,7,3]
目标总和:500
接受的答案:
下面的答案
function items2T([n,...ns],t){cnt++ //remove cnt in production code
var c = ~~(t/n);
return ns.length ? Array(c+1).fill()
.reduce((r,_,i) => r.concat(items2T(ns, t-n*i).map(s => Array(i).fill(n).concat(s))),[])
: t % n ? []
: [Array(c).fill(n)];
};
var cnt = 0, result;
console.time("combos");
result = items2T([14, 6, 10, 7, 3], 500)
console.timeEnd("combos");
console.log(`${result.length} many unique ways to sum up to 500
and ${cnt} recursive calls are performed`);
另一个重要的一点是,如果给定的数组降序排列,则递归迭代的次数将减少(有时会大大减少),从而使我们可以从柠檬中榨出更多汁。输入数组降序排列时,将上面的内容与下面的内容进行比较。
function items2T([n,...ns],t){cnt++ //remove cnt in production code
var c = ~~(t/n);
return ns.length ? Array(c+1).fill()
.reduce((r,_,i) => r.concat(items2T(ns, t-n*i).map(s => Array(i).fill(n).concat(s))),[])
: t % n ? []
: [Array(c).fill(n)];
};
var cnt = 0, result;
console.time("combos");
result = items2T([14, 10, 7, 6, 3], 500)
console.timeEnd("combos");
console.log(`${result.length} many unique ways to sum up to 500
and ${cnt} recursive calls are performed`);