如何优化此类以解决此数学序列

时间:2014-08-30 14:19:47

标签: java optimization micro-optimization

给出一个像这样的无限序列(插入逗号以使模式更明显)

  

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 1,2 2 4 5 6 7 8,1 2 3 4 5 6 7 8 9,1 2 3 4 5 6 7 8 9 1 0,1 2 3 4 5 6 7 8 9 1 0 1 1,1 2 3 4 5 6 7 8 9 1 0 1 1 1 2,1 2 3。 。 。 。 。 。 。 。 。

我得到一个索引(1< = index< = 10 ^ 10),我需要找到该索引中的数字。

我写过这个工作代码,但速度太慢了。我已经尽可能地优化了它,但它仍然不够。还有其他方法可以让它跑得更快吗?

public class Foo {

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


    public static void main(String[] args) {
        numberOfInputs = Integer.parseInt(sc.nextLine().trim());

        while (inputCounter != numberOfInputs) {
            input = Long.parseLong(sc.nextLine().trim());
            System.out.println(step());
            inputCounter++;
        }
    }

    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++;
        }
    }

    private static long getNumberOfDigits(int n) {
        // 5 or less
        if (n < 100) {
            // 1 or 2
            if (n < 10)
                return 1;
            else
                return 2;
        } else {
            // 3 or 4 or 5
            if (n < 1000)
                return 3;
            else {
                // 4 or 5
                if (n < 10000)
                    return 4;
                else
                    return 5;
            }
        }
    }
}

编辑:归功于Marian's method of getting the number of digits in a number.他的分而治之的方法我已经命名为 getNumberOfDigits(int n)加快了我的程序执行速度。最初我将数字转换为String然后调用length(),这比我预期的要长很多

EDIT2:一些示例I / O:

1 : 1
2 : 1
3 : 2
4 : 1
5 : 2
6 : 3
7 : 1
8 : 2
9 : 3
10 : 4
11 : 1
12 : 2
13 : 3
14 : 4
15 : 5
16 : 1
17 : 2
18 : 3
19 : 4
20 : 5
21 : 6
22 : 1
23 : 2
24 : 3
25 : 4
26 : 5
27 : 6
28 : 7
29 : 1
30 : 2
31 : 3
32 : 4
33 : 5
34 : 6
35 : 7
36 : 8
37 : 1
38 : 2
39 : 3
40 : 4
41 : 5
42 : 6
43 : 7
44 : 8
45 : 9
46 : 1
47 : 2
48 : 3
49 : 4
50 : 5
51 : 6
52 : 7
53 : 8
54 : 9
55 : 1
56 : 0
57 : 1
58 : 2
59 : 3
60 : 4
61 : 5
62 : 6
63 : 7
64 : 8
65 : 9
66 : 1
67 : 0
68 : 1
69 : 1
70 : 1
71 : 2
72 : 3
73 : 4
74 : 5
75 : 6
76 : 7
77 : 8
78 : 9
79 : 1
80 : 0
81 : 1
82 : 1
83 : 1
84 : 2
85 : 1
86 : 2
87 : 3
88 : 4
89 : 5
90 : 6
91 : 7
92 : 8
93 : 9
94 : 1
95 : 0
96 : 1
97 : 1
98 : 1
99 : 2

4 个答案:

答案 0 :(得分:5)

如果我们查看数字的位置,我认为三角形数字在这里发挥作用:

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

的三角数
k            :  1   2    3    4    5     6
k(k+1)/2     :  1   3    6   10   15    21      triangle numbers
k(k+1)/2+1   :  2   4    7   11   16    22      plus one
N(k(k+1)/2+1):  1   1    1    1    1     1      item at this position

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

给出一个位置p,我们可以找到最近的k,以便k(k + 1)/ 2 +1 <= p。我们可以通过重新排列来解决二次x(x + 1)/ 2 + 1 = p

0.5 x^2 + 0.5 x + 1 - p = 0.

因此a = 0.5,b = 0.5且c = 1-p。解决x给出

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 = floor(-0.5 +/- sqrt(2 p - 1.75)),我们找到数字k。接下来找到l = p-k(k + 1)/ 2,它给出了第p个位置的数字。

正如我们指出的那样,只要我们得到两位数字就会失败。但我们可以做出调整。我们可以得到一个公式“三角数字数”TD(k)。对于k <1,其行为类似于三角形数字T(k)。 10,但增加了额外的数字。

k     : 1  ...  9  10  11  12
T(k)  : 1      45  55  66  78
change              1   3   6
TD(k) : 2      45  56  69  84

我们看到,对于10&lt; = k&lt; = 99,我们只需要添加T(k)+ T(k-9)。这应该给我们另一个二次方法,我们可以解决。类似的情况发生在100&lt; = k&lt; = 999,T(k)+ T(k-9)+ T(k-99)。

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 ^ 2 -8 k + 37 - p = 0给出

x = ( 8 +/- sqrt(64 - 4 *(37-p) ) ) /2
  = ( 8 +/- sqrt(4 p - 64) )/2
  =   4 +/- sqrt(p - 21) 

在此基础上给出了k值。


我们想找到三角形的总和T(k)+ T(k-9)+ T(k-99)+ .... 对于任何n,对于第一近似值T(k-n)= T(n)。总而言之 d * T(k)其中d为k的位数。 T(k)约为k ^ 2/2,因此总和约为d * k^2/2。这很容易解决,让d为位置p的位数,然后k = sqrt(2*p/d)。您可以使用它来粗略猜测k。

答案 1 :(得分:4)

以下代码几乎是直接计算。它产生与@maaartinus完全相同的结果(见下面的结果),但是在&lt; 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");
        }


    }

结果

    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

答案 2 :(得分:3)

我添加了一些可以大大缩短运行时间的代码 - 跳到底部查看示例。

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

因此,我的算法跟踪子序列的长度(添加每次迭代的位数),以及我们所处的子序列。如果输入大于子序列的长度,则只需减去该长度并再次迭代。如果它更小(或相等,因为问题是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);
}
}

显示时间改进的示例输出:

> 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

答案 3 :(得分:2)

我没有太多想法就写了program ......

  • length(n)计算n
  • 的十进制表示位数
  • cummulativLength(n)计算以n
  • 结尾的序列的总位数
  • doublyCummulativLength(n)计算最多以n结尾的所有序列的总位数
  • fullSequenceBefore(pos)使用二进制搜索计算位置pos之前的最长完整序列
  • digitAt(n)首先计算n并减去其长度,然后计算位置fullSequenceBefore的数字;然后它使用另一个二进制搜索最后一个序列

我到处都使用long因为它很快。有一个基本的test和一个demo产生以下结果

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_VALUE/2。理论上它也适用于Long.MAX_VALUE,但我在那里溢出。