Java 我如何优化这个解决这个数学序列的类
给定这样的无限序列(插入逗号以使模式更明显): 1,12,1234,1234,12345,123456,1234567,12345678,123456789,123456789,1234567910,1234567810,1234567901,1234567811Java 我如何优化这个解决这个数学序列的类,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如果我们看数字的位置,我认为三角形数字在这里起作用:
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