『题解』CodeForces CF1177B Digits Sequence (Hard Edition)

题目传送门

题目大意

输出 \(1234567891011121314151617\dots\) 的第 \(k(1 \le k \le 10^{12})\) 项。

思路

考虑直接模拟,\(\mathcal{O}(k)\) 时间复杂度建立这个序列,并输出第 \(k\) 项。显然,这是不可行的方法,数据范围为 \(10^{12}\)但是CF1177A可以。

那么我们就要考虑更快的方法:找出包含第 \(k\) 项的那个数。

确定位数

首先,需要确定这个数的位数。

先来观察一下位数分别为 \(1,2,3,4,5\dots\) 位的数分别有多少个:

  • \(1\) 位:\([1 \dots 9]\)\(9\) 个数。
  • \(2\) 位:\([10 \dots 99]\)\(90\) 个数。
  • \(3\) 位:\([100 \dots 999]\)\(900\) 个数。
  • \(\dots\)

但是,我们要求的是这些数合在一起,就像字符串拼接一样,一共有多少位。很简单,将不同位数的数的数量乘上它们各自的位数,就是一共的位数了。

  • \(1\) 位:一共 \(9\) 个数,\(9\) 位。
  • \(2\) 位:一共 \(90\) 个数,\(180\) 位。
  • \(3\) 位:一共 \(900\) 个数,\(2700\) 位。
  • \(\dots\)

设位数为 \(len\),那么当前位数的数的总位数计算公式为:\(len \times 10^{len-1} \times 9\)

得到这些结论,那么得出到底是几位数就很简单了。

进行一个循环,定义 \(len=1\)\(len\) 每次加一,我们每次将 \(k\) 减去有 \(len\) 位的数的总位数,直到发现再减就比 \(0\) 小了为止。

\(len\) 最后的值即为包含第 \(k\) 位数的数的位数。

怎么这么绕口令啊。

代码实现:

ll i,len; // 用 i 存放 10^len-1
// 只要序列第 k 位不在当前位数的数里,那就继续减,直到找到为止
for(i=1; k-len*i*9>=0; len++,i*=10){
    k-=len*i*9; // 减去当前位数的数的总长度,下一次要找比当前多一位的
}

确定包含第 \(k\) 位的数是多少

我们定义包含第 \(k\) 位的数为 \(n\)

最基本的,就是将位数 \(\le len\) 的数的个数都加进去,这样就找到了这个数的最小值。这个操作要和上面的找位数一起执行

如何准确确定这个数是多少呢?

在我们找到这个数的最小值后,\(k\) 成为了序列上从有 \(len\) 位数开始的第 \(k\) 项。\(len\) 是位数,那么 \(n+\dfrac{k}{len}\) 就是包含第 \(k\) 位的数。

例如 \(len=3\),经过操作后的 \(k=6\),那么 \(n\) 就应该是 \(\dots 100101102103 \dots\) 中的 \(101\),也就是 \(99+\dfrac{3}{6}\)

应该很容易理解吧,就是要求第 \(k\) 位,\(k\) 除以 \(len\) 就代表有多少个三位数在 \([k,len]\) 之间,就这么简单。

还有一个问题,就是——

\(k \bmod len \ne 0\)

上面只考虑了最基本的情况,这里需要加上一个特判。

还是一个例子:\(len=3,k=5\),截取的序列为 \(\dots 100101102103 \dots\),尝试使 \(n \gets n+\dfrac{k}{len}\)C++ 有自动向下取整,所以得到的结果为 \(100\)。显然,包含第 \(k\) 位的数并不是 \(100\),而是 \(100+1=101\)

\(n \gets n+1\) 后,答案也有所变更。我们可以通过式子 \(len-(k \bmod len)\) 得出答案在哪一位,设 \(len-(k \bmod len)\)\(bit\),那么我们将 \(n\) 的后 \(bit-1\) 位去掉即可。

处理后,\(n\) 的个位就是答案。

代码

代码里注释也很详细的(可以说是差不多又讲了一遍)。

此代码也可过CF1177A

#include <iostream>
using namespace std;
typedef long long ll;
// n 存放第 k 个数字所在的数,用于计算最后的答案
// len 存放 k 所在数的位数
// bit 用于存储序列第 k 位在 n 中从右往左数哪一位
ll k,n,len=1,bit;

int main(){
    // k 存放的是要求序列的位数,我们循环时直接让它减去已经跳过的位
    cin >> k;
    // 越到高位,包含的数越多(废话)
    // 只要序列第 k 位不在当前位数的数里,那就继续减,直到找到为止
    for(ll i=1; k-len*i*9>=0; len++,i*=10){
        k-=len*i*9; // 减去当前位数的数的总长度,下一次要找比当前多一位的
        n+=i*9; // 计算序列第 k 位所在的数
    }
    // 经过上面的处理后的 k 就是序列从有 len 位数开始的第 k 项
    // 就比如 len=3,那么现在的答案就是 100101102103... 的第 k 项
    // 那么 k/len 就是从 len 位数开始,经过了多少个数才到包含第 k 项的那个数
    // 于是包含 k 的那个数 n 加上 k/len 就正好是包含 k 的那个数了
    n+=k/len;
    // 还有一个问题:k/len 除不尽的情况
    // 如果 k/len 刚好除的尽,那么答案就是这个数的个位,应该很容易理解吧
    // 若除不尽,万(hěn)能(dú)的 C++(C cǎo) 会向下取整
    // 那就说明,现在的 n 还不是包含 k 的那个数,n+1 才是包含 k 的数
    // 例如,执行完上面代码的 k=5,len=3,n=100
    // 在 ...100101102... 中,显然,n 应该是 101 才对
    // 所以就要加一个特判
    if(k%len!=0){
        n++; // 转移到下一个数
        bit=len-(k%len);
        // 我们还要将 n 的个位变为答案
        // 可以通过 len-(k%len) 得出最终答案在 n 的从右往左数第几位
        // 然后,将 n 后面 bit 位都去掉,就得到最终答案;n 的第一位了
        while(bit-->0){
            n/=10;
        }
    }
    cout << n%10 << endl; // 剩下数的最后一位即为答案
    return 0;
}

代码里是不是好多注释都是废话?

posted @ 2022-01-26 21:44  仙山有茗  阅读(40)  评论(0编辑  收藏  举报