我的一个朋友带一个从1到n(其中n> 0)的数字序列
在此序列中,他选择了两个数字a和b
他说a和b的乘积应等于序列中所有数字的和,不包括a和b
给出一个数字n,你能告诉我他从序列中排除的数字吗?
已经从Code Wars找到了该Kata的解决方案,但是当我运行它时,它在编辑器中超时(12秒后);还有什么想法我应该如何进一步优化嵌套的for循环或删除它?
function removeNb(n) {
var nArray = [];
var sum = 0;
var answersArray = [];
for (let i = 1; i <= n; i++) {
nArray.push(n - (n - i));
sum += i;
}
var length = nArray.length;
for (let i = Math.round(n / 2); i < length; i++) {
for (let y = Math.round(n / 2); y < length; y++) {
if (i != y) {
if (i * y === sum - i - y) {
answersArray.push([i, y]);
break;
}
}
}
}
return answersArray;
}
console.log(removeNb(102));
.as-console-wrapper { max-height: 100% !important; top: 0; }
答案 0 :(得分:8)
我认为填充数组后没有必要计算总和,可以在填充数组时进行计算。
function removeNb(n) {
let nArray = [];
let sum = 0;
for(let i = 1; i <= n; i++) {
nArray.push(i);
sum += i;
}
}
并且由于公式a * b = sum - a - b
的输入只能有两个数字a和b,因此每个数字只能有一个可能的值。因此,找到它们后无需继续循环。
if(i*y === sum - i - y) {
answersArray.push([i,y]);
break;
}
我建议以另一种方式看问题。
您正尝试使用此公式a * b = sum - a - b
查找两个数字a和b。
为什么不这样简化公式:
a * b + a = sum - b
a ( b + 1 ) = sum - b
a = (sum - b) / ( b + 1 )
然后,您只需要一个for循环即可产生b的值,请检查(sum-b)是否可被(b + 1)整除,并且除法是否会产生小于n的数字。
for(let i = 1; i <= n; i++) {
let eq1 = sum - i;
let eq2 = i + 1;
if (eq1 % eq2 === 0) {
let a = eq1 / eq2;
if (a < n && a != i) {
return [[a, b], [b, a]];
}
}
}
答案 1 :(得分:6)
您可以使用two pointers method(本书第77页)在线性时间内解决此问题。
为了获得对解决方案的直觉,让我们开始考虑代码的这一部分:
for(let i = Math.round(n/2); i < length; i++) {
for(let y = Math.round(n/2); y < length; y++) {
...
您已经发现这是您的代码中缓慢的部分。您正在尝试i
和y
的每种组合,但是如果不必尝试每种组合怎么办?
让我们举一个小例子来说明为什么不必尝试每种组合。
假设n == 10
,所以我们有1 2 3 4 5 6 7 8 9 10
,其中sum = 55
。
假设我们尝试的第一个组合是1*10
。
接下来尝试1*9
有意义吗?当然不是,因为我们知道1*10 < 55-10-1
,所以我们知道我们必须增加产品,而不是减少产品。
因此,让我们尝试2*10
。好吧,20 < 55-10-2
,所以我们仍然需要增加。
3*10==30 < 55-3-10==42
4*10==40 < 55-4-10==41
然后5*10==50 > 55-5-10==40
。现在我们知道我们必须减少产品。我们可以减少5
或减少10
,但是我们已经知道,减少5
是没有解决方案的(因为在上一步中已经尝试过了)。因此,唯一的选择是减少10
。
5*9==45 > 55-5-9==41
。同样,我们必须减少9
。
5*8==40 < 55-5-8==42
。现在我们必须再次增加...
您可以将上面的示例视为具有2个指针,这些指针被初始化为序列的开头和结尾。在每一步我们都
在开头,指针之间的区别是n-1
。在每一步中,指针之间的差异减少一。我们可以在指针相互交叉时停止(并说,如果到目前为止还没有找到解决方案,那么将无法获得解决方案)。因此很明显,在得出解决方案之前,我们只能进行n
个计算。这就是说关于n
的解是 linear 的意思。无论n
有多大,我们所做的计算都不会超过n
。将此与您的原始解决方案进行对比,实际上,随着n^2
的增长,我们最终进行了n
计算。
答案 2 :(得分:0)
哈桑是正确的,这是一个完整的解决方案:
function removeNb (n) {
var a = 1;
var d = 1;
// Calculate the sum of the numbers 1-n without anything removed
var S = 0.5 * n * (2*a + (d *(n-1)));
// For each possible value of b, calculate a if it exists.
var results = [];
for (let numB = a; numB <= n; numB++) {
let eq1 = S - numB;
let eq2 = numB + 1;
if (eq1 % eq2 === 0) {
let numA = eq1 / eq2;
if (numA < n && numA != numB) {
results.push([numA, numB]);
results.push([numB, numA]);
}
}
}
return results;
}
答案 3 :(得分:0)
这部分是评论,部分是答案。
用工程术语来说,最初发布的功能是使用“蛮力”解决问题,迭代每个(或超过所需的)可能的组合。迭代次数很大-如果您尽一切可能
n * (n-1) = bazillio n
少即是
因此,让我们看一下可以优化的事情,首先是一些小事情,我对第一个for
循环和nArray
感到有些困惑:
// OP's code
for(let i = 1; i <= n; i++) {
nArray.push(n - (n - i));
sum += i;
}
???您真的不使用nArray
做任何事情吗? Length
只是n ..我是否因此失去睡眠,所以想念某些东西?虽然可以使用1-n
循环来求和连续的整数for
序列,但是有一种避免循环的直接简便的方法:
sum = ( n + 1 ) * n * 0.5 ;
圈
// OP's loops, not optimized
for(let i = Math.round(n/2); i < length; i++) {
for(let y = Math.round(n/2); y < length; y++) {
if(i != y) {
if(i*y === sum - i - y) {
优化注意事项:
我看到您在某种程度上处于正确的轨道上 ,将自i, y
的起始值减少了一半。但是,您正在朝着相同的方向迭代它们:UP。而且,较低的数字看起来可以将其降低到n的一半以下(也许不是因为序列从1开始,我没有证实这一点,但确实如此)。
此外,我们希望在每次启动循环实例化时避免除法(即,将变量设置一次,并且还要对其进行更改)。最后,使用IF语句,i和y永远不会像我们将要创建循环的方式那样彼此相等,因此这是一个可能消失的条件。
但是更重要的是 遍历循环的方向 。较小的因子low
可能接近最低的循环值(约为n的一半),较大的因子hi
大概将接近于。如果我们有一些扎实的数学理论说“ hi永远不会小于0.75n”,那么我们可以制作几个mod来利用这些知识。
下面显示循环的方式,它们在高和低循环相遇之前中断并迭代。
此外,哪个循环选择较低或较高的数字无关紧要,因此我们可以使用它来缩短测试数字对时的内部循环,从而使每次循环变小。我们不想浪费时间多次检查同一对数字!较低因子的循环将从n的一半以下开始并上升,而较高因子的循环将从n开始并下降。
// Code Fragment, more optimized:
let nHi = n;
let low = Math.trunc( n * 0.49 );
let sum = ( n + 1 ) * n * 0.5 ;
// While Loop for the outside (incrementing) loop
while( low < nHi ) {
// FOR loop for the inside decrementing loop
for(let hi = nHi; hi > low; hi--) {
// If we're higher than the sum, we exit, decrement.
if( hi * low + hi + low > sum ) {
continue;
}
// If we're equal, then we're DONE and we write to array.
else if( hi * low + hi + low === sum) {
answersArray.push([hi, low]);
low = nHi; // Note this is if we want to end once finding one pair
break; // If you want to find ALL pairs for large numbers then replace these low = nHi; with low++;
}
// And if not, we increment the low counter and restart the hi loop from the top.
else {
low++;
break;
}
} // close for
} // close while
教程:
因此,我们设置了一些变量。请注意,low设置为略小于n的一半,因为更大的数字看起来可能会少几个点。此外,我们不舍入,而是截断,本质上是“总是舍入”,并且在性能上稍好一些(尽管在这种情况下仅分配一次就很重要)。
while循环从最低值开始并递增,可能一直到n-1。 hi FOR循环从n开始(复制到nHi),然后递减直到找到因子,或者它在低+ 1处截取。
条件: 第一个IF::如果我们高于总和,则退出,递减并继续以较低的hi因子值继续。 ELSE IF::如果我们相等,那就完成了,休息一下吃午饭。我们将low设置为nHi,这样当我们退出FOR循环时,我们还将退出WHILE循环。 ELSE:如果到达此处是因为我们小于总和,因此我们需要增加while循环并重置hi FOR循环以从n(nHi)重新开始。
答案 4 :(得分:0)
如果有兴趣,CY白羊座pointed this out:
ab + a + b = n(n + 1)/2
在两侧加1
ab + a + b + 1 = (n^2 + n + 2) / 2
(a + 1)(b + 1) = (n^2 + n + 2) / 2
因此,我们正在寻找(n^2 + n + 2) / 2
的因数,并有关于最小因数的迹象。这不一定意味着实际搜索的复杂性会大大提高,但这仍然很酷。