Let’s call \(S\) the (infinite)
string that is made by concatenating the consecutive positive integers
(starting from \(1\)) written down in
base \(10\).
# valid_splits:用于跨一个边界 (y-1)|y 的计数 # # 设模式串 P 长度 m,考虑切分: # P = A | B # 其中 A 非空、B 非空。 # # 若 P 跨越边界 (y-1)|y,且 y 为右端整数: # - B 必须是 y 的前缀(所以 B 不能以 '0' 开头,否则 y 会出现前导零) # - A 必须是 (y-1) 的后缀 # # 关键变换(等价写法): # 让 y 的前缀为 B, # 且 y 的后缀(长度 |A|)等于 (A + 1) mod 10^{|A|} # 这样通过“固定前缀 + 固定后缀”的计数,就能统计跨边界出现次数。 # # 存储形式:(prefix_len=b, prefix_val=int(B), suffix_len=a, suffix_val=(int(A)+1) mod 10^a) self.valid_splits = [] for a inrange(1, self.m): A = self.pat_str[:a] B = self.pat_str[a:] # B 是 y 的前缀,必须保证 B 的首位不是 0(否则 y 作为十进制数会有前导零) if B[0] == '0': continue b = self.m - a prefix_val = int(B) suffix_len = a suffix_val = (int(A) + 1) % pow10[a] self.valid_splits.append((b, prefix_val, suffix_len, suffix_val)) self.s_count = len(self.valid_splits) # 可用切分的数量
# -------- internal: full blocks -------- defcount_internal_full_block(self, d: int) -> int: """ 统计:所有 d 位数 y(范围 [10^{d-1}, 10^d-1]),模式串 P 完全落在 y 内部的出现总数。 若 d < m:不可能 若 d == m:每个 y 只有一个窗口,且 y==P 时贡献 1,所以总数为 1 若 d > m:有两类窗口: - 从开头对齐(i=0):形如 y = P * 10^{d-m} + suffix,共 10^{d-m} 个 - 中间对齐(i>0):需要枚举前缀长度 i (1..d-m),每个 i 对应 前缀有 9*10^{i-1} 种(首位不能为 0) 后缀有 10^{d-i-m} 种 总数为 sum_{i=1..d-m} 9*10^{i-1} * 10^{d-i-m} = (d-m)*9*10^{d-m-1} 所以总数: 10^{d-m} + (d-m)*9*10^{d-m-1} """ dm = d - self.m if dm == 0: return1 return pow10[dm] + dm * 9 * pow10[dm - 1]
# -------- internal: <= limit for fixed length d and position i -------- defcount_internal_pos_leq(self, limit: int, d: int, i: int) -> int: """ 在所有 d 位数 y 且 y <= limit 的范围内,统计: 模式串 P 作为 y 的子串,且子串起点在 y 的第 i 位(0-index)时的出现次数。 记: y = (prefix of length i) + P + (suffix of length (d-i-m)) 特殊处理: i==0:prefix 为空,此时 y = P*10^{d-m} + suffix,suffix 取 0..10^{d-m}-1 i>0:prefix 首位不能为 0(保证 y 是 d 位数),suffix 任意 计数方法: 通过把 limit 拆成 prefix_limit 与 rest, 精确计算 prefix < prefix_limit 的全量贡献 + prefix == prefix_limit 的边界贡献。 """ m = self.m # limit 不足以达到 d 位数 if limit < pow10[d - 1]: return0
# i == 0:y = P*10^{d-m} + suffix if i == 0: base = self.pat_int * pow10[d - m] if base > limit: return0 # suffix 的可取数量受 limit 限制 returnmin(pow10[d - m], limit - base + 1)
# i > 0:prefix(长度 i, 首位不为0) + P + suffix suffix_len = d - i - m suffix_pow = pow10[suffix_len] # suffix 取值范围大小
# prefix == prefix_limit:需要 rest >= P*suffix_pow 才能放下 P need = self.pat_int * suffix_pow if rest >= need: count += min(suffix_pow, rest - need + 1) return count
defcount_internal_large(self, N: int) -> int: """ 统计 internal 部分(P 完全落在某个 y 内部)在 y <= N 的出现次数, 仅对 y >= 10^{m-1} 的“长整数部分”计数(短整数部分由 base_count 暴力覆盖)。 做法: - 对所有位数 d < D(D = len(N)),整段使用 count_internal_full_block(d) - 对位数 d = D,只统计 y <= N 的部分,需对每个起点 i 使用 count_internal_pos_leq(N,D,i) """ if N < pow10[self.m - 1]: return0 D = len(str(N)) total = 0
# 完整位数段:d = m..D-1 for d inrange(self.m, D): total += self.count_internal_full_block(d)
# 边界位数段:d = D,只能统计 <= N 的那部分 for i inrange(0, D - self.m + 1): total += self.count_internal_pos_leq(N, D, i) return total
# -------- boundary: count y <= limit with fixed prefix/suffix -------- defcount_affix_leq(self, limit: int, d: int, prefix_len: int, prefix_val: int, suffix_len: int, suffix_val: int) -> int: """ 统计:在所有 d 位数 y 且 y <= limit 的范围内,满足: y 的前 prefix_len 位 == prefix_val y 的后 suffix_len 位 == suffix_val 的 y 的数量。 对应跨边界匹配: y 的前缀固定为 B(prefix_len=b) y 的后缀固定为 (A+1) mod 10^a(suffix_len=a) 设中间长度: mid_len = d - prefix_len - suffix_len 那么 y 结构为: y = prefix_val * 10^{d-prefix_len} + mid * 10^{suffix_len} + suffix_val mid 的范围:[0, 10^{mid_len}-1] 所以问题变成:求满足上述形式且 y <= limit 的 mid 的数量。 """ if limit < pow10[d - 1]: return0 mid_len = d - prefix_len - suffix_len if mid_len < 0: return0
defcount_boundary_full_block(self, d: int) -> int: """ 对所有 d 位数 y(整段)统计跨边界出现次数: 对每一种合法切分 (A|B),固定前缀 B 和固定后缀 (A+1 mod 10^a), 在所有 d 位数 y 中,满足“前缀+后缀”的 y 数量为 10^{d-m}(中间自由位数 d-m)。 合法切分共有 s_count 种,因此整段总数: s_count * 10^{d-m} """ returnself.s_count * pow10[d - self.m]
defcount_boundary_large(self, N: int) -> int: """ 统计 boundary 部分(跨 (y-1)|y 的出现)在 y <= N 的出现次数, 同样只对 y >= 10^{m-1} 的“长整数部分”计数(短整数部分由 base_count 暴力覆盖)。 """ if N < pow10[self.m - 1]: return0 D = len(str(N)) total = 0
# 完整位数段:d = m..D-1 for d inrange(self.m, D): total += self.count_boundary_full_block(d)
# 位数段 d=D:对每个 split 用 count_affix_leq 处理 <=N 的边界 for (b, prefix_val, a, suffix_val) inself.valid_splits: total += self.count_affix_leq(N, D, b, prefix_val, a, suffix_val) return total
defocc(self, N: int) -> int: """ Occ(N) = 模式串 P 在 "123...N" 中出现次数。 若 N <= small_limit:直接暴力(保证涵盖所有跨多边界的特殊情况)。 否则: Occ(N) = base_count + internal_large(N) + boundary_large(N) """ if N <= self.small_limit: return count_in_prefix_string(self.pat_str, N) returnself.base_count + self.count_internal_large(N) + self.count_boundary_large(N)
# ----------------------------------------- # 4) find f(n): the start position of n-th occurrence of n in S # ----------------------------------------- deffind_f(n: int) -> int: """ 求题目定义的 f(n):n 在无限串 S 中第 n 次出现的起始位置(1-indexed)。 做法: 1) 用 PatternCounter 提供的 occ_end(N) 对 N 二分: 找到最小 y,使得 Occ(y) >= n 那么第 n 次出现一定“新发生在 y 的加入过程中” 即发生在串 "...(y-1)(y)" 的某个窗口中,并且该窗口必须使用到 y 的数字。 2) 令 before = Occ(y-1),则在加入 y 这一段里新增的出现次数为: k = n - before 我们只需在 W = str(y-1)+str(y) 中找出所有“跨到 y 一侧”的匹配起点, 取第 k 个。 3) 将该起点换算到全局位置: 全局起点 = len("123...(y-2)") + (窗口起点 p) + 1 其中 len("123...(y-2)") = digit_count_upto(y-2) """ pc = PatternCounter(n)
# --------------- 二分找 y --------------- # 找到最小 y,使得 Occ(y) >= n l, r = 1, 1 while pc.occ(r) < n: r *= 2 while l < r: mid = (l + r) // 2 if pc.occ(mid) >= n: r = mid else: l = mid + 1 y = l
# before = Occ(y-1),k 表示在“新增部分(加入 y)”里是第 k 个新增匹配 before = pc.occ(y - 1) if y > 1else0 k = n - before
# --------------- 在 str(y-1)+str(y) 里定位第 k 个新增匹配 --------------- P = pc.pat_str m = pc.m
left = str(y - 1) if y > 1else"" right = str(y) W = left + right L = len(left) # y-1 的长度,用于判断匹配是否“使用到了 y 的数字”
# 收集所有匹配起点 p,且必须满足 p+m > L: # - 若 p+m <= L,则匹配完全落在 left(y-1)内部,这不是“新增匹配”,应排除 starts = [] for p inrange(0, len(W) - m + 1): if W[p:p + m] == P and p + m > L: starts.append(p)