【问题标题】:Optimal and efficient solution for the heavy number calculation?重数计算的最佳高效解决方案?
【发布时间】:2016-11-14 00:24:16
【问题描述】:

我需要找到两个整数AB 之间的重整数个数,其中A <= B 始终存在。

当一个整数的位数大于7时,它就被认为是重的。

例如:9878 被认为很重,因为(9 + 8 + 7 + 8)/4 = 8 ,而1111 不是,因为(1 + 1 + 1 + 1)/4 = 1

我有下面的解决方案,但它非常糟糕,并且在大量输入运行时会超时。我可以做些什么来提高效率?

int countHeavy(int A, int B) {
    int countHeavy = 0;

    while(A <= B){
        if(averageOfDigits(A) > 7){
            countHeavy++;
        }
        A++;
    }

    return countHeavy;
}
 
float averageOfDigits(int a) {
    float result = 0;
    int count = 0;

    while (a > 0) {
        result += (a % 10);
        count++;
        a = a / 10;
    }

    return result / count;
}

【问题讨论】:

  • averageOfDigits 中,我会将结果设为int,然后简单地设为return (float)result / count;。这将继续使用整数数学,直到函数返回。可能不是很大的优化,但仍然......
  • FWIW,如果A &gt;= B 一直是,那么A 应该递减并且条件应该是while (A &gt;= B)
  • 它应该可以在 log(A) 时间内通过动态编程或记忆化来实现。如果您遍历第一个数字的所有可能性,那么您可以计算剩余数字的可能性。您的目标平均值将是一个参数,而不是一个常数 7。不过,处理前导 0 和
  • @RudyVelthuis 抱歉,这是一个错字,我的意思是 A
  • 啊,好的。我确实希望您逐字复制您的代码,并且没有将其输入到此网页的编辑器中?错别字可以使原始源代码中没有错误的代码看起来有缺陷。这就是为什么我们喜欢看到真正的源代码,而不是某种模型。

标签: java algorithm performance time-complexity complexity-theory


【解决方案1】:

我对这个问题的看法与你不同。我的看法是这个问题是基于一个数字的以 10 为底的表示,所以你应该做的第一件事是将数字放入以 10 为底的表示。可能有更好的方法,但 Java 字符串以 base-10 表示整数,所以我使用了这些。将单个字符转换为整数实际上非常快,因此不会花费太多时间。

最重要的是,您在这件事上的计算永远不需要使用除法或浮点数。问题的核心在于整数。数(整数)中的所有位数(整数)加起来是否等于或大于七(整数)乘以位数(整数)?

警告 - 我并不是说这是最快的方法,但这可能比您原来的方法更快。

这是我的代码:

package heavyNum;

public class HeavyNum
{
    public static void main(String[] args)
    {
        HeavyNum hn = new HeavyNum();
        long startTime = System.currentTimeMillis();
        hn.countHeavy(100000000, 1);
        long endTime = System.currentTimeMillis();
        System.out.println("Time elapsed: "+(endTime- startTime));
    }

    private void countHeavy(int A, int B)
    {
        int heavyFound = 0;
        for(int i = B+1; i < A; i++)
        {
            if(isHeavy(i))
                heavyFound++;
        }
        System.out.println("Found "+heavyFound+" heavy numbers");
    }

    private boolean isHeavy(int i)
    {
        String asString = Integer.valueOf(i).toString();
        int length = asString.length();
        int dividingLine = length * 7, currTotal = 0, counter = 0;
        while(counter < length)
        {
            currTotal += Character.getNumericValue(asString.charAt(counter++));
        }
        return currTotal > dividingLine;
    }
}

感谢 this SO Question 了解我如何获取整数中的位数,感谢 this SO Question 了解如何在 java 中快速将字符转换为整数

在没有调试器的功能强大的计算机上运行 1 到 100,000,000 之间的数字会产生以下输出:

找到 569484 个重号

经过时间:6985

编辑:我最初是在寻找位数大于或等于位数的 7 倍的数字。我之前在 7025 毫秒内得到了 843,453 个数字的结果。

【讨论】:

  • 你确定toString()比重复的mod/div更好吗?毕竟,toString() 很可能也是这样做的。我同意计算位数就足够了,看看最后的count &gt; 7*numDigits
  • Java Integer 到 String 的转换经过优化并且非常快:查看源代码:grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…。此外,我希望它将整数转换为字符串的主要原因是,将字符作为整数提取基本上是常数时间。这让我将复杂性隔离到 Java 中最优化的库之一——它不一定是最好的选择,但它会非常非常好。每秒检查 14,000 多个数字对我来说已经足够了。
  • toString(i, 10) 作为重复的 mod/div 也没有其他任何作用。它并不比问题中使用的代码更快(更方便),因为它不仅可以获得权重,还可以同时计算位数。它不应该返回一个浮点数,而是一个布尔值。现在已经很晚了。明天我可能会发布一个替代方案。
  • 看看我的回答。绕道intString,然后绕道charint 并不比我使用的简单 div/mod 循环快。 Java 中的字符串例程实际上是相当标准的,而且肯定不是非常快。
  • 你需要 curTotal > divideLine,而不是 curTotal >= divideLine
【解决方案2】:

您确实可以使用字符串来获取位数,然后将各个数字的值相加,以查看它们的sum &gt; 7 * length,就像 Jeutnarg 似乎所做的那样。我拿了他的代码并添加了我自己的,简单的isHeavyRV(int)

private boolean isHeavyRV(int i)
{
    int sum = 0, count = 0;
    while (i > 0)
    {
        sum += i % 10;
        count++;
        i = i / 10;
    }
    return sum >= count * 7;
}

现在,而不是

        if(isHeavy(i))

我试过了

        if(isHeavyRV(i))

实际上,我首先使用字符串测试了他对isHeavy() 的实现,它在我的机器(一台较旧的 iMac)上运行了 12388 毫秒,发现了 843453 个重数字。

使用我的实现,我发现了完全相同数量的重数,但时间仅为 5416 毫秒。

字符串可能很快,但它们无法击败一个简单的循环,基本上就像 Integer.toString(i, 10) 所做的那样,但没有字符串绕道。

【讨论】:

    【解决方案3】:

    当您将 1 加到一个数字时,您将增加一位数字,并将所有较小的数字更改为零。如果递增从重数变为非重数,这是因为太多的低位数字被归零。在这种情况下,很容易找到下一个重数,而无需检查其间的所有数字:

    public class CountHeavy
    {
        public static void main(String[] args)
        {
            long startTime = System.currentTimeMillis();
            int numHeavy = countHeavy(1, 100000000);
            long endTime = System.currentTimeMillis();
            System.out.printf("Found %d heavy numbers between 1 and 100000000\n", numHeavy);
            System.out.println("Time elapsed: "+(endTime- startTime)+" ms");
        }
    
        static int countHeavy(int from, int to)
        {
            int numdigits=1;
            int maxatdigits=9;
            int numFound = 0;
            if (from<1)
            {
                from=1;
            }
            for(int i = from; i < to;)
            {
                //keep track of number of digits in i
                while (i > maxatdigits)
                {
                    long newmax = 10L*maxatdigits+9;
                    maxatdigits = (int)Math.min(Integer.MAX_VALUE, newmax);
                    ++numdigits;
                }
                //get sum of digits
                int digitsum=0;
                for(int digits=i;digits>0;digits/=10)
                {
                    digitsum+=(digits%10);
                }
    
                //calculate a step size that increments the first non-zero digit
                int step=1;
                int stepzeros=0;
                while(step <= (Integer.MAX_VALUE/10) && to-i >= step*10 && i%(step*10) == 0)
                {
                    step*=10;
                    stepzeros+=1;
                }
                //step is a 1 followed stepzeros zeros
    
                //how much is our sum too small by?
                int need = numdigits*7+1 - digitsum;
                if (need <= 0)
                {
                    //already have enough.  All the numbers between i and i+step are heavy
                    numFound+=step;
                }
                else if (need <= stepzeros*9)
                {
                    //increment to the smallest possible heavy number. This puts all the
                    //needed sum in the lowest-order digits
                    step = need%9;
                    for(;need >= 9;need-=9)
                    {
                        step = step*10+9;
                    }
                }
                //else there are no heavy numbers between i and i+step
                i+=step;
            }
            return numFound;
        }
    }
    

    找到 569484 个介于 1 和 100000000 之间的重数

    经过的时间:31 毫秒

    请注意,答案与 @JeutNarg 的不同,因为您要求平均 > 7,而不是平均 >= 7。

    【讨论】:

    • 目前没有选票,但这确实是解决方案。 比其他解决方案快得多而且更加优雅。
    • @RudyVelthuis 对于long numHeavy = countHeavy(1111111111L, 9999999999L); 来说似乎有点慢@ 我的立即输出 20 位数字的结果 ^^。 :)
    • 是的,与其他一些解决方案相比,它很慢。
    【解决方案4】:

    用查表数数

    您可以生成一个表来存储有多少具有 d 位数字的整数的数字总和大于数字 x。然后,您可以快速查找在 10、100、1000 ... 整数的任何范围内有多少重数。这些表只保存 9×d 的值,因此它们占用的空间非常小,并且可以快速生成。

    然后,要检查 B 具有 d 位的范围 AB,您为 1 到 d-1 位构建表,然后将范围 AB 拆分为10、100、1000 的块...并查找表中的值,例如对于范围 A = 782,B = 4321:

       RANGE      DIGITS  TARGET     LOOKUP      VALUE
    
     782 -  789     78x    > 6    table[1][ 6]     3    <- incomplete range: 2-9
     790 -  799     79x    > 5    table[1][ 5]     4
     800 -  899     8xx    >13    table[2][13]    15
     900 -  999     9xx    >12    table[2][12]    21
    1000 - 1999    1xxx    >27    table[3][27]     0
    2000 - 2999    2xxx    >26    table[3][26]     1
    3000 - 3999    3xxx    >25    table[3][25]     4
    4000 - 4099    40xx    >24    impossible       0
    4100 - 4199    41xx    >23    impossible       0
    4200 - 4299    42xx    >22    impossible       0
    4300 - 4309    430x    >21    impossible       0
    4310 - 4319    431x    >20    impossible       0
    4320 - 4321    432x    >19    impossible       0    <- incomplete range: 0-1
                                                  --
                                                  48
    

    如果第一个和最后一个范围不完整(不是 *0 - *9),请对照目标检查起始值或结束值。 (示例中,2 不大于 6,因此所有 3 个重数都包含在范围内。)

    生成查找表

    对于1位十进制整数,大于值x的整数n个数为:

    x:  0  1  2  3  4  5  6  7  8  9
    n:  9  8  7  6  5  4  3  2  1  0
    

    如您所见,这很容易通过取 n = 9-x 来计算。

    对于2位十进制整数,位数和大于x的整数n个数为:

    x:   0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18
    n:  99 97 94 90 85 79 72 64 55 45 36 28 21 15 10  6  3  1  0
    

    对于3位十进制整数,位数和大于x的整数n个数为:

    x:   0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27
    n: 999 996 990 980 965 944 916 880 835 780 717 648 575 500 425 352 283 220 165 120  84  56  35  20  10   4   1   0
    

    这些序列中的每一个都可以从前一个序列中生成:从值 10d 开始,然后从该值中反向减去前一个序列(跳过第一个零)。例如。从 2 位序列生成 3 位序列,从 103 = 1000 开始,然后:

     0. 1000 -   1      = 999
     1.  999 -   3      = 996
     2.  996 -   6      = 990
     3.  990 -  10      = 980
     4.  980 -  15      = 965
     5.  965 -  21      = 944
     6.  944 -  28      = 916
     7.  916 -  36      = 880
     8.  880 -  45      = 835
     9.  835 -  55      = 780
    10.  780 -  64 +  1 = 717  <- after 10 steps, start adding the previous sequence again
    11.  717 -  72 +  3 = 648
    12.  648 -  79 +  6 = 575
    13.  575 -  85 + 10 = 500
    14.  500 -  90 + 15 = 425
    15.  425 -  94 + 21 = 352
    16.  352 -  97 + 28 = 283
    17.  283 -  99 + 36 = 220
    18.  220 - 100 + 45 = 165  <- at the end of the sequence, keep subtracting 10^(d-1)
    19.  165 - 100 + 55 = 120
    20.  120 - 100 + 64 =  84
    21.   84 - 100 + 72 =  56
    22.   56 - 100 + 79 =  35
    23.   35 - 100 + 85 =  20
    24.   20 - 100 + 90 =  10
    25.   10 - 100 + 94 =   4
    26.    4 - 100 + 97 =   1
    27.    1 - 100 + 99 =   0
    

    顺便说一句,如果“重”数字定义为 7 以外的值,您可以使用相同的表。


    代码示例

    下面是演示该方法的 Javascript 代码 sn-p(我不会说 Java)。它非常未优化,但它在不到 0.07 毫秒的时间内完成了 0→100,000,000 示例。它也适用于 7 以外的权重。翻译成 Java 后,它应该可以轻松击败任何实际运行数字并检查其权重的算法。

    function countHeavy(A, B, weight) {
        var a = decimalDigits(A), b = decimalDigits(B);        // create arrays
        while (a.length < b.length) a.push(0);                 // add leading zeros
        var digits = b.length, table = weightTable();          // create table
        var count = 0, diff = B - A + 1, d = 0;                // calculate range
        for (var i = digits - 1; i >= 0; i--) if (a[i]) d = i; // lowest non-0 digit
        while (diff) {                                         // increment a until a=b
            while (a[d] == 10) {                               // move to higher digit
                a[d++] = 0;
                ++a[d];                                        // carry 1
            }
            var step = Math.pow(10, d);                        // value of digit d
            if (step <= diff) {
                diff -= step;
                count += increment(d);                         // increment digit d
            }
            else --d;                                          // move to lower digit
        }
        return count;
    
        function weightTable() {                               // see above for details
            var t = [[],[9,8,7,6,5,4,3,2,1,0]];
            for (var i = 2; i < digits; i++) {
                var total = Math.pow(10, i), final = total / 10;
                t[i] = [];
                for (var j = 9 * i; total > 0; --j) {
                    if (j > 9) total -= t[i - 1][j - 10]; else total -= final;
                    if (j < 9 * (i - 1)) total += t[i - 1][j];
                    t[i].push(total);
                }
            }
            return t;
        }
        function increment(d) {
            var sum = 0, size = digits;
            for (var i = digits - 1; i >= d; i--) {
                if (a[i] == 0 && i == size - 1) size = i;      // count used digits
                sum += a[i];                                   // sum of digits
            }
            ++a[d];
            var target = weight * size - sum;
            if (d == 0) return (target < 0) ? 1 : 0;           // if d is lowest digit
            if (target < 0) return table[d][0] + 1;            // whole range is heavy
            return (target > 9 * d) ? 0 : table[d][target];    // use look-up table
        }
        function decimalDigits(n) {
            var array = [];
            do {array.push(n % 10);
                n = Math.floor(n / 10);
            } while (n);
            return array;
        }
    }
    document.write("0 &rarr; 100,000,000 = " + countHeavy(0, 100000000, 7) + "<br>");
    document.write("782 &rarr; 4321 = " + countHeavy(782, 4321, 7) + "<br>");
    document.write("782 &rarr; 4321 = " + countHeavy(782, 4321, 5) + " (weight: 5)");

    【讨论】:

      【解决方案5】:

      我真的很喜欢@m69 的帖子,所以我写了受它启发的实现。表创建不是那么优雅,但有效。对于 n+1 位长整数,我从 n 位长整数中求和(最多)10 个值,每个数字 0-9 一个。

      我使用这种简化来避免任意范围计算:

      countHeavy(A, B) = countHeavy(0, B) - countHeavy(0, A-1)

      结果在两个循环中计算。一个用于比给定数字短的数字,一个用于其余数字。我无法轻松合并它们。 getResult只是通过范围检查查找table,其余代码应该很明显。

      public class HeavyNumbers {
          private static int maxDigits = String.valueOf(Long.MAX_VALUE).length();
          private int[][] table = null;
      
          public HeavyNumbers(){
              table = new int[maxDigits + 1][];
              table[0] = new int[]{1};
      
              for (int s = 1; s < maxDigits + 1; ++s) {
                  table[s] = new int[s * 9 + 1];
                  for (int k = 0; k < table[s].length; ++k) {
                      for (int d = 0; d < 10; ++d) {
                          if (table[s - 1].length > k - d) {
                              table[s][k] += table[s - 1][Math.max(0, k - d)];
                          }
                      }
                  }
              }
          }
      
          private int[] getNumberAsArray(long number) {
              int[] tmp = new int[maxDigits];
              int cnt = 0;
      
              while (number != 0) {
                  int remainder = (int) (number % 10);
                  tmp[cnt++] = remainder;
                  number = number / 10;
              }
      
              int[] ret = new int[cnt];
              for (int i = 0; i < cnt; ++i) {
                  ret[i] = tmp[i];
              }
              return ret;
          }
      
          private int getResult(int[] sum, int digits, int fixDigitSum, int heavyThreshold) {
              int target = heavyThreshold * digits - fixDigitSum + 1;
              if (target < sum.length) {
                  return sum[Math.max(0, target)];
              }
              return 0;
          }
      
          public int getHeavyNumbersCount(long toNumberIncl, int heavyThreshold) {
              if (toNumberIncl <= 0) return 0;
      
              int[] numberAsArray = getNumberAsArray(toNumberIncl);
      
              int res = 0;
      
              for (int i = 0; i < numberAsArray.length - 1; ++i) {
                  for (int d = 1; d < 10; ++d) {
                      res += getResult(table[i], i + 1, d, heavyThreshold);
                  }
              }
      
              int fixDigitSum = 0;
              int fromDigit = 1;
              for (int i = numberAsArray.length - 1; i >= 0; --i) {
                  int toDigit = numberAsArray[i];
                  if (i == 0) {
                      toDigit++;
                  }
                  for (int d = fromDigit; d < toDigit; ++d) {
                      res += getResult(table[i], numberAsArray.length, fixDigitSum + d, heavyThreshold);
                  }
      
                  fixDigitSum += numberAsArray[i];
                  fromDigit = 0;
              }
      
              return res;
          }
      
          public int getHeavyNumbersCount(long fromIncl, long toIncl, int heavyThreshold) {
              return getHeavyNumbersCount(toIncl, heavyThreshold) -
                      getHeavyNumbersCount(fromIncl - 1, heavyThreshold);
          }
      }
      

      它是这样使用的:

      HeavyNumbers h = new HeavyNumbers();
      System.out.println( h.getHeavyNumbersCount(100000000,7));
      

      打印出569484,不初始化表的重复计算时间在1us以下

      【讨论】:

      • 在我写我的 JS 版本之前,我没有注意到你的回答。我在 JS 中获得的速度大约是 0.07 毫秒,所以 Java 显然比 JS 快 100 倍,这甚至比我预期的要好:-)
      • @m69 好像有问题,JS应该不会那么慢吧。也许我们测量时间的方式不同。我制作了查找表(一次)并计算了 1000000 个随机 0..x 范围的重数。它在 1 秒以下,因此平均一次计算在 1 秒以下。这不是完美的时间估计,但是没有循环的测量会完全误导。
      • 我用每次重新生成的查找表来测量它,因为这样你可以将速度与其他没有可重用部分的算法进行比较。 (但我的代码可以在很多方面进行改进,例如,我正在生成表格中从未使用过的部分。)
      【解决方案6】:

      这是一个非常简单的带有记忆的递归,它逐个枚举固定数字的数字可能性。在计算对应的位数时,可以通过控制i的范围来设置AB

      看起来相当快(查看 20 位数的结果)。

      JavaScript 代码:

      var hash = {}
       
      function f(k,soFar,count){
        if (k == 0){
          return 1;
        }
      
        var key = [k,soFar].join(",");
        
        if (hash[key]){
          return hash[key];
        }
      
        var res = 0;
      
        for (var i=Math.max(count==0?1:0,7*(k+count)+1-soFar-9*(k-1)); i<=9; i++){
          res += f(k-1,soFar+i,count+1);
        }
      
        return hash[key] = res;
      }
      
      // Output:
      
      console.log(f(3,0,0)); // 56
      hash = {};
      
      console.log(f(6,0,0)); // 12313
      hash = {};
      
      console.log(f(20,0,0)); // 2224550892070475

      【讨论】:

      • 8 位数字的时钟似乎在 0.333 毫秒左右。但是,它给出的是 478302 而不是 569484;你在解释吗? “3 位数”为000, 001, 002 ... 999?我们一直在使用0, 1, 2 ... 999,所以896 被认为很重。
      • @m69 我认为使用我的代码的 8 位数字将是 1000000099999999 的报告,这有意义吗?我对 8 和 9 的结果与 Matt Timmermans 的代码一致。 (对于特定范围,我认为应该能够通过i 上的条件来控制AB。)
      • 啊哈,f(1,0,0)+f(2,0,0)+...+f(8,0,0) 确实在 0.8 毫秒左右返回 569484。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-01-07
      • 1970-01-01
      • 2020-04-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-13
      相关资源
      最近更新 更多