【问题标题】:Execution Timed Out (12000 ms): How can I optimize JS kata to run faster? [Upside-Down Numbers - Challenge Edition]执行超时(12000 毫秒):如何优化 JS kata 以更快地运行? [颠倒数字-挑战版]
【发布时间】:2022-01-20 19:57:43
【问题描述】:
    function upsideDown(x, y) {
      const list = ["0", "1", "6", "8", "9"];
      let count = 0;
      for (let i = parseInt(x); i <= parseInt(y); i++) {
        let curr = i.toString().split("");
        let upsideDownCurr = "";
        if (
          !curr.every(function (x) {
            return list.includes(x);
          })
        )
          continue; // Skips if number includes non-upDown num
        for (let j = 0; j < curr.length; j++)
          if (curr[j] === "6") upsideDownCurr = "9" + upsideDownCurr;
          else if (curr[j] === "9") upsideDownCurr = "6" + upsideDownCurr;
          else upsideDownCurr = `${curr[j]}` + upsideDownCurr;
        if (upsideDownCurr === curr.join("")) count++;
      }
      return count;
    }

输入:

您的函数将接收两个字符串,每个字符串由代表一个正整数的数字组成。这两个值将代表范围的上限和下限。 输出:

您的函数必须返回两个输入参数范围内的有效倒置数字的数量,包括上限和下限。 什么是倒置数字?

倒置的数字是一个整数,旋转 180 度后显示相同,如下图所示。

这工作正常,直到 Test.assertEquals(upsideDown('100000','12345678900000000'),718650) 知道如何优化代码吗?

【问题讨论】:

  • 所以“颠倒”的意思是“从左到右和从右到左一样”。有时我真的不喜欢那些“kata”的东西,因为术语不严谨。
  • @Pointy 不完全是。如果我理解得很好,例如 6 变成 9...
  • @Damien 好吧,“2”会变成什么?还是“5”?
  • 什么都没有,如果数字中有 2 或 5,它应该跳到下一个数字。只有 6 和 9 需要更改。
  • 您正在处理回文数字,但 69 位(我将称之为 X)不能是奇数位数字的中间数字。所以1X1 无效,但1XX1 有效并计为两个数字:16911961。有三个一位数:0、1、8。四个两位数:11、88、XX(假设带前导零的数字如 00 不算数)。要生成三位数字,请取两位数字并在中间放一个有效数字,因此 1y18y8XyX 其中y 是 0、1 或 8。依此类推...

标签: javascript algorithm


【解决方案1】:

编辑:根据 user3386109 的评论,阐明可翻转与可旋转(“倒置”或旋转 180 度)。

真正的问题是测试上限的剪切大小,约为 1.2 x 10^16。如果迭代每秒可以处理 10 亿个数字,那么该算法原样将需要约 12,000,000 秒的处理时间,即约 138 天。因此,为什么算法会超时...

如果将数字范围限制为可旋转的 5 位数字的排列,则排列数为 2 x 5^16 或 305,175,781,250。 (2 代表第一个数字,可以是 0 或 1,5^16 代表其余 16 个数字中的每一个的 5 种可能性(即 0、1、6、8、9)。)这更易于管理在合理的时间内计算...

因此,如果遵循遍历数字范围的路径,则只生成给定可旋转数字(即 0,1,6,8,9)的可能性,而不是遍历整个数字范围...

不过,更好的是,如果您只是在寻找完全可旋转的数字的计数,那么当您通过一些逻辑和数学运算就可以计算出刚才举例说明的可能性时,几乎不需要实际迭代该范围。更具体地说,假设您希望计算可旋转的 5 位数字的数量,即在 10000 - 99999 范围内。

  • 首先,可旋转数字必须分成“自旋转”和“镜像对”。即,0,1,8 自旋,6/9 形成镜像对。
  • 然后,简单地计算五个数字中每个数字的可能性:
    • 数字 4:可以是 1、6、8、9(4 种可能性)

    • 数字 3:可以是 0、1、6、8、9(5 种可能性)

    • 数字 2:只能是 0、1、8(3 种可能)这是 5 位数字的中心数字,因此不能是 6 或 9,因为中心数字必须是自旋的。

    • 第 1 位:必须与第 3 位相同。(1 种可能性)

    • 第 0 位:必须与第 4 位相同。(1 种可能性)

    • 所以,可旋转的 5 位数字的数量是......

      • 4 * 5 * 3 * 1 * 1 - 或 - 60 个可旋转数字。

以上只是基于所涉及的位数的算法。如果在问题中范围涉及一个 17 位数字,那么必须遍历数字范围,并在此过程中求和。即,

  • 1 位 (0 - 9):3 个可旋转数字

  • 2 位 (10 - 99):4 * 1 -或- 4 个可旋转数字

  • 3 位 (100 - 999):4 * 3 * 1 - 或 - 12 个可旋转数字

  • 4 位 (1000 - 9999):4 * 5 * 1 * 1 - 或 - 20 个可旋转数字

    o

    o

    o

  • 15 位数(100000000000000 - 999999999999999):4 * 5 * 5 * 5 * 5 * 5 * 5 * 3 * 1 * 1 * 1 * 1 * 1 * 1 * 1 -或- 187,500 个可旋转数字-

  • 16 位(1000000000000000 - 9999999999999999):4 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 1 * 1 * 1 * 1 * 1 * 1 * 1 * 1 - 或 - 312,500 个可旋转数字

我会在这里停下来,因为现在在处理最后一个范围时需要进一步考虑,因为通常数字的全宽(在问题的情况下为 17 位)不会运行整个范围17 位数。即,范围是从 10000000000000000 - 12345678900000000 而不是 99999999999999999。在这种情况下,仍然可以使用所提出的算法,但现在必须考虑到 17 位数字中每个数字施加的限制...

编辑 2:在之前提出的策略之后,下面的代码 sn-p 结合了计算每个数字范围的微调器的逻辑,直到数字的全宽和数字全宽的微调器。也就是说,这必然是一个两阶段算法,因为在处理数字的全宽度时,第二阶段必须考虑到数字的尾随数字可能不一定在开头的可能可旋转数字范围内号码。因此,第二阶段必须在每个增加分辨率的范围内计算微调器...

代码 sn-p 的控制台输出可能比试图解释第二阶段背后概念的一段文字更能说明问题,尤其是数字 189942 的示例。

function calcSpinners( x ) {
  const selfies = [ 0, 1, 8 ];
  const mirrors = [ [ 6, 9 ] ];
  
  // Stage 1.  Walk from 1 digit up to n - 1 digits, calculating the number
  // of possible spinners at each step and adding to the total
  let n = x.toString().length;
  let sum1 = 0;
  
  for ( let i = 1; i < n; i++ ) {
    let count = 1;
    // If an even number of digits, then there is no center digit.  If an odd
    // number of digits, then let's factor in the number of possible selfies.
    count = count * ( i % 2 === 0 ? 1 : selfies.length );
    
    let halfDigits = i / 2 |0;
    if ( 0 < halfDigits ) {
      count = count * ( selfies.filter( sx => sx !== 0 ).length + mirrors.length * 2 );
      count = count * ( selfies.length + mirrors.length * 2 ) ** ( halfDigits - 1 );
    }
    console.log( `Range ${10n ** BigInt( i - 1)} - ${10n ** BigInt( i ) - 1n} has ${count} spinners` );
    sum1 += count;
  }
  
  console.log( `Subtotal of spinners from 1 to ${n-1} digits: ${sum1}` );
  
  // Stage 2:  Now we're just dealing with the full width of x.  Eg, if x is 17
  // digits, then the evaluation of the spinnable numbers is evaluated at each
  // range of increasing resolution of x, always 17 digits.
  
  let sum2 = 0;
  n = BigInt( x.toString().length );
  let loopTill = n / 2n + ( n % 2n );
  
  for ( let i = 0n; i <= loopTill; i++ ) {
    let dhi = 10n ** ( n - i - 1n );
    let dlo = dhi * 10n
    
    let hi = ( i === loopTill ? x : ( x / dhi ) * dhi - 1n );
    let lo = ( i === 0n ? dhi : ( x / dlo ) * dlo );
    
    count = 0;
    if ( lo < hi ) {
      let hiNum = hi.toString().split( '' ).map( x => parseInt( x ) );
      let loNum = lo.toString().split( '' ).map( x => parseInt( x ) );

      // If an even number of digits, then there is no center digit.  If an odd
      // number of digits, then let's factor in the number of possible selfies.
      let center = hiNum.length / 2 |0;
      count = ( hiNum.length % 2 === 0 ? 1 : selfies.filter( sx => loNum[ center ] <= sx && sx <= hiNum[ center ] ).length );

      for ( let j = 0; j < center; j++ ) {
        hiNumHiDigit = hiNum[ j ];
        hiNumLoDigit = hiNum[ hiNum.length - 1 - j ];
        loNumHiDigit = loNum[ j ];
        loNumLoDigit = loNum[ loNum.length - 1 - j ];
        
        // For the digit in question, count the number of possible selfie and
        // mirror digits.
        let eligible =
          selfies.filter( sx =>
            loNumHiDigit <= sx && sx <= hiNumHiDigit
            && loNumLoDigit <= sx && sx <= hiNumLoDigit
            && !( sx === 0 && j === 0 )
          ).length
          + mirrors.filter( pair =>
            ( loNumHiDigit <= pair[ 0 ] && pair[ 0 ] <= hiNumHiDigit
            && loNumLoDigit <= pair[ 1 ] && pair[ 1 ] <= hiNumLoDigit )
          ).length 
          + mirrors.filter( pair =>
            ( loNumHiDigit <= pair[ 1 ] && pair[ 1 ] <= hiNumHiDigit
            && loNumLoDigit <= pair[ 0 ] && pair[ 0 ] <= hiNumLoDigit )
          ).length; 
        
        count = count * eligible;
      }
      
      console.log( `Range ${lo} - ${hi} has ${count} spinners.` );
      sum2 += count;
    }
  }
  
  console.log( `Subtotal of spinners of ${n} digits up to and including ${x}: ${sum2}` );
  console.log( `Total spinners: ${sum1 + sum2}` );
  return sum1 + sum2;
  
}

let loVal = 100000n;
let hiVal = 12345678900000000n;
console.log( `Calculating spinner count for ${loVal}.` );
let loTotal = calcSpinners( loVal );
console.log( `${loVal} has ${loTotal} spinners.` );
console.log( `` );

console.log( `Calculating spinner count for ${hiVal}.` );
let hiTotal = calcSpinners( hiVal );
console.log( `${hiVal} has ${hiTotal} spinners.` );
console.log( `` );

console.log( `Between ${loVal} - ${hiVal} there are ${hiTotal - loTotal} spinners.` );
console.log( `` );
console.log( `` );

let testVal = 189942n;
console.log( `Calculating spinner count for ${testVal}.` );
let testTotal = calcSpinners( testVal );
console.log( `${testVal} has ${testTotal} spinners.` );
console.log( `` );

底线是,虽然这个算法更复杂,但它在最短的时间内解决了问题,因为它没有尝试迭代输入数字的整个数字范围(或可旋转数字的组合)......

这个问题是一个很好的例子,说明必须重构的不是代码,而是底层算法,因为蛮力方法无法在经典计算机上扩展......

【讨论】:

  • 我不认为“垂直翻转”和“旋转 180 度”是一样的。例如,垂直翻转的 819 是 816,但 6 会向后翻转。 819 旋转 180 度是 618,而 6 方向正确。当然,816 在任何一种方法中都不是有效的“倒置”数字,因为在翻转或旋转之后它就不一样了。一个有效的倒置数字是 986,因为旋转 180 度会得到 986。
  • @user3386109 澄清的好点,我会相应地调整响应。它仍然没有否定提出的提高性能的一般策略(问题的核心),通过否决票,很明显有人无法透过树木看到森林......这是我提出的问题之一讨论在不知道 OP 正在寻求多少指导的情况下采取多远的答案......
  • 是的,一般策略通过将基数从 10 减半到 5 大大减少了问题的规模。意识到数字是回文数,将数字的数量从 16 减半到 8。将两者结合起来,你只有5^8 = 390625 个要处理的数字。投赞成票。干杯
猜你喜欢
  • 1970-01-01
  • 2022-12-21
  • 2022-12-07
  • 2023-02-05
  • 2021-10-08
  • 1970-01-01
  • 2016-02-27
  • 2015-05-22
  • 1970-01-01
相关资源
最近更新 更多