可莉学数学 - 题解
标签与难度
标签: 字符串, 枚举, 模拟, 数学, 大整数处理 难度: 2100
题目大意喵~
呜喵~ 旅行者,你好呀!可莉又被关禁闭了,这次是因为数学题... 我们要帮帮她,这样她才能早点出来玩!
题目是这样的:有一个神奇的无限小数,它的样子是 0.012345678910111213...,小数部分是把所有自然数(从0开始哦)一个接一个拼起来组成的。
丽莎阿姨会问 q 次,每次给出一个数字字符串 x,我们需要找到这个字符串 x 第一次 在这个无限小数的小数部分出现时,它的起始位置是小数点后的第几位。
比如说,对于 0.0123... 这个小数:
0出现在第 1 位。1出现在第 2 位。8910这个串,是由数字8、9和10拼接起来的一部分,它第一次出现的位置就是8的起始位置,也就是第 9 位。
我们要做的就是写一个程序,来回答丽莎阿姨的所有问题,帮可莉解决这个大麻烦,喵~
解题思路分析
这道题看起来是在一个无限长的字符串里找东西,感觉有点吓人,但别怕,我带你一步步把它分解开,呐!
这个无限长的小数串 S 是由 0, 1, 2, 3, ... 这样有序的数字拼接起来的。我们要找的查询字符串 x,它在 S 中的出现方式,仔细想想,无非就下面几种情况:
x恰好是某个数字N的一部分。比如x="23"是数字123的一部分。x恰好是某个完整的数字N。比如x="123"就是数字123。x跨越了两个或多个数字。比如x="910"是数字9的末尾和数字10的开头拼成的。
这么多可能性,要怎么找才能找到第一次出现的那个位置呢?第一次出现,意味着它一定是由尽可能小的数字构成的。所以,我们可以从这个角度入手!
任何一个 x 的出现,我们都可以看作是 ... str(N-1) + str(N) + str(N+1) ... 这个序列的一部分。x 在这个序列里,要么完全包含在某个 str(N) 中,要么跨越了 N-1 和 N 的边界,要么是 str(N) 和 str(N+1) 的拼接,等等。
一个非常直接且有效的思路是:枚举查询字符串 x 中,哪一部分是它遇到的第一个完整的数字。
听起来有点绕?让我举个例子!假设查询 x = "99100"。
- 它可能是由
(某个数字的后缀)+100+(后续数字的前缀)组成的。这里的100就是它遇到的第一个完整数字。 - 它也可能是由
99+100组成的。这里的99是它遇到的第一个完整数字。
所以,我们可以遍历 x 的所有子串,并假设这个子串就是 x 在无限小数串中遇到的第一个完整的数字,我们称之为 N。
设查询字符串 x 的长度为 len。我们用 i 和 j 来表示 x 的一个子串 x.substr(i, j),其中 i 是起始位置,j 是长度。
对于每一个子串 sub = x.substr(i, j):
我们把它当做第一个完整数字
N。所以,N = stoll(sub)。(如果sub以 '0' 开头且长度大于1,那它就不是一个合法的数字表示,可以直接跳过,喵~)现在我们有了假设的数字
N,那么x中在N前面的部分x.substr(0, i),就必须是数字N-1的后缀。- 我们可以生成
N-1的字符串str(N-1)。 - 检查
x.substr(0, i)是不是str(N-1)的后缀。如果不是,或者长度对不上,说明我们的假设是错的,就继续尝试下一个子串。 - 特别注意:如果
N=0,它前面没有数字了,所以x的前面必须是空的,即i必须等于0。
- 我们可以生成
同理,
x中在N后面的部分x.substr(i+j),就必须是str(N+1) + str(N+2) + ...这个序列的前缀。- 我们可以从
N+1开始,一个一个生成数字的字符串,然后和x.substr(i+j)进行匹配。 - 如果中途有任何不匹配,说明这个假设也是错的。
- 我们可以从
如果以上两步检查都通过了,恭喜!我们找到了一个
x的合法出现方式!它的起始位置是什么呢?x的出现,是从N-1的某个后缀开始的。- 我们需要一个辅助函数
getPosition(k),用来计算数字k在无限小数串中的起始位置(1-based)。 - 那么
N的起始位置是getPosition(N)。 - 我们的
x比N早i个字符开始,所以x的起始位置就是getPosition(N) - i。 - 我们把这个位置记录下来,作为候选答案。
我们遍历完所有可能的子串 x.substr(i, j) 作为 N 的情况,取所有候选答案中最小的那个,就是最终的答案啦!
如何计算 getPosition(k) 呢? 这个也简单!我们可以分段计算:
- 1位数字(0-9)有10个,总长度是
10 * 1 = 10。 - 2位数字(10-99)有90个,总长度是
90 * 2 = 180。 - 3位数字(100-999)有900个,总长度是
900 * 3 = 2700。 - ...
d位数字有9 * 10^(d-1)个,总长度是... * d。
要找 k 的位置,我们先把所有位数比 k 小的数字的总长度加起来,再加上从该位数最小的数字(如10, 100)到 k-1 的这些数字的总长度。最后别忘了最开始的 0 占了1位,所以要基于这个计算。
这个方法虽然要枚举所有子串,看起来有点暴力,但因为查询字符串 x 的长度不会特别长(一般在20以内),所以 O(len^3) 的复杂度是可以通过的,喵~
代码实现
这是我根据上面的思路,为你精心准备的一份代码,注释超详细的哦!
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <limits>
// 使用 long long 来处理可能很大的数字和位置
using namespace std;
using ll = long long;
// 计算数字 k 在 0123... 序列中的起始位置 (1-based)
// 比如 get_pos(0) -> 1, get_pos(1) -> 2, get_pos(10) -> 11
ll get_pos(ll k) {
if (k < 0) return -1; // 不应该发生
if (k == 0) return 1;
ll length_before = 1; // 数字 '0' 的长度
ll power_of_10 = 1;
ll num_digits = 1;
while (true) {
// 当前位数下,数字的数量 (例如 1-9, 10-99, 100-999)
// 注意,我们从0开始,所以1位数的特殊处理
ll count_in_this_tier = (num_digits == 1) ? 9 : power_of_10 * 9;
// 如果 k 大于等于当前位数区间的上界
if (k >= power_of_10 + count_in_this_tier) {
length_before += count_in_this_tier * num_digits;
power_of_10 *= 10;
num_digits++;
} else {
// k 就在当前位数区间内
length_before += (k - power_of_10) * num_digits;
break;
}
}
// 返回 k 的起始位置,等于 0 到 k-1 所有数字的总长度 + 1
return length_before + 1;
}
void solve() {
string query_str;
cin >> query_str;
int n = query_str.length();
ll min_pos = -1;
// 枚举 query_str 的所有子串,假设这个子串是第一个遇到的完整数字 N
for (int i = 0; i < n; ++i) { // i: 第一个完整数字在 query_str 中的起始索引
for (int j = 1; i + j <= n; ++j) { // j: 第一个完整数字的长度
string num_part_str = query_str.substr(i, j);
// 如果数字长度大于1且以'0'开头,则不是一个合法的数字表示
if (j > 1 && num_part_str[0] == '0') {
continue;
}
ll N = stoll(num_part_str);
// --- 1. 检查 N 前面的部分 ---
// N 前面的部分是 query_str.substr(0, i)
// 它必须是 N-1 的后缀
string prefix_part_str = query_str.substr(0, i);
if (i > 0) {
if (N == 0) { // N=0 前面没有数字,所以 i 必须是 0
continue;
}
string prev_num_str = to_string(N - 1);
if (prev_num_str.length() < i || prev_num_str.substr(prev_num_str.length() - i) != prefix_part_str) {
continue; // 不是 N-1 的后缀,假设失败
}
}
// --- 2. 检查 N 后面的部分 ---
// N 后面的部分是 query_str.substr(i + j)
// 它必须是 N+1, N+2, ... 序列的前缀
string suffix_part_str = query_str.substr(i + j);
string generated_suffix = "";
ll current_num = N + 1;
while (generated_suffix.length() < suffix_part_str.length()) {
generated_suffix += to_string(current_num);
current_num++;
}
if (generated_suffix.rfind(suffix_part_str, 0) != 0) { // 检查前缀是否匹配
continue; // 不匹配,假设失败
}
// --- 3. 假设成功,计算位置并更新答案 ---
ll current_pos = get_pos(N) - i;
if (min_pos == -1 || current_pos < min_pos) {
min_pos = current_pos;
}
}
}
cout << min_pos << endl;
}
int main() {
// 加速输入输出,喵~
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int q;
cin >> q;
while (q--) {
solve();
}
return 0;
}复杂度分析
时间复杂度:
Q是询问的次数。- 对于每次询问,我们有两层循环来枚举子串。外层循环
i从0到N-1,内层循环j从1到N-i,这里有 种组合。 - 在循环内部,
stoll、to_string和字符串比较操作的耗时与字符串长度成正比,最坏情况下是 。特别是检查后缀序列的部分,虽然看起来会生成很长的串,但总长度不会超过N,所以也是 。 - 因此,总的时间复杂度是 。
空间复杂度:
- 对于每次询问,我们主要消耗的空间是存储查询字符串
query_str和一些在计算过程中生成的临时字符串,它们的长度都与N相关。所以空间复杂度是 。
- 对于每次询问,我们主要消耗的空间是存储查询字符串
知识点总结
这道题真是一次有趣的冒险,我们来总结一下学到了什么吧,喵~
- 问题分解: 面对一个复杂的问题,把它分解成更小、更易于处理的子问题是王道!我们把 "在无限串中找子串" 分解为 "枚举子串作为第一个完整数字" 的情况。
- 系统性枚举: "暴力" 有时候并不可怕,只要它是有组织的、能覆盖所有情况的。这里的
O(N^3)枚举就是一个例子,通过锚定一个关键部分(第一个完整数字),让搜索变得有序。 - 字符串处理: 熟练运用
substr,to_string,stoll等函数是解决这类问题的基本功,呐。 - 数学计算与模式发现:
get_pos函数的实现,需要我们发现数字串长度的规律,并用数学方式表达出来。这在很多数论和组合问题中都很常见。 - 边界情况处理: 一定要小心像
N=0这样的特殊情况。在算法竞赛中,细节决定成败!
希望能帮到可莉,也希望能帮到正在看题解的你!加油哦,你超棒的,喵~