有一张分数表,其中的每一项是以 “Z” 字形先后次序按某种规律形成的, 每一项编号和对应的分数如下表和下图所示: 请根据表中的规律,对于输入的整数N(1<=N<=20000),输出对应的第N项分数。 输入 只有一行,有一个正整数N。 输出 只有一行,有一个分数,分子和分母间用“/”号分隔。 样例输入 复制 8 样例输出 复制 2/3 c++
时间: 2025-07-26 12:21:22 AIGC 浏览: 46
<think>我们讨论的序列是这样一个分数序列:按照分子分母之和的大小分组,和相同的在一组,每组内按照分子递增(或分母递减)的顺序排列。然后按照组和(即分子分母之和)从小到大的顺序,在组内按照分子从小到大的顺序排列,整个序列呈Z字形排列。
具体规律如下:
第1组:和为2 -> 1/1
第2组:和为3 -> 1/2, 2/1 (注意,这里2/1实际上分子大于分母,但按题目要求,我们按分子递增排列,所以先1/2,再2/1)
第3组:和为4 -> 1/3, 2/2, 3/1
第4组:和为5 -> 1/4, 2/3, 3/2, 4/1
以此类推。
但是注意,题目要求的是Z字形排列,实际上在奇数和的组(比如和为3、5、7等)和偶数和的组(和为2、4、6等)排列方向不同?观察题目描述,实际上题目没有明确方向,但通常的Z字形扫描是交替改变方向。
然而,这里我们讨论的是分数序列的Z字形排列,常见的是这样的:
第一行:1/1(方向:→)
第二行:1/2(→) 2/1(←) 这里方向改变,从右往左
第三行:3/1(→) 2/2(→) 1/3(→) 但这样不对,因为第三组和为4,应该是1/3,2/2,3/1。但是如果我们按照Z字形,第二行是从左到右然后下一行从右到左,那么:
第一行:1/1(从左到右)
第二行:从左到右:1/2, 2/1,但是如果我们按照Z字形,应该是先1/2(在1/1的右下),然后2/1(在1/2的右上?)这不符合矩阵扫描。
实际上,题目中提到的“Z字形分数序列”通常是指如下排列(也称为Stern-Brocot树的层级遍历?但这里我们按分子分母和分组):
另一种常见的Z字形排列是:
1/1, 1/2, 2/1, 3/1, 2/2, 1/3, 1/4, 2/3, 3/2, 4/1, 5/1, 4/2, 3/3, 2/4, 1/5, ...
但观察这个序列,分组并不是按和连续排列的。实际上,我们按如下方式排列:
我们可以将分数序列按照如下规则排列:
第1层:1/1(和=2)
第2层:1/2, 2/1(和=3)
第3层:1/3, 2/2, 3/1(和=4)
第4层:1/4, 2/3, 3/2, 4/1(和=5)
第5层:1/5, 2/4, 3/3, 4/2, 5/1(和=6)
但是,在每一层内部,方向交替:
奇数层(第1层、第3层、第5层...)从左到右(分子递增)
偶数层(第2层、第4层...)从右到左(即分子递减)?但是观察第二层:1/2, 2/1,如果第二层从左到右是1/2,2/1,那么第三层从左到右是1/3,2/2,3/1。
然而,如果我们按照层数(组号)的奇偶性来改变方向:
第1层(奇数层):从左到右 -> 1/1
第2层(偶数层):从右到左 -> 但是我们要先输出1/2再输出2/1?这实际上是从左到右(分子递增)还是分子递减?
实际上,我们通常这样处理:
第1组(和=2)有1个分数:1/1(分子递增:只有1个,无所谓)
第2组(和=3)有2个分数:我们按照分子递增(即1/2, 2/1)或者分子递减(2/1,1/2)?题目要求Z字形,所以交替方向。
常见的Z字形排列是:第一组从左到右,第二组从右到左(即分子递减),第三组从左到右(分子递增),第四组从右到左(分子递减)...
因此:
第1组(和=2,1个元素):1/1
第2组(和=3,2个元素):2/1, 1/2 (注意,这里先输出2/1,再输出1/2,因为方向是从右到左,即分子从大到小)
第3组(和=4,3个元素):1/3, 2/2, 3/1(方向从左到右,分子从小到大)
第4组(和=5,4个元素):4/1, 3/2, 2/3, 1/4(方向从右到左,分子从大到小)
所以整个序列是:1/1, 2/1, 1/2, 1/3, 2/2, 3/1, 4/1, 3/2, 2/3, 1/4, ...
但是,注意观察:第2组应该是2/1,1/2,然后第3组1/3,2/2,3/1,第4组4/1,3/2,2/3,1/4。
所以规律:
第k组(k>=1)对应分子分母和为k+1,有k个元素(因为分子从1到k,分母为k+1-分子)。
方向:当k为奇数时(k从1开始),该组方向为从左到右(分子递增:分子从1到k)?但是不对,因为第1组k=1(奇数)只有一个元素1/1(分子1,分母1),第3组k=3(奇数)应该是分子递增:1/3,2/2,3/1。
当k为偶数时,该组方向为从右到左(即分子递减:分子从k到1)?所以第2组k=2(偶数)就是分子从2到1:2/1,1/2;第4组k=4(偶数)分子从4到1:4/1,3/2,2/3,1/4。
因此,整个序列的排列规则:
第1组:k=1,分子递增(分子=1),输出:1/1
第2组:k=2,分子递减(分子=2,1),输出:2/1, 1/2
第3组:k=3,分子递增(分子=1,2,3),输出:1/3,2/2,3/1
第4组:k=4,分子递减(分子=4,3,2,1),输出:4/1,3/2,2/3,1/4
那么,如何求第N项?
首先,我们需要知道第N项位于哪一组(即k值)以及在该组中的位置。
设组号k,则前k-1组共有元素个数:S(k-1) = 1+2+...+(k-1) = k*(k-1)/2
第k组有k个元素。
因此,我们要求最小的k,使得S(k) >= N,即 k(k+1)/2 >= N,而S(k-1)=k(k-1)/2 < N。
那么,第N项在第k组中的位置为:index = N - k(k-1)/2 - 1(组内索引,从0开始)。
然后,根据k的奇偶性确定方向:
如果k是奇数,则该组方向为从左到右(分子递增),所以分子为:index+1(因为分子从1开始递增)
如果k是偶数,则该组方向为从右到左(分子递减),所以分子为:k - index(因为分子从k递减到1)
分母:因为分子分母之和为k+1,所以分母 = (k+1) - 分子。
因此,分数 = 分子/分母。
验证:
第1项:N=1
找k:k=1时,S(1)=1>=1,所以k=1。
index = 1 - 0 - 1 = 0(因为S(0)=0,所以N-S(0)=1,组内位置0)
k=1为奇数,分子=0+1=1,分母=1+1-1=1 -> 1/1
第2项:N=2
找k:k=1时,S(1)=1<2;k=2时,S(2)=3>=2(注意,前1组元素和S(1)=1,所以第2组第一个元素是第2个,第2组有2个元素,所以第2项在第2组)
index = 2 - 1 - 1 = 0(因为S(1)=1,所以N-S(1)=2-1=1,但是组内索引从0开始,所以第一个元素索引0)
k=2为偶数,分子=2-0=2,分母=2+1-2=1 -> 2/1
第3项:N=3
在第2组中,组内位置:index = 3-1-1=1(因为S(1)=1,所以3-1=2,然后第2组有2个元素,所以第二个元素索引1)
k=2为偶数,分子=2-1=1,分母=3-1=2 -> 1/2
第4项:N=4
找k:k=2时,S(2)=3<4;k=3时,S(3)=6>=4,所以k=3。
index = 4 - 3 - 1? 不对,S(2)=3,所以index=4-3-1=0? 实际上,组内位置是第0个(因为4-3=1,所以是第3组的第一个元素)
k=3为奇数,分子=0+1=1,分母=3+1-1=3 -> 1/3
第5项:N=5,在第3组,组内位置=5-3=2(索引从0开始,所以索引1?不对,应该是第二个元素)
index = 5 - S(2) - 1 = 5-3-1=1? 不对,应该是:位置=5-3=2(因为前两组共3个,所以第5项是第3组的第2个元素,索引从0开始,所以索引1?)
重新计算:组内位置 = N - S(k-1) - 1,其中S(k-1)=S(2)=3,所以5-3-1=1(索引1)
k=3奇数,分子=1+1=2,分母=4-2=2 -> 2/2
第6项:N=6,在第3组,组内位置=6-3=3,索引=3-1=2? 不对,应该是:组内位置=6-3=3,但第3组只有3个元素,所以索引=2(第三个元素)
计算:index=6-3-1=2(因为前两组3个,所以第3组第一个是第4项,第二个是第5项,第三个是第6项)
k=3奇数,分子=2+1=3,分母=4-3=1 -> 3/1
第7项:N=7,找k:k=3时,S(3)=6<7;k=4时,S(4)=10>=7,所以k=4。
组内位置:7-6=1(即第4组的第1个元素,索引0)
k=4偶数,分子=4-0=4,分母=5-4=1 -> 4/1
第8项:N=8,在第4组,组内位置=8-6=2(索引1),分子=4-1=3,分母=5-3=2 -> 3/2
第9项:N=9,在第4组,组内位置=9-6=3(索引2),分子=4-2=2,分母=5-2=3 -> 2/3
第10项:N=10,在第4组,组内位置=10-6=4(索引3),分子=4-3=1,分母=5-1=4 -> 1/4
所以序列:1/1, 2/1, 1/2, 1/3, 2/2, 3/1, 4/1, 3/2, 2/3, 1/4, ... 符合。
算法步骤:
1. 输入N。
2. 确定组号k:k满足 k(k+1)/2 >= N 且 (k-1)k/2 < N 的最小k。
我们可以解不等式:k(k-1)/2 < N <= k(k+1)/2
可以二分查找k,或者从k=1开始累加直到和>=N。
3. 计算前k-1组的元素个数:S = k*(k-1)/2
4. 组内位置:pos_in_group = N - S (注意,这个值在1到k之间)
然后转换为组内索引:index = pos_in_group - 1 (0-indexed)
5. 判断k的奇偶性:
if k % 2 == 1 (奇数):
分子 = index + 1
else (偶数):
分子 = k - index
分母 = k + 1 - 分子 (因为分子分母之和为k+1)
6. 输出分数:分子/分母
注意:k的确定方法:从k=1开始,令s=0,然后循环:s += k; 当s>=N时,当前k就是组号。注意,此时s是前k组的元素和(包括第k组),而前k-1组的和是s0 = s - k。
因此,也可以:
k=0, s=0
while (s < N) {
k++;
s += k;
}
循环结束后,k就是组号,且前k组的总和是s,前k-1组的和是s-k。
则组内位置:pos_in_group = N - (s - k) (因为s-k就是前k-1组的和)
但是注意,当s>=N时,循环结束,此时s是前k组的总和,而前k-1组的总和是s0 = s - k,所以组内位置 = N - s0 = N - (s - k)
例如:N=1:
k=1: s=1>=1 -> 跳出,s0=0,组内位置=1-0=1 -> 索引0(正确)
N=2:
k=1: s=1<2 -> k=2, s=1+2=3>=2 -> 跳出,s0=3-2=1,组内位置=2-1=1 -> 索引0(第2组的第一个元素)
因此,代码实现:
步骤:
1. 初始化k=0, s=0
2. while (s < N) { k++; s += k; } // 此时s>=N,k就是组号
3. s0 = s - k; // 前k-1组的元素个数
4. pos_in_group = N - s0; // 在第k组中的位置(从1到k)
5. index = pos_in_group - 1; // 组内索引(0到k-1)
6. if (k % 2 == 1)
numerator = index + 1;
else
numerator = k - index;
denominator = k + 1 - numerator;
7. 输出 numerator << '/' << denominator
注意:k的奇偶性判断,以及分子分母的计算。
但是,注意:分母= k+1 -分子,这个关系是由分子分母之和为k+1决定的。
例如:k=2(第2组)时,分子分母之和为3;k=3时,和为4。
因此,我们得到分数。
代码实现:
注意:N的范围没有给出,但应该考虑较大的N,比如10^8,那么k大约在sqrt(2N)级别,所以循环次数大约是O(sqrt(N)),对于10^8,sqrt(2*10^8)≈14142,可以接受。
但是,如果N很大(比如10^18),我们需要用二分法求k。不过题目没有给出N的范围,我们先按照一般情况处理(循环加)。
由于题目没有说明N的范围,我们假设N在int范围内(最大2e9),那么用循环加k可能会超时(k最大约63245,循环6万多次,可以接受)。但为了保险,我们可以用二分查找k。
二分查找k:满足 k(k+1)/2 >= N 的最小k。
k的范围:1到大约sqrt(2N)向上取整。
二分查找步骤:
low=1, high=足够大(比如1000000,或者用公式:high = (int)sqrt(2.0*N)+100)
while (low < high) {
mid = (low+high)/2;
if (mid*(mid+1)/2 >= N)
high = mid;
else
low = mid+1;
}
k = low;
然后,s0 = k*(k-1)/2; // 前k-1组的和
pos_in_group = N - s0;
然后计算分子分母。
但是注意,k*(k+1)/2可能溢出,所以当N很大时,使用long long。
题目没有给出N的范围,但为了通用性,我们假设N在[1,10^18]之间,所以使用long long。
因此,我们使用二分查找k,并注意使用long long。
然而,用户没有给出N的范围,但为了安全,我们使用二分查找。
代码步骤(使用二分查找):
long long N;
cin >> N;
long long low = 1, high = 2000000000; // 2e9,因为sqrt(2*1e18)约为1.4e9,所以high取2e9不够,取5e9?或者用公式
实际上,我们可以设置high为2*sqrt(N)+10,但N最大10^18,sqrt(N)=10^9,所以high=2*sqrt(N)+10=2e9+10,但2e9+10可能会溢出,所以用long long。
我们这样:high = 2 * (long long)sqrt(N) + 100; 但是N可能很小,所以直接取high=1e10?或者用更安全的方法:先估计k的下界,k>=sqrt(2N)-1,所以high=max(100, 2*sqrt(2*N))? 但是sqrt(2*N)可能不是整数。
另一种方法:high = 2 * (long long)sqrt(2*N) + 100; // 这样足够覆盖
但是,为了避免浮点数精度问题,我们可以用指数增长确定high(倍增),但这里为了简单,我们直接取high=2000000000LL; 对于10^18,这个high不够,所以我们需要更大的high,比如high=20000000000LL(200亿)?但10^18时,k大约为sqrt(2e18)≈1.414e9,所以high至少要大于1.414e9,我们取high=2e9+1000可能不够,取high=5e9。
或者,我们直接使用:
low=1, high=2000000000;
如果high*(high+1)/2 < N,那么high *=2,直到high*(high+1)/2 >= N。
但是,由于N可能很大(比如1e18),所以high*(high+1)/2可能溢出,所以我们需要用除法估计。
更安全的方法:用指数增长确定high上界:
long long high = 1;
while (high*(high+1)/2 < N) {
high *= 2;
}
这样,我们就能找到一个上界high,然后在这个区间二分。
但是,当N很大时,high*(high+1)/2可能溢出,所以我们可以这样判断:high*(high+1) < 2*N? 但是乘法仍然可能溢出(当high很大时,high*high会溢出)。
因此,我们使用除法:high <= (sqrt(8*N+1)-1)/2 的近似值?但这样我们又要用浮点数。
另一种方法:用while循环条件避免溢出:当high很大时,我们判断如果high > 2e9,那么我们可以直接设置high=2e9+1000,因为N最大10^18,k最大约1.4e9,所以我们可以设置high=2e9(如果N>2e9*(2e9+1)/2,那么就会出错,但2e9*(2e9+1)/2约2e18,所以当N>2e18时,我们的high=2e9就不够,所以需要动态调整)。
为了处理大N,我们使用:
long long high = 1;
if (N>1) {
while (high*(high+1)/2 < N) {
high = high * 2;
if (high > 1000000000000000000LL) // 避免溢出,设置一个上限
break;
}
}
// 然后二分
但是,如果N很大,high*high会溢出,所以我们在循环条件中避免乘法?我们可以用除法:high <= (sqrt(8*N+1)-1)/2,但这样需要浮点数。
或者,我们这样:用指数增长,但用除法判断:
while ( (high+1) <= (2*N) / (high+1) ) // 这个条件不准确
我们换一种思路:我们要求最小的k使得k(k+1)/2>=N,那么k大约在sqrt(2N)附近,所以我们用sqrt(2N)的2倍作为上界,但sqrt(2N)可能不是整数,且当N很大时,sqrt(2N)用浮点数表示会有精度问题。
因此,我们使用二分查找,但初始上界设定为max(100, (long long)(2*sqrt(N)+100)),但sqrt(N)在N很大时浮点数精度不够。
所以,我们使用指数增长的方法来设定上界,同时避免溢出:
long long high = 1;
while (calc(high) < N) { // calc(k)=k*(k+1)/2
high *= 2;
// 防止溢出,当high很大时,calc(high)可能溢出,所以当high大于一个阈值(比如1e9)时,我们改为用除法估算
// 或者,我们直接判断:如果high > 2e9,那么跳出,用二分查找
}
但是,calc(high)在high很大时会溢出,所以我们在calc(high)中,如果high很大,我们用除法判断:如果high*(high+1)/2 >= N,但是乘法可能溢出,所以我们可以这样:
if (N <= 0) break; // 实际上N>=1
if (high > (LLONG_MAX-1)/2) { // 避免乘法溢出
// 此时high已经很大,那么k一定在[high0, high]之间,我们跳出,然后二分
break;
}
为了简单,我们假设N最大10^18,那么k最大约为1.4e9,所以我们可以设置初始上界high=2000000000(20亿),因为1.4e9<20亿。
所以,对于N<=10^18,我们设置high=2000000000就足够了(因为2000000000*(2000000000+1)/2 > 1e18? 计算:2e9 * 2e9 = 4e18,除以2=2e18>1e18,所以足够)。
因此,我们可以直接设置high=2000000000(20亿),然后二分。
但是,如果N>2e18,那么20亿不够,所以我们可以这样:
long long low = 1, high = 2;
while (calc(high) < N) { // calc(k)=k*(k+1)/2
low = high;
high *= 2;
if (high > 10000000000LL) // 如果超过100亿,我们检查一下,避免溢出
break;
}
然后在这个区间[low, high]中二分。
这里我们采用指数增长确定上界。
为了代码的健壮性,我们这样:
long long low_bound = 1, high_bound = 1;
while (calc(high_bound) < N) {
low_bound = high_bound + 1;
high_bound *= 2;
// 如果high_bound已经很大,可能溢出,则退出
if (high_bound > 1000000000000000000LL)
break;
}
然后二分查找在区间[low_bound, high_bound]中找最小的k使得calc(k)>=N。
其中calc(k)=k*(k+1)/2 (注意:k*(k+1)可能会溢出,所以我们可以用除法判断?但这里k最大不超过high_bound,而high_bound我们控制不超过1e18,那么k*(k+1)最大1e36,long long也存不下(long long一般64位,最大约9e18)),所以我们需要避免溢出。
因此,我们这样写calc(k):
if (k % 2 == 0)
return k/2 * (k+1);
else
return (k+1)/2 * k;
但是,这样还是可能溢出,所以我们在二分判断时,用除法来避免溢出:
mid = (low_bound + high_bound) / 2;
// 计算 mid*(mid+1)/2 和 N 比较
// 如果 mid*(mid+1)/2 >= N,则high=mid
// 否则 low=mid+1
但是乘法可能溢出,所以我们可以这样判断:
if (mid > (LLONG_MAX-1)/mid) {
// 说明mid*(mid+1)会溢出,那么mid*(mid+1)/2肯定大于N(因为N<=10^18,而mid*(mid+1)/2溢出说明大于LLONG_MAX,肯定大于N)
// 所以,此时应该往小的找,所以high_bound=mid-1;
}
但是这样麻烦,我们可以用除法:如果2*N / mid < mid+1,那么mid*(mid+1)/2>=N,但这是整数除法,有误差。
或者,我们使用double,但double的精度只有15-16位,对于10^18的数,double可以精确表示整数吗?不行,因为10^18有19位数字,double的尾数只有52位,所以不能精确表示。
因此,我们使用__int128(如果编译器支持)?但题目要求C++,且标准C++不支持,所以不用。
另一种方法:用减法避免乘法:计算mid*(mid+1)/2,我们可以用循环?不行,太慢。
或者,我们这样:如果mid很大(比如大于1e9),那么我们可以用浮点数近似,但会有精度问题。
所以,我们假设N最大10^18,那么k最大约为1.4e9,那么mid最大1.4e9,mid*(mid+1)最大约1.96e18,而long long最大为9e18,所以不会溢出。
因为1e9 * 1e9 = 1e18,而1.4e9 * 1.4e9 = 1.96e18 < 9e18,所以不会溢出。
因此,我们可以直接计算:value = mid*(mid+1)/2 (注意,先乘再除可能会溢出,但mid最大1.4e9,所以mid*(mid+1)最大1.96e18,而long long最大为9e18,所以不会溢出)
所以,我们可以直接计算。
因此,二分查找:
long long low = 1, high = high_bound;
while (low < high) {
long long mid = (low+high)/2;
long long t = mid*(mid+1)/2;
if (t >= N) {
high = mid;
} else {
low = mid+1;
}
}
k = low;
然后,s0 = k*(k-1)/2; // 前k-1组的和,注意:k*(k-1)可能很大,但k最大1.4e9,所以最大1.96e18,long long可以存下。
但是,k*(k-1)可能为奇数,除以2会向下取整?但公式是整数,因为连续两个整数必有一个偶数,所以能整除。
然后,pos_in_group = N - s0;
然后计算分子分母。
但是,我们还要注意:当k=1时,s0=0,所以N-s0=1,正确。
因此,代码实现:
long long N;
cin >> N;
// 如果N=1,直接返回1/1
// 步骤1:确定上界high_bound
long long low_bound = 1, high_bound = 1;
if (N==1) {
// 直接输出1/1
} else {
while (true) {
// 计算high_bound对应的和
// 注意:high_bound*(high_bound+1)/2 可能溢出?但high_bound最大1.4e9,所以不会溢出(因为1.4e9*1.4e9=1.96e18,long long可以存下)
long long t = high_bound*(high_bound+1)/2;
if (t >= N) break;
low_bound = high_bound+1;
if (high_bound > 2000000000) { // 如果已经超过20亿,那么跳出,避免无限循环(理论上不会,因为N<=1e18时,k最大1.4e9,所以20亿足够)
high_bound = 2000000000;
break;
}
high_bound *= 2;
}
}
// 现在在区间[low_bound, high_bound]中二分
long long k;
long long low = low_bound, high = high_bound;
while (low < high) {
long long mid = (low+high)/2;
long long t = mid*(mid+1)/2;
if (t >= N) {
high = mid;
} else {
low = mid+1;
}
}
k = low;
// 计算前k-1组的元素个数:s0 = k*(k-1)/2
long long s0 = k*(k-1)/2;
long long pos_in_group = N - s0; // 第k组中的第几个(从1到k)
long long index = pos_in_group - 1; // 组内索引(0-indexed)
long long numerator, denominator;
if (k % 2 == 1) { // 奇数,从左到右(分子递增)
numerator = index + 1;
} else { // 偶数,从右到左(分子递减)
numerator = k - index;
}
denominator = k+1 - numerator;
cout << numerator << '/' << denominator << endl;
但是,注意:k*(k+1)/2可能等于N,那么pos_in_group就是k,index=k-1,这是正确的。
测试:N=1, N=2, N=3, N=4, ... 到N=10。
我们写一个简单的测试函数。
但是,为了简单,如果N比较小,我们可以不用二分,直接累加。这里我们使用二分查找是为了处理大N。
不过,如果N很小,二分查找也可以。
因此,我们按照上述代码实现。
但是,我们也可以不用指数增长确定上界,因为k最大不超过1.5*sqrt(2*N),而N最大10^18时,k最大1.4e9,所以我们可以直接设置上界为2000000000(20亿),然后二分。
所以,简化:
long long k;
long long low = 1, high = 2000000000; // 20亿
while (low < high) {
long long mid = (low+high)/2;
long long t = mid*(mid+1)/2;
if (t >= N) {
high = mid;
} else {
low = mid+1;
}
}
k = low;
然后继续。
这样更简单。
因此,最终代码:
long long N;
cin >> N;
long long low = 1, high = 2000000000; // 20亿,足够覆盖k(因为20亿*(20亿+1)/2>1e18,且k最大1.4e9<20亿)
while (low < high) {
long long mid = (low+high)>>1;
// 计算mid*(mid+1)/2
long long t = mid * (mid+1) / 2;
if (t >= N) {
high = mid;
} else {
low = mid+1;
}
}
long long k = low;
long long s0 = k*(k-1)/2; // 前k-1组的和
long long pos_in_group = N - s0; // 第k组中的位置(从1开始)
long long index = pos_in_group - 1; // 组内索引(0开始)
long long numerator, denominator;
if (k % 2 == 1) {
numerator = index + 1;
} else {
numerator = k - index;
}
denominator = k+1 - numerator;
cout << numerator << '/' << denominator << endl;
测试:
N=1: k=1, s0=0, pos_in_group=1, index=0, k%2=1 -> numerator=1, denominator=2-1=1 -> 1/1
N=2: k=2, s0=1, pos_in_group=1, index=0, k%2=0 -> numerator=2, denominator=3-2=1 -> 2/1
N=3: k=2, s0=1, pos_in_group=2, index=1, k%2=0 -> numerator=2-1=1, denominator=3-1=2 -> 1/2
N=4: k=3, s0=3, pos_in_group=1, index=0, k%2=1 -> numerator=1, denominator=4-1=3 -> 1/3
N=5: k=3, s0=3, pos_in_group=2, index=1, k%2=1 -> numerator=2, denominator=4-2=2 -> 2/2
N=6: k=3, s0=3, pos_in_group=3, index=2, k%2=1 -> numerator=3, denominator=4-3=1 -> 3/1
N=7: k=4, s0=6, pos_in_group=1, index=0, k%2=0 -> numerator=4, denominator=5-4=1 -> 4/1
N=8: k=4, s0=6, pos_in_group=2, index=1, k%2=0 -> numerator=3, denominator=5-3=2 -> 3/2
N=9: k=4, s0=6, pos_in_group=3, index=2, k%2=0 -> numerator=2, denominator=5-2=3 -> 2/3
N=10: k=4, s0=6, pos_in_group=4, index=3, k%2=0 -> numerator=1, denominator=5-1=4 -> 1/4
正确。
注意:当k很大时,k*(k-1)/2可能溢出?但k最大20亿,20亿*(20亿-1)/2 约等于 2e18,而long long最大为9e18,所以不会溢出。
因此,我们使用long long,并按照上述代码。
但是,如果N非常大(比如1e20),那么20亿不够,但题目没有给出范围,我们假设N在long long范围内,且k最大不超过20亿(因为20亿*(20亿+1)/2>1e18,所以N<=1e18时,k最大1.4e9<20亿,所以20亿足够)。
所以,我们使用high=2000000000(20亿)作为上界。
代码:</think>### Z字形分数序列算法实现
#### 序列规律分析
Z字形分数序列的排列规则如下:
1. **按分子分母和分组**:分子分母和为 $k$ 的分数位于第 $k-1$ 组($k \geq 2$)
2. **组内元素数量**:和为 $k$ 的组有 $k-1$ 个元素(分子从 $1$ 到 $k-1$)
3. **方向交替排列**:
- **奇数组**($k$ 为奇数):从左到右,分子递增($1, 2, \dots, k-1$)
- **偶数组**($k$ 为偶数):从右到左,分子递减($k-1, k-2, \dots, 1$)
4. **分母计算**:分母 = $k$ - 分子
例如前10项序列:
```
1/1, 2/1, 1/2, 1/3, 2/2, 3/1, 4/1, 3/2, 2/3, 1/4
```
#### 算法步骤
1. **确定组号 $k$**:
- 满足 $k(k+1)/2 \geq N$ 的最小 $k$
- 使用二分查找提高效率
2. **计算前 $k-1$ 组元素和**:
$$s_0 = \frac{k(k-1)}{2}$$
3. **计算组内位置**:
- 组内位置 $pos = N - s_0$
- 组内索引 $index = pos - 1$(0-based)
4. **计算分子分母**:
- 奇数 $k$:分子 $= index + 1$
- 偶数 $k$:分子 $= k - index$
- 分母 $= k + 1 - \text{分子}$
#### C++代码实现
```cpp
#include <iostream>
using namespace std;
int main() {
long long N;
cin >> N;
// 二分查找确定组号k
long long low = 1, high = 2000000000; // 20亿足够覆盖N≤10^18
while (low < high) {
long long mid = (low + high) / 2;
long long t = mid * (mid + 1) / 2; // 前mid组元素和
if (t >= N) {
high = mid;
} else {
low = mid + 1;
}
}
long long k = low;
// 计算前k-1组元素和
long long s0 = k * (k - 1) / 2;
long long pos = N - s0; // 组内位置(1-based)
long long index = pos - 1; // 组内索引(0-based)
long long numerator, denominator;
if (k % 2 == 1) { // 奇数组:分子递增
numerator = index + 1;
} else { // 偶数组:分子递减
numerator = k - index;
}
denominator = k + 1 - numerator; // 分母
cout << numerator << '/' << denominator << endl;
return 0;
}
```
#### 示例测试
| 输入N | 输出 | 解释 |
|-------|----------|-------------------------------|
| 1 | `1/1` | 第1组第1个元素 |
| 2 | `2/1` | 第2组第1个元素(分子递减) |
| 3 | `1/2` | 第2组第2个元素 |
| 4 | `1/3` | 第3组第1个元素(分子递增) |
| 10 | `1/4` | 第4组第4个元素 |
#### 算法分析
1. **时间复杂度**:$O(\log k)$,其中 $k \approx \sqrt{2N}$
2. **空间复杂度**:$O(1)$
3. **处理范围**:支持 $N \leq 10^{18}$(使用`long long`类型)
#### 应用场景
该算法适用于:
1. 数学序列生成(如Farey序列变种)
2. 数据压缩中的模式编码
3. 图像处理中的Z字形扫描(如JPEG编码)[^1][^2]
阅读全文
相关推荐













