Java 我如何优化这个解决这个数学序列的类

Java 我如何优化这个解决这个数学序列的类,java,optimization,micro-optimization,Java,Optimization,Micro Optimization,给定这样的无限序列(插入逗号以使模式更明显): 1,12,1234,1234,12345,123456,1234567,12345678,123456789,123456789,1234567910,1234567810,1234567901,1234567811 我得到了一个索引(1如果我们看数字的位置,我认为三角形数字在这里起作用: Position: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15, 16 17 18 19 20 21, 22 23 24

给定这样的无限序列(插入逗号以使模式更明显):

1,12,1234,1234,12345,123456,1234567,12345678,123456789,123456789,1234567910,1234567810,1234567901,1234567811


我得到了一个索引(1如果我们看数字的位置,我认为三角形数字在这里起作用:

Position: 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 28 
Number:   1, 1 2, 1 2 3, 1 2 3 4,  1   2  3  4 5,    1  2  3  4  5  6,  1   2  3  4  5  6  7, 
把这个序列称为N(p)

现在看看公式为k(k+1)/2的三角形数

所以第n个三角形数后面的项总是1

给出一个位置
p
我们可以找到最近的
k
,这样k(k+1)/2+1我就写了一篇文章,没有太多思考

  • length(n)
    计算
    n
  • cumulativelength(n)
    计算以
    n
  • doublyCummulativLength(n)
    计算以至多
    n
  • fullSequenceBefore(pos)
    使用二进制搜索计算位置
    pos
    之前最长的完整序列
  • digitAt(n)
    首先计算
    fullSequenceBefore
    并减去其长度,从而计算位置
    n
    处的数字;然后对最后一个序列使用另一个二进制搜索
我在任何地方都使用了
long
,因为它非常快

Found 1 at position 1
Found 1 at position 2
Found 2 at position 3
Found 3 at position 9
Found 2 at position 2147483647
Found 4 at position 10
Found 1 at position 100
Found 4 at position 1000
Found 9 at position 10000
Found 2 at position 100000
Found 6 at position 1000000
Found 2 at position 10000000
Found 6 at position 100000000
Found 8 at position 1000000000
Found 1 at position 10000000000
Found 1 at position 100000000000
Found 9 at position 1000000000000
Found 8 at position 10000000000000
Found 3 at position 100000000000000
Found 7 at position 1000000000000000
Found 6 at position 10000000000000000
Found 1 at position 100000000000000000
Found 1 at position 1000000000000000000
Found 7 at position 4611686018427387903

Computed in 0.030 seconds.

我尝试它的最大数字是
Long.MAX\u VALUE/2
。理论上它也可以用于
Long.MAX\u VALUE
,但我在那里得到了溢出。

我添加了一些代码,极大地提高了运行时间-跳到底部查看示例

我发现的一个关键洞察是,如果输入不在子序列中的任何位置,您可以跳过子序列。例如,如果您正在查找第100000000个数字,您知道它不在第5个子序列{1,2,3,4,5}。那么为什么要迭代它呢?这个版本似乎要快得多(试着用100000000的输入运行它,看看时间差),据我所知,它在所有情况下都返回相同的结果

因此,我的算法跟踪子序列的长度(每次迭代加上位数)和我们所处的子序列。如果输入大于子序列的长度,只需减去该长度,然后再次迭代。如果它更小(或相等,因为问题是1索引的),开始分解该子序列

一个小提示:我还更新了getNumberOfDigits,这样它就可以通过递归方式处理任何数字,但是新版本和旧版本都依赖于这个新方法,因此它在时间上的改进没有得到认可

public class Foo {

private static Scanner sc = new Scanner(System.in);
private static long input;
private static long inputCounter = 0;
private static int numberOfInputs;


/** Updated main method that calls both the new and old step() methods
 * to compare their outputs and their respective calculation times.
 * @param args
 */
public static void main(String[] args) {
    numberOfInputs = Integer.parseInt(sc.nextLine().trim());

    while (inputCounter != numberOfInputs) {
        long i = Long.parseLong(sc.nextLine().trim());
        input = i;
        System.out.println("Processing " + input);
        long t = System.currentTimeMillis();
        System.out.println("New Step result - " + newStep() + " in " + (System.currentTimeMillis() - t)+"ms");
        input = i;
        t = System.currentTimeMillis();
        System.out.println("Old Step result - " + step() + " in " + (System.currentTimeMillis() - t)+"ms");
        inputCounter++;
    }
}

/** Old version of step() method given in question. Used for time comparison */
public static char step() {
    int incrementor = 1;
    long _counter = 1L;

    while (true) {
        for (int i = 1; i <= incrementor; i++) {
            _counter += getNumberOfDigits(i);

            if (_counter > input) {
                return ((i + "").charAt((int)(input - _counter
                        + getNumberOfDigits(i))));
            }
        }
        incrementor++;
    }
}

/** New version of step() method.
 * Instead of iterating one index at a time, determines if the result lies within this
 * sub-sequence. If not, skips ahead the length of the subsequence.
 * If it does, iterate through this subsequence and return the correct digit
 */
public static int newStep() {
    long subSequenceLength = 0L;
    long subSequenceIndex = 1L;
    while(true){
        //Update to the next subsequence length
        subSequenceLength += getNumberOfDigits(subSequenceIndex);
        if(input <= subSequenceLength){
            //Input lies within this subsequence
            long element = 0L;
            do{
                element++;
                long numbDigits = getNumberOfDigits(element);
                if(input > numbDigits)
                    input -= numbDigits;
                else
                    break;
            }while(true);

            //Correct answer is one of the digits in element, 1-indexed.
            //Speed isn't that important on this step because it's only done on return
            return Integer.parseInt(("" + element).substring((int)input-1, (int)input));


        } else{
            //Input does not lie within this subsequence - move to next sequence
            input -= subSequenceLength;
            subSequenceIndex++;
        }

    }
}

/** Updated to handle any number - hopefully won't slow down too much.
 * Won't handle negative numbers correctly, but that's out of the scope of the problem */
private static long getNumberOfDigits(long n){
    return getNumberOfDigits(n, 1);
}

/** Helper to allow for tail recursion.
 * @param n - the number of check the number of digits for
 * @param i - the number of digits thus far. Accumulator. */
private static long getNumberOfDigits(long n, int i) {
    if(n < 10) return i;
    return getNumberOfDigits(n/10, i+1);
}
}

下面的代码是一个几乎直接的计算。它产生的结果与@maaartinus的结果完全相同(见下面的结果),但它在<1ms内完成,而不是30ms

请参阅代码注释以了解其工作原理的详细信息。如果需要进一步解释,请告诉我

    package com.test.www;

    import java.util.ArrayList;
    import java.util.List;

    public final class Test  {

        /** <p> 
         *  Finds digit at {@code digitAt} position. Runs in O(n) where n is the max
         *  digits of the 'full' number (see below), e.g. for {@code digitAt} = 10^10, 
         *  n ~ 5, for 10^20, n ~ 10. 
         *  <p>
         *  The algorithm is thus basically a direct 'closed form' calculation. 
         *  It finds the quadratic equation to calculate triangular numbers (x(x+1)/2) but also
         *  takes into a account the transitions from 9 to 10, from 99 to 100, etc. and 
         *  adjusts the quadratic equation accordingly. This finds the last 'full' number
         *  on each 'line' (see below). The rest follows from there.
         *     
         */
        public static char findDigitAt(long digitAt) {

            /* The line number where digitAt points to, where:
             *  1, 1 2, 1 2 3, 1 2 3 4, etc. ->
             *  1        <- line 1
             *  1 2      <- line 2
             *  1 2 3    <- line 3
             *  1 2 3 4  <- line 4
             */
            long line;

            // ---- Get number of digits of 'full' numbers where digitAt at points, e.g. 
            //      if digitAt = 55 or 56 then digits = the number of digits in 10 which is 2.

            long nines = 0L; // = 9 on first iteration, 99 on second, etc.
            long digits = 0;
            long cutoff = 0; // Cutoff of digitAt where number of digits change
            while (digitAt > cutoff) {
                digits++;
                nines = nines + Math.round(Math.pow(10L, digits-1L)) * 9L;

                long nines2 = 0L;
                cutoff = 0L;
                for (long i = 1L; i <= digits; i++) {
                    cutoff = cutoff + ((nines-nines2)*(nines-nines2+1)/2);
                    nines2 = nines2 + Math.round(Math.pow(10L, i-1L)) * 9L;
                }
            }

            /* We build a quadratic equation to take us from digitAt to line */

            double r = 0; // Result of solved quadratic equation 
                          // Must be double since we're using Sqrt()
                          // even though result is always an integer.

            // ---- Define the coefficients of the quadratic equation
            long xSquared = digits;
            long x = 0L;
            long c = 0L;
            nines = 0L; // = 9 on first iteration, 99 on second, etc.

            for (long i = 1L; i <= digits; i++) {
                x = x + (-2L*nines + 1L);
                c = c + (nines * (nines - 1L));
                nines = nines + Math.round(Math.pow(10L, i-1L)) * 9L;
            }
            // ---- Solve quadratic equation, i.e. y - ax^2 + bx + c  =>  x = [ -b +/- sqrt(b^2 - 4ac) ] / 2 
            r = (-x + Math.sqrt(x*x - 4L*xSquared*(c-2L*digitAt))) / (2L*xSquared); 

            // Make r an integer
            line = ((long) r) + 1L;
            if (r - Math.floor(r) == 0.0) { // Simply takes care of special case
                line = line - 1L;
            }

            /* Now we have the line number ! */

            // ---- Calculate the last number on the line
            long lastNum = 0; 
            nines = 0;
            for (int i = 1; i <= digits; i++) {
                long pline = line - nines; 
                lastNum = lastNum + (pline * (pline+1))/2;
                nines = nines + Math.round(Math.pow(10, i-1)) * 9;
            }

            /* The hard work is done now. The piece of cryptic code below simply counts
             * back from LastNum to digitAt to find first the 'full' number at that point
             * and then finally counts back in the string representation of 'full' number
             * to find the actual digit.
             */
            long fullNumber = 0L;
            long line_decs = 1 + (int) Math.log10(line);
            boolean done = false;
            long nb;
            long a1 = Math.round(Math.pow(10, line_decs-1));
            long count_back = 0;
            while (!done) {
                nb = lastNum - (line - a1) * line_decs; 
                if (nb-(line_decs-1) <= digitAt) {
                    fullNumber = line - (lastNum - digitAt) / line_decs;
                    count_back = (lastNum - digitAt) % line_decs; 
                    done = true;
                } else {
                    lastNum = nb-(line_decs); 
                    line = a1-1; 
                    line_decs--; 
                    a1 = a1 / 10; 
                }
            }

            String numStr = String.valueOf(fullNumber);
            char digit = numStr.charAt(numStr.length() - (int) count_back - 1);  

            //System.out.println("digitAt = " + digitAt + "  -  fullNumber =  " + fullNumber + "  -  digit = " + digit);
            System.out.println("Found " + digit + " at position " + digitAt);
            return digit;
        }

        public static void main(String... args) {
            long t = System.currentTimeMillis();

            List<Long> testList = new ArrayList<Long>();
            testList.add(1L); testList.add(2L); testList.add(3L); testList.add(9L);
            testList.add(2147483647L);
            for (int i = 1; i <= 18; i++) {
                testList.add( Math.round(Math.pow(10, i-1)) * 10);
            }
            //testList.add(4611686018427387903L); // OVERFLOW OCCURS

            for (Long testValue : testList) {
                char digit = findDigitAt(testValue);
            }

            long took = t = System.currentTimeMillis() - t;
            System.out.println("Calculation of all above took: " + t + "ms");
        }


    }

如果输入为66,那么预期的输出是什么?您的
步骤
函数应该是“读取到下一个分隔符”而且你不应该担心数字的数量…特别是因为你的数字代码似乎没有考虑到无限空间中的所有可能性…所以,你说9后面的“1”和“0”将被计算为两个数字,而不是一个数字“10”?为什么你要在每个用户输入上重新创建这个序列?如果如果你需要一个较长的序列,你可以缓存最新的序列并扩展它。我很确定有一个封闭形式的解决方案。只有我的数论书在我的阁楼里。考虑把它作为一个数学问题,放在相应的SE站点上。对于p=55,p=56,p=57,(也许全部在中间)失败。P==67使用三角形数的想法确实是聪明的,但是当内部序列达到10时,事情开始变得混乱,它占据2个位置(至多),然后当它达到100时,占据3个位置(至多再次)等等。任何封闭的公式应该以某种方式考虑数字表示的基数。(在OP案例中:基数10)。@Beefyhalo同意。这不是解决方案,而是一个有价值的贡献(另见),所以+1@rslemos我敢打赌,纯数学在这里太复杂了。也许有一些特殊的10次方的外壳,它可以工作。很好,但很慢。我需要27毫秒。我很困惑…我发布的最慢结果是11毫秒。每个结果下面更长的时间是原始代码的运行时间。抱歉…我看我看不懂。我需要对于我的整个计算来说,27毫秒,所以我们很可能是一致的。在整个计算中,你的随机数在1…10 ^ 10?不,我是指在我的回答中给出的几个例子的时间。时间不包括预计算,也需要几毫秒。@ Floris会同意(注意我在复习后改进了代码)。。使用您的代码,我找不到任何输入值的“off by one”错误。您能给我一个示例吗。(显然,我无法测试所有输入值:)@Floris我想现在没有错误了。但是有很多错误,最初一些错误主要来自于基于1的
pos
,后来在
Long.MAX_值/2
上出现了溢出。
0.5 x^2 + 0.5 x + 1 - p = 0.
x = -0.5 +/- sqrt( 0.25 - 2 * (1-p) )
1    0
2    1
3    1.5615528128
4    2
5    2.3722813233
6    2.7015621187
7    3
8    3.2749172176
9    3.5311288741
10   3.7720018727
11   4
12   4.216990566
13   4.4244289009
14   4.623475383
15   4.8150729064
16   5
k     : 1  ...  9  10  11  12
T(k)  : 1      45  55  66  78
change              1   3   6
TD(k) : 2      45  56  69  84
Now T(k)+T(k-9) + 1 = k(k+1)/2 +(k-9)(k-8)/2 + 1
                     = 0.5 k^2 + 0.5 k + 0.5 k^2 - 17/2 k + 72/2 + 1
                     = k^2 -8 k + 37 
x = ( 8 +/- sqrt(64 - 4 *(37-p) ) ) /2
  = ( 8 +/- sqrt(4 p - 64) )/2
  =   4 +/- sqrt(p - 21) 
Found 1 at position 1
Found 1 at position 2
Found 2 at position 3
Found 3 at position 9
Found 2 at position 2147483647
Found 4 at position 10
Found 1 at position 100
Found 4 at position 1000
Found 9 at position 10000
Found 2 at position 100000
Found 6 at position 1000000
Found 2 at position 10000000
Found 6 at position 100000000
Found 8 at position 1000000000
Found 1 at position 10000000000
Found 1 at position 100000000000
Found 9 at position 1000000000000
Found 8 at position 10000000000000
Found 3 at position 100000000000000
Found 7 at position 1000000000000000
Found 6 at position 10000000000000000
Found 1 at position 100000000000000000
Found 1 at position 1000000000000000000
Found 7 at position 4611686018427387903

Computed in 0.030 seconds.
public class Foo {

private static Scanner sc = new Scanner(System.in);
private static long input;
private static long inputCounter = 0;
private static int numberOfInputs;


/** Updated main method that calls both the new and old step() methods
 * to compare their outputs and their respective calculation times.
 * @param args
 */
public static void main(String[] args) {
    numberOfInputs = Integer.parseInt(sc.nextLine().trim());

    while (inputCounter != numberOfInputs) {
        long i = Long.parseLong(sc.nextLine().trim());
        input = i;
        System.out.println("Processing " + input);
        long t = System.currentTimeMillis();
        System.out.println("New Step result - " + newStep() + " in " + (System.currentTimeMillis() - t)+"ms");
        input = i;
        t = System.currentTimeMillis();
        System.out.println("Old Step result - " + step() + " in " + (System.currentTimeMillis() - t)+"ms");
        inputCounter++;
    }
}

/** Old version of step() method given in question. Used for time comparison */
public static char step() {
    int incrementor = 1;
    long _counter = 1L;

    while (true) {
        for (int i = 1; i <= incrementor; i++) {
            _counter += getNumberOfDigits(i);

            if (_counter > input) {
                return ((i + "").charAt((int)(input - _counter
                        + getNumberOfDigits(i))));
            }
        }
        incrementor++;
    }
}

/** New version of step() method.
 * Instead of iterating one index at a time, determines if the result lies within this
 * sub-sequence. If not, skips ahead the length of the subsequence.
 * If it does, iterate through this subsequence and return the correct digit
 */
public static int newStep() {
    long subSequenceLength = 0L;
    long subSequenceIndex = 1L;
    while(true){
        //Update to the next subsequence length
        subSequenceLength += getNumberOfDigits(subSequenceIndex);
        if(input <= subSequenceLength){
            //Input lies within this subsequence
            long element = 0L;
            do{
                element++;
                long numbDigits = getNumberOfDigits(element);
                if(input > numbDigits)
                    input -= numbDigits;
                else
                    break;
            }while(true);

            //Correct answer is one of the digits in element, 1-indexed.
            //Speed isn't that important on this step because it's only done on return
            return Integer.parseInt(("" + element).substring((int)input-1, (int)input));


        } else{
            //Input does not lie within this subsequence - move to next sequence
            input -= subSequenceLength;
            subSequenceIndex++;
        }

    }
}

/** Updated to handle any number - hopefully won't slow down too much.
 * Won't handle negative numbers correctly, but that's out of the scope of the problem */
private static long getNumberOfDigits(long n){
    return getNumberOfDigits(n, 1);
}

/** Helper to allow for tail recursion.
 * @param n - the number of check the number of digits for
 * @param i - the number of digits thus far. Accumulator. */
private static long getNumberOfDigits(long n, int i) {
    if(n < 10) return i;
    return getNumberOfDigits(n/10, i+1);
}
}
> 8
> 10000
Processing 10000
New Step result - 9 in 0ms
Old Step result - 9 in 2ms
> 100000
Processing 100000
New Step result - 2 in 0ms
Old Step result - 2 in 4ms
> 1000000
Processing 1000000
New Step result - 6 in 0ms
Old Step result - 6 in 3ms
> 10000000
Processing 10000000
New Step result - 2 in 1ms
Old Step result - 2 in 22ms
> 100000000
Processing 100000000
New Step result - 6 in 1ms
Old Step result - 6 in 178ms
> 1000000000
Processing 1000000000
New Step result - 8 in 4ms
Old Step result - 8 in 1765ms
> 10000000000
Processing 10000000000
New Step result - 1 in 11ms
Old Step result - 1 in 18109ms
> 100000000000
Processing 100000000000
New Step result - 1 in 5ms
Old Step result - 1 in 180704ms
    package com.test.www;

    import java.util.ArrayList;
    import java.util.List;

    public final class Test  {

        /** <p> 
         *  Finds digit at {@code digitAt} position. Runs in O(n) where n is the max
         *  digits of the 'full' number (see below), e.g. for {@code digitAt} = 10^10, 
         *  n ~ 5, for 10^20, n ~ 10. 
         *  <p>
         *  The algorithm is thus basically a direct 'closed form' calculation. 
         *  It finds the quadratic equation to calculate triangular numbers (x(x+1)/2) but also
         *  takes into a account the transitions from 9 to 10, from 99 to 100, etc. and 
         *  adjusts the quadratic equation accordingly. This finds the last 'full' number
         *  on each 'line' (see below). The rest follows from there.
         *     
         */
        public static char findDigitAt(long digitAt) {

            /* The line number where digitAt points to, where:
             *  1, 1 2, 1 2 3, 1 2 3 4, etc. ->
             *  1        <- line 1
             *  1 2      <- line 2
             *  1 2 3    <- line 3
             *  1 2 3 4  <- line 4
             */
            long line;

            // ---- Get number of digits of 'full' numbers where digitAt at points, e.g. 
            //      if digitAt = 55 or 56 then digits = the number of digits in 10 which is 2.

            long nines = 0L; // = 9 on first iteration, 99 on second, etc.
            long digits = 0;
            long cutoff = 0; // Cutoff of digitAt where number of digits change
            while (digitAt > cutoff) {
                digits++;
                nines = nines + Math.round(Math.pow(10L, digits-1L)) * 9L;

                long nines2 = 0L;
                cutoff = 0L;
                for (long i = 1L; i <= digits; i++) {
                    cutoff = cutoff + ((nines-nines2)*(nines-nines2+1)/2);
                    nines2 = nines2 + Math.round(Math.pow(10L, i-1L)) * 9L;
                }
            }

            /* We build a quadratic equation to take us from digitAt to line */

            double r = 0; // Result of solved quadratic equation 
                          // Must be double since we're using Sqrt()
                          // even though result is always an integer.

            // ---- Define the coefficients of the quadratic equation
            long xSquared = digits;
            long x = 0L;
            long c = 0L;
            nines = 0L; // = 9 on first iteration, 99 on second, etc.

            for (long i = 1L; i <= digits; i++) {
                x = x + (-2L*nines + 1L);
                c = c + (nines * (nines - 1L));
                nines = nines + Math.round(Math.pow(10L, i-1L)) * 9L;
            }
            // ---- Solve quadratic equation, i.e. y - ax^2 + bx + c  =>  x = [ -b +/- sqrt(b^2 - 4ac) ] / 2 
            r = (-x + Math.sqrt(x*x - 4L*xSquared*(c-2L*digitAt))) / (2L*xSquared); 

            // Make r an integer
            line = ((long) r) + 1L;
            if (r - Math.floor(r) == 0.0) { // Simply takes care of special case
                line = line - 1L;
            }

            /* Now we have the line number ! */

            // ---- Calculate the last number on the line
            long lastNum = 0; 
            nines = 0;
            for (int i = 1; i <= digits; i++) {
                long pline = line - nines; 
                lastNum = lastNum + (pline * (pline+1))/2;
                nines = nines + Math.round(Math.pow(10, i-1)) * 9;
            }

            /* The hard work is done now. The piece of cryptic code below simply counts
             * back from LastNum to digitAt to find first the 'full' number at that point
             * and then finally counts back in the string representation of 'full' number
             * to find the actual digit.
             */
            long fullNumber = 0L;
            long line_decs = 1 + (int) Math.log10(line);
            boolean done = false;
            long nb;
            long a1 = Math.round(Math.pow(10, line_decs-1));
            long count_back = 0;
            while (!done) {
                nb = lastNum - (line - a1) * line_decs; 
                if (nb-(line_decs-1) <= digitAt) {
                    fullNumber = line - (lastNum - digitAt) / line_decs;
                    count_back = (lastNum - digitAt) % line_decs; 
                    done = true;
                } else {
                    lastNum = nb-(line_decs); 
                    line = a1-1; 
                    line_decs--; 
                    a1 = a1 / 10; 
                }
            }

            String numStr = String.valueOf(fullNumber);
            char digit = numStr.charAt(numStr.length() - (int) count_back - 1);  

            //System.out.println("digitAt = " + digitAt + "  -  fullNumber =  " + fullNumber + "  -  digit = " + digit);
            System.out.println("Found " + digit + " at position " + digitAt);
            return digit;
        }

        public static void main(String... args) {
            long t = System.currentTimeMillis();

            List<Long> testList = new ArrayList<Long>();
            testList.add(1L); testList.add(2L); testList.add(3L); testList.add(9L);
            testList.add(2147483647L);
            for (int i = 1; i <= 18; i++) {
                testList.add( Math.round(Math.pow(10, i-1)) * 10);
            }
            //testList.add(4611686018427387903L); // OVERFLOW OCCURS

            for (Long testValue : testList) {
                char digit = findDigitAt(testValue);
            }

            long took = t = System.currentTimeMillis() - t;
            System.out.println("Calculation of all above took: " + t + "ms");
        }


    }
    Found 1 at position 1
    Found 1 at position 2
    Found 2 at position 3
    Found 3 at position 9
    Found 2 at position 2147483647
    Found 4 at position 10
    Found 1 at position 100
    Found 4 at position 1000
    Found 9 at position 10000
    Found 2 at position 100000
    Found 6 at position 1000000
    Found 2 at position 10000000
    Found 6 at position 100000000
    Found 8 at position 1000000000
    Found 1 at position 10000000000
    Found 1 at position 100000000000
    Found 9 at position 1000000000000
    Found 8 at position 10000000000000
    Found 3 at position 100000000000000
    Found 7 at position 1000000000000000
    Found 6 at position 10000000000000000
    Found 1 at position 100000000000000000
    Found 1 at position 1000000000000000000
    Calculation of all above took: 0ms