题目描述
下面的图形是著名的杨辉三角形:
如果我们按从上到下、从左到右的顺序把所有数排成一列,可以得到如下数列: 1,1,1,1,2,1,1,3,3,1,1,4,6,4,1,⋯1,1,1,1,2,1,1,3,3,1,1,4,6,4,1,⋯
给定一个正整数 N,请你输出数列中第一次出现 N 是在第几个数?
输入描述
输入一个整数 N。
输出描述
输出一个整数代表答案。
输入输出样例
示例 1
输入
6
输出
13
评测用例规模与约定
对于 20 的评测用例,1≤N≤10; 对于所有评测用例,1≤N≤1000000000。
运行限制
- 最大运行时间:1s
- 最大运行内存: 256M
作者题解
这道题的难点并不是什么高深的算法运用,而是重在优化和各方面的细节,对考生的综合素质要求极高,若能在考场高压环境下把握这道题的所有细节,想必你一定是比较资深的算法高手。
思路框架
用暴力算法不断地去算每一行的每一个数,直到遇到目标数,输出其位置序号。
观察发现,杨辉三角是左右对称的,因此n第一次出现的位置一定在左侧,所以我们想到可以只用计算一半的数即可。
继续观察,发现第1行和第2行需要计算前一个数,第3行和第4行需要前两个数……这意味着每到奇数行,我们这一行所需要计算的数就要加一位。
同时我们发现,新增加的这个数一定是杨辉三角原型图偶数行中间两个一模一样的数的和,所以新增加的数的值就是上一行最后一个数的两倍。
1 | ||
1 | ||
1 | 2 | |
1 | 3 | |
1 | 4 | 6 |
1 | 5 | 10 |
仅仅是这样的优化是远远不够的,我们继续找规律。
我们发现除了第一列全是1以外,其他所有的列从上至下都是递增的,所以假设我们在某一列计算出一个数值,发现它比n要大,那就意味着它这一列下面的所有数字也都比n大,不需要再计算了,所以我们使用一个maxCol来记录最大列数,后面的每一行都不需要再计算到这一列。
这样的优化已经非常棒了,但还要考虑到一个更特殊的情况,就是从第三列开始的每一列,下一行的数值跨度都非常大,而且越往下跨度越大,即便是我们只需要算两列,也要算好多好多行才能找到这个数字,这个过程还是太冗余了。
我们还需要注意到一件事,杨辉三角中所有的整数都一定至少会出现一次,其原因就是第二列从2开始往下递增的数值永远是1不变,2,3,4,5,6……
因此我们可以确定,当计算到某一行的第三个数时就发现已经比n大了,我们就不必再继续计算下去了,因为目标数一定在第二列,而由于第二列是公差为1的等差数列,所以此时的n一定在第n+1行的第二个数,这时就可以马上计算出n所在的位置。
其他细节
1.若n为1时,直接输出1即可
2.新的一行的数值只由前一行控制,因此使用两个ArrayList维护两行数据即可
示例代码
import java.util.ArrayList;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
long n = scan.nextLong();
scan.close();
// 若某一行有21亿的两倍之多的数,其最大值已经是天文数字,故最大列不会溢出int型
int maxCol = Integer.MAX_VALUE;
if(n==1){
System.out.println(1);
return;
}
// 由于杨辉三角每一行后一半的数据是前一半的逆序,我们只计算保留前一半内容(规律)
// before为前一行内容(初始化为第二行),target为根据before计算的目标行内容(下一行)
ArrayList<Long> before = new ArrayList<>();
ArrayList<Long> target;
before.add(1L);
// 第二行的前一半只有一个数——1,目标行从第三行开始计算
for(int i = 3; true; i++){
//目标行的第一个数字一定是1
target = new ArrayList<>();
target.add(1L);
// 此处j为ArrayList的下标(index),由于0号下标为1,所以从1号下标开始计算
// target所需要计算的长度不能超过以下两个值:before数组的长度、最大列数标记
for(int j = 1; j < before.size() && j < maxCol; j++){
long newNum = before.get(j-1) + before.get(j);
target.add(newNum);
// 如果发现n,直接输出,退出程序
if(newNum == n){
System.out.println((long) (1+i-1)*(i-1)/2 + j + 1);
return;
}
}
// 使用temp来记录target中的最后一个数的值
long temp = target.get(target.size() - 1);
// 当最大列数标记为初始值,并且行数为奇数时
// target需要增加一位,其值为before最后一个数值的两倍(规律)
if(maxCol == Integer.MAX_VALUE && i%2 == 1) {
temp = 2 * before.get(before.size() - 1);
target.add(temp);
}
// 当target的最后一位数temp的值比n大时,可知下一行不需要再算到这一位了
if(temp > n){
// 更新maxCol的值,表示下一行target不需要再算到这一列(这一下标)————优化
maxCol = target.size() - 1;
// 如果发现以后都不用算到第三个数时(下标为2),那么n一定在第二列
// 根据第二列数值的规律,可以轻松计算出n的位置————优化
if(maxCol == 2){
System.out.println((long) ((1+n)*n/2) + 2);
return;
}
}
//System.out.println(target); //测试target是否正确生成
// 当temp也就是target的最后一位就是目标n,那么直接计算输出其位置
if(temp == n){
System.out.println((long) (1+i-1)*(i-1)/2 + target.size());
return;
}
// 将target作为新的before,进行下一次迭代
before = target;
}
}
}