原文地址:https://luvletter.blog.luogu.org/p1014-ti-jie
P1014 [NOIP1999 普及组] Cantor 表
题目描述
现代数学的著名证明之一是 Georg Cantor 证明了有理数是可枚举的。他是用下面这一张表来证明这一命题的:
\(1/1\) , \(1/2\) , \(1/3\) , \(1/4\) , \(1/5\) , …
\(2/1\) , \(2/2\) , \(2/3\) , \(2/4\) , …
\(3/1\) , \(3/2\) , \(3/3\) , …
\(4/1\) , \(4/2\) …
\(5/1\) , …
…
我们以 Z 字形给上表的每一项编号。第一项是,然后是 \(1/2\),\(2/1\),\(3/1\),\(2/2\),…
输入格式
整数 \(N\)(\(1 \leq N \leq 10^7\))。
输出格式
表中的第 \(N\) 项。
输入输出样例
输入 #1
7
输出 #1
1/4
题解
这一题其实非常简单,不过我一开始做的时候为了方便调试在外面加了一个死循环,后面又忘了删掉了,导致提交结果出现TLE。
刚开始我还以为是我求平方根的算法的问题导致的超时(求平方根用的是从1开始一个一个平方去试的方法,时间复杂度为\(O(n)\)),所以自己还搞了一个用二分法求平方根的算法(时间复杂度为\(O(\log n)\)),但是还是没有用,后来才发现是之前测试用的循环的问题,把它删掉了就好了。不过也算是给自己多一次练习了。
不过在实际应用中求平方根也不可能用一个一个试的方法,效率太低,所以会用二分法、牛顿法等算法进行优化,其实牛顿法理论上计算的步骤更少,时间复杂度更低(时间复杂度为\(O(\log(\log n))\)),不过这些都不在一题的考察范围内,所以这里就不再多说了。
在这一题里面,求某一项所在的对角线序数其实可以直接用累加法一个一个加进行实现,但是这样太麻烦,所以可以采用解一元二次方程+求平方根的方法进行简化,不过这里的求平方根算法的时间复杂度最好要低于\(O(n)\),否则就没有起到简化的作用,失去了求平方根的意义了。解决了这个问题后,后面的问题就十分简单了。
代码(C++):
#include <iostream>
using namespace std;
//整数平方根函数
unsigned mysqrt(unsigned x)
{
//二分法计算平方根
unsigned min, max, mid;
//计算x的位数
unsigned digit = 0;
for (unsigned x0 = x;x0 > 0;)
{
x0 >>= 1;
digit++;
}
//确定二分法计算区间的范围,通过求出的位数缩小查找范围
if (digit % 2) //位数为奇数
{
//2^(digit-1) <= x < 2^digit,2^((digit-1)/2) <= √x<2^(digit/2) = √2*2^((digit-1)/2) < 1.5*2^((digit-1)/2)
min = 1 << (digit - 1) / 2;
max = digit == 1 ? 2 : min / 2 * 3; //digit=1时特殊处理
}
else //位数为偶数
{
//2^(digit-1) <= x < 2^digit,1.25*2^(digit/2-1) < √2*2^(digit/2-1) = 2^((digit-1)/2) <= √x<2^(digit/2)
max = 1 << digit / 2;
min = digit > 4 ? max / 8 * 5 : max / 2; //digit=0,2,4时特殊处理
}
//二分法求平方根
while (max - min > 1)
{
mid = (min + max) / 2;
if (mid*mid == x)
return mid;
else if (mid*mid < x)
min = mid;
else
max = mid;
}
//max-min=1时,直接比较max*max和x的值即可
if (max*max == x)
return max;
else
return min;
}
int main()
{
unsigned n;
unsigned a = 0;
cin >> n;
//计算第n项所在的对角线的序号a
//-----------------------------------------------------------------
//(这里其实可以直接用累加法进行相加比较计算对角线的序号a,不需要去求平方根)
//第a条对角线的第一项在表中的序号为a(a-1)/2+1
//n=a(a-1)/2+1
//a=[(√(8n-7)+1)/2](a取使a(a-1)/2+1小于等于n的最大值,即上面方程的正数解向下取整后的值)
a = mysqrt(n * 8 - 7); //[√(8n-7)]
a = (a + 1) / 2; //[(√(8n-7)+1)/2]
//-----------------------------------------------------------------
//第n项所在的对角线的第一项在表中的序号n0为a(a-1)/2+1
unsigned b = n - (a*(a - 1)) / 2; //第n项是所在的对角线的第b项,b=n-n0+1
if (a % 2) //对角线序号为奇数
cout << a - b + 1 << '/' << b << endl; //表中的第n项为(a-b+1)/b
else //对角线序号为偶数
cout << b << '/' << a - b + 1 << endl; //表中的第n项为b/(a-b+1)
return 0;
}