跳转到内容
返回

Advent of Code 2025 - 第2天题解

编辑此页

核心任务描述

给定一个数字范围,找出范围中所有由一个完全相同的数字片段重复若干次拼接而成的数字。其中,第一部分要求仅需找出由两个相同片段组成的数字,第二部分则要求找出由任意数量(不少于两个)相同片段组成的数字。

数字是否符合要求(第一部分)是否符合要求(第二部分)说明
123123由两个 “123” 组成
121212由三个 “12” 组成
1234无重复片段组成
1111可看作由两个 “11” 或四个 “1” 组成

思考过程

字符串判断

题目本身并不难,最简单的思路就是使用字符串操作来判断一个数字是否由重复片段组成。具体来说:

  1. 将数字转换为字符串形式 s
  2. 遍历可能的片段长度 k,范围从 1s.len() / 2
  3. 对于每个片段长度,检查字符串是否可以被该长度整除。
  4. 如果可以整除,则提取第一个片段 part = &s[0..k],并检查 s 是否等于 part 重复若干次的结果。
  5. 如果找到了符合条件的片段,则该数字符合要求。

这种方法虽然简单直观,但时间复杂度较高,尤其是在数字范围较大时,效率不佳。

数学方法

我们从第一部分的要求出发,考虑如何通过数学方法来判断一个数字是否由两个相同片段组成。

比如数字 123123,我们可以将其表示为:

123123=123×103+123=123×(103+1)=123×1001\begin{aligned} 123123 = & 123 \times 10^3 + 123 \\ = & 123 \times (10^3 + 1) \\ = & 123 \times 1001 \end{aligned}

不难发现,对于任何长度为偶数的数字,如果它由两个相同的 kk 位片段组成,那么它一定可以写成 part×(10k+1)part \times (10^{k} + 1) 的形式,其中 kkpartpart 的长度。我们可以将 10k+110^{k} + 1 记为 basebase,那么问题就转化为了判断一个数字 xx 是否可以被 basebase 整除。

基于上述分析,我们可以设计如下算法:

  1. 对于长度为偶数的数字 xx,计算其长度 LL
  2. 计算 k=L/2k = L / 2,并计算 base=10k+1base = 10^{k} + 1
  3. 检查 xx 是否可以被 basebase 整除。
  4. 如果满足上述条件,则 xx 符合要求。

这种方法的时间复杂度较低,因为我们只需进行一次除法运算即可判断。但仍然需要遍历数字范围内的所有数字。

反向思考 - 生成法

既然我们需要找出所有符合条件的数字,不妨反向思考,直接生成这些数字,而不是逐个判断每个数字是否符合要求。对于第一部分,我们可以通过以下步骤生成所有符合条件的数字:

  1. 对于一个特定的数字长度 LL,计算 k=L/2k = L / 2
  2. 计算 base=10k+1base = 10^{k} + 1
  3. 遍历所有可能的片段值 partpart,如果 part×basepart \times base 在给定范围内,则将其加入结果集。
范围限制

要小心,partpart 的范围有限制,必须令 partpart 是一个 kk 位数,防止生成不符合要求的数字。

比如使用 10011001 作为 basebase 时,1010 不能作为 partpart(生成的 1001010010 不符合要求);类似的,12341234 也不可用。

我们需要限制 partpart 的范围为 part[10k1,10k)part \in [10^{k-1}, 10^{k}),正好填充 kk 位数。

更进一步,由于题目最终需要的是所有符合条件的数字之和,我们可以直接使用高斯求和公式来计算这些数字的和,而不必显式地生成每个数字。算法因此变为:

  1. 对于给定的数字范围 [low,high][low, high] 和每个可能的数字长度 LL(偶数),计算 k=L/2k = L / 2base=10k+1base = 10^{k} + 1left=10k1left = 10^{k-1}right=10k1right = 10^{k} - 1
  2. 计算 partpart 的起始值和结束值,分别为 start=max(lowbase,left)start = \max\left(\left\lceil \frac{low}{base} \right\rceil, left\right)end=min(highbase,right)end = \min\left(\left\lfloor \frac{high}{base} \right\rfloor, right\right)
  3. 使用高斯求和公式计算 partpart 的和,并乘以 basebase 得到最终结果。

这样一来,时间复杂度就大幅下降了,我们不再需要遍历每个数字,而是直接计算出结果。因此结果复杂度从近似 O(N)O(N) 降低到了 O(logN)O(\log N) (与数字位数 log10N\log_{10} N 接近线形关系)。

推广到任意数量的重复片段

有了第一部分的基础,我们可以尝试推广到第二部分,寻找由任意数量(不少于两个)相同片段组成的数字。比如数字 121212,我们可以表示为:

121212=12×104+12×102+12=12×(104+102+100)\begin{aligned} 121212 = & 12 \times 10^4 + 12 \times 10^2 + 12 \\ = & 12 \times (10^4 + 10^2 + 10^0) \end{aligned}

我们不难发现,这实际上只需要将原先固定的 basebase 进行修改即可。对于由 rr 个相同的、长为 kk 片段组成的数字,我们可以将 basebase 定义为:

base=i=0r110ik=10rk110k1=10L110k1\begin{aligned} base = & \sum_{i=0}^{r-1} 10^{i \cdot k} \\ = & \frac{10^{r \cdot k} - 1}{10^{k} - 1} \\ = & \frac{10^{L} - 1}{10^{k} - 1} \end{aligned}

我们可以将符合条件的数字表示为:

n=part×10L110k1n = part \times \frac{10^{L} - 1}{10^{k} - 1}

基于此,我们可以设计如下算法:

  1. 对于给定的数字范围 [low,high][low, high] 和每个可能的数字长度 LL,计算 kk 的所有可能值(kk 必须是 LL 的因数且 k<Lk < L)。
  2. 对于每个 kk,计算 r=L/kr = L / kbase=10L110k1base = \frac{10^{L} - 1}{10^{k} - 1}
  3. 计算 partpart 的起始值和结束值,分别为 start=max(lowbase,left)start = \max\left(\left\lceil \frac{low}{base} \right\rceil, left\right)end=min(highbase,right)end = \min\left(\left\lfloor \frac{high}{base} \right\rfloor, right\right)
  4. 使用高斯求和公式计算 partpart 的和,并乘以 basebase 得到最终结果。

但这对吗?我们来想一想。比如对于一个数字 666666666666,我们看看会发生什么:

k=1part=6,r=6k=2part=66,r=3k=3part=666,r=2\begin{aligned} k = 1 & \Rightarrow part = 6, r = 6 \\ k = 2 & \Rightarrow part = 66, r = 3 \\ k = 3 & \Rightarrow part = 666, r = 2 \end{aligned}

类似的,来看数字 1212121212121212

k=2part=12,r=4k=4part=1212,r=2\begin{aligned} k = 2 & \Rightarrow part = 12, r = 4 \\ k = 4 & \Rightarrow part = 1212, r = 2 \end{aligned}

糟糕,这样的话,我们会多次计算同一个数字,导致结果错误。为了解决这个问题,我们需要确保每个数字只被计算一次。

那么我们到底重复计算了哪些数字?正是那些由更短片段组成的数字。比如 666666666666 可以看作是由片段长度为 11 的数字组成的,因此当我们计算片段长度为 2233 时,就重复计算了这个数字;类似地,1212121212121212 也在片段长度为 2244 时被重复计算。

对此我们不难发现,如果一个数字的两种分法 k1k_1k2k_2 满足 k1k_1k2k_2 的因数,那么较长的分法就会重复计算较短的分法所生成的数字。即:对于一个给定的总位数 LL,其因数 kk 对应的分法,会包含所有那些由 kk 的因数(记为 dd)所对应的分法生成的数字(其中 dd 也是 LL 的因数)。这也引出了一个核心的去重思路:对每个 LL,只考虑那些“重复次数 rr 为质数”的分法,然后将所有结果相加。因为任何合数 rr 的重复模式,都可以拆分为更小的质数次重复,从而被包含在质数 rr 的计数中。在算法上,我们可以拆解为两步:

  1. 找出 LL 的所有质因数,记为 r1,r2,,rmr_1, r_2, \ldots, r_m,分别计算以 rir_i 作为重复次数对应的结果。
  2. 使用容斥原理,去除重复计算的部分。由于质数之间互质,唯一的可能重叠计算部分即为长度为 11 的片段,因此我们只需从总和中减去长度为 11 的部分即可。

如此一来,我们不仅确保了结果的正确性,还保持了较高的计算效率。

核心算法实现

day02/src/main.rs
type Range = (u64, u64);
struct Puzzle {
ranges: Vec<Range>,
}
impl Puzzle {
fn prime_factors(mut n: u32) -> Vec<u32> {
let mut factors = BTreeSet::new();
while n.is_multiple_of(2) {
factors.insert(2);
n /= 2;
}
let mut divisor = 3;
while divisor * divisor <= n {
while n.is_multiple_of(divisor) {
factors.insert(divisor);
n /= divisor;
}
divisor += 2;
}
if n > 1 {
factors.insert(n);
}
factors.into_iter().collect()
}
fn get_sum_invalid_ids(range: Range, n: u32, repeat: u32) -> u64 {
// The pattern repeats every k = n / repeat digits
let k = n / repeat;
// Calculate the lower and upper bounds for n-digit numbers with the given
// pattern
let upper = 10u64.pow(n) - 1;
let base = upper / (10u64.pow(k) - 1);
let lower = 10u64.pow(k - 1) * base;
// Get the overlap between the given range and (lower, upper)
let (start, end) = range;
let start = start.max(lower);
let end = end.min(upper);
// Convert back to the base range
let start = start.div_ceil(base);
let end = end / base;
if start > end {
return 0;
}
(end - start + 1) * (start + end) / 2 * base
}
}
impl Solution for Puzzle {
fn part1(&self) -> String {
self.ranges
.par_iter()
.map(|&(start, end)| {
let min_n = start.ilog10() + 1;
let max_n = end.ilog10() + 1;
(min_n..=max_n)
.filter(|n| n % 2 == 0)
.map(|n| Self::get_sum_invalid_ids((start, end), n, 2))
.sum::<u64>()
})
.sum::<u64>()
.to_string()
}
fn part2(&self) -> String {
self.ranges
.par_iter()
.map(|&(start, end)| {
let min_n = start.ilog10() + 1;
let max_n = end.ilog10() + 1;
(min_n..=max_n)
.filter(|&n| n > 1)
.map(|n| {
// Sum of all repeating digits (e.g., 1111, 2222, ..., 9999 for n=4)
let all_same = Self::get_sum_invalid_ids((start, end), n, n);
// Get all patterns with smaller, prime repeat factors
Self::prime_factors(n).into_iter().filter(|&k| k < n).fold(
all_same,
|mut sum, k| {
sum += Self::get_sum_invalid_ids((start, end), n, k);
sum -= all_same;
sum
},
)
})
.sum::<u64>()
})
.sum::<u64>()
.to_string()
}
}

总结

完整代码见此处

通过对题目的深入分析与数学方法的应用,我们成功地将问题从一个需要遍历大量数字的任务,转化为一个高效的计算问题。利用数字的结构特性和容斥原理,我们不仅提高了算法的效率,还避免了复杂的判断逻辑。虽然题目本身并不复杂,但是优化过程却是十分有趣。


编辑此页
分享此文章:

上一篇
Advent of Code 2025 - 第7天题解
下一篇
Advent of Code 2025 - 总结