蓝桥杯真题解题思路——因数计数
- 开源代码
- 2025-09-15 07:15:01

前言
印象里这题我在 24 年 4 月的蓝桥杯 C++ 本科生 A 组见到过,但是当时我实在太菜了,在赛场上没有想出来。时隔一年我重温这道题,没有借鉴任何人的思路,一上来就奔着 O ( n log  n ) O(n\log n) O(nlogn) 的复杂度,虽然最后实际上是 O ( n log  2 n ) O(n\log^2 n) O(nlog2n) 的复杂度,但也算并独立完成了这道题的求解。这个时间复杂度可能不是最优的,但是应该是比较好理解的。
思路首先,我们可以理解一下题目的意思。这里是题目链接。
求解二元组的个数首先,题目里提到了一个所谓的“二元组”。
小蓝随手写出了含有 n n n 个正整数的数组 { a 1 , a 2 , ⋯ , a n } \{a_1,a_2,\cdots,a_n\} {a1,a2,⋯,an},他发现可以轻松地算出有多少个有序二元组 ( i , j ) (i,j) (i,j) 满足 a j a_j aj 是 a i a_i ai 的一个因数。
我们肯定要先研究这些二元组,再去研究后面四元组的事情。求解二元组说起来容易,可能是因为小蓝写的数组太小了, O ( n 2 ) O(n^2) O(n2) 的时间复杂度也无所谓。但是限制于本题的数据,我们最好是能找到一种 O ( n log  n ) O(n\log n) O(nlogn) 的方式求出这些二元组。 那么这样的二元组有多少个呢?我们可以定义一些符号:
p i p_i pi:满足 a j ( j ≠ i ) a_j(j\neq i) aj(j=i) 是 a i a_i ai 的倍数的 j j j 的个数。即数组里面除了 a i a_i ai,其它这 n − 1 n-1 n−1 个里面有多少个为 a i a_i ai 的倍数。 q i q_i qi:满足 a j ( j ≠ i ) a_j(j\neq i) aj(j=i) 是 a i a_i ai 的因数的 j j j 的个数。即数组里面除了 a i a_i ai,其它这 n − 1 n-1 n−1 个里面有多少个为 a i a_i ai 的因数。显然,这些二元组的总数应为 s = ∑ i = 1 n p i = ∑ i = 1 n q i s=\sum_{i=1}^np_i=\sum_{i=1}^nq_i s=∑i=1npi=∑i=1nqi。
为什么 ∑ i = 1 n p i = ∑ i = 1 n q i \sum_{i=1}^np_i=\sum_{i=1}^nq_i ∑i=1npi=∑i=1nqi?你可以理解为,当我们找到一个二元组 ( i , j ) (i,j) (i,j) 时, p i p_i pi 将增加 1 1 1,而 q j q_j qj 也会增加 1 1 1。每发现一个二元组,都会导致 ∑ p \sum p ∑p 和 ∑ q \sum q ∑q 都增加 1 1 1,所以有 ∑ i = 1 n p i = ∑ i = 1 n q i \sum_{i=1}^np_i=\sum_{i=1}^nq_i ∑i=1npi=∑i=1nqi。
那么怎么求解 ∑ i = 1 n p i \sum_{i=1}^np_i ∑i=1npi 或者 ∑ i = 1 n q i \sum_{i=1}^nq_i ∑i=1nqi?我们可以先对数组 a \boldsymbol a a 从小到大排序,这对答案显然不会有影响,排序后的数组仍然记为 a \boldsymbol a a。 接下来令 i i i 从 1 1 1 遍历到 n n n,分别去求解 p i p_i pi,然后全部求和就是 s s s。说到求解 p i p_i pi,一个朴素的想法是:遍历 a i + 1 a_{i+1} ai+1 到 a n a_n an,一个一个判断是不是 a i a_i ai 的倍数。很遗憾,这种方法时间复杂度过高,为 O ( n 2 ) O(n^2) O(n2)。 那我们不妨换一种思路。我要求 p i p_i pi,也就是 a i a_i ai 的倍数的个数,我可以看数组中等于 2 a i 2a_i 2ai 的有多少个,再看等于 3 a i 3a_i 3ai 的有多少个……一直这样找下去,然后全部加起来就是 p i p_i pi。由于现在的 a \boldsymbol a a 是一个升序数组,使用二分查找,可以使得找数组中等于 t a i ta_i tai 元素个数的时间复杂度降到 O ( log  n ) O(\log n) O(logn)。这个 t t t 是从 2 2 2 开始枚举,然后是 3 3 3,那么一直枚举到多少可以结束呢?如果记 a \boldsymbol a a 中最大的元素是 m m m,那么答案是 ⌊ m / a i ⌋ \lfloor m/a_i\rfloor ⌊m/ai⌋。 根据上面的思路,求解 p i p_i pi 的时间复杂度应该是 O ( ( m log  n ) / a i ) O((m\log n)/a_i) O((mlogn)/ai)。那么求解 s = ∑ i = 1 n p i s=\sum_{i=1}^np_i s=∑i=1npi,时间复杂度应该就是: O ( ( m log  n ) ∑ i = 1 n ( 1 / a i ) ) O\left((m\log n)\sum_{i=1}^n(1/a_i)\right) O((mlogn)∑i=1n(1/ai))。假设 a i a_i ai 互不相等,那么通过基本的放缩,可以知道 ∑ i = 1 n ( 1 / a i ) ≤ ∑ i = 1 n ( 1 / i ) ∼ O ( log  n ) \sum_{i=1}^n(1/a_i)\leq \sum_{i=1}^n(1/i)\sim O(\log n) ∑i=1n(1/ai)≤∑i=1n(1/i)∼O(logn),因此求解 s s s 的时间复杂度是 O ( m log  2 n ) O(m\log^2n) O(mlog2n)。题目中 m m m 和 n n n 是同上限的,因此也可以将总时间复杂度写作 O ( n log  2 n ) O(n\log^2n) O(nlog2n)。 那如果 a i a_i ai 有重复的,上面的放缩就不成立。但是重复的 a i a_i ai 意味着其中某个的 p i p_i pi 计算出来了,其它的直接照抄这个 p i p_i pi 就行,此时就不需要额外算了。这样时间复杂度还是 O ( n log  2 n ) O(n\log^2n) O(nlog2n)。 顺带说一句这个 q i q_i qi,其实顺带就给算完了。比如说我们找到数组中等于 2 a i 2a_i 2ai 的元素为 a j a_j aj 到 a k a_k ak。那么 p i p_i pi 的变动是增加 k − j + 1 k-j+1 k−j+1,而 q j q_j qj 到 q k q_k qk 都需要加上 1 1 1。由于每次对数组 q q q 都是在某个连续区间上加上 1 1 1,因此可以构造 q q q 的差分数组 q d i f f \mathit{qdiff} qdiff,保证在常数时间复杂度内完成 q d i f f \mathit{qdiff} qdiff 的修改(否则会拖累时间复杂度到 O ( n 2 ) O(n^2) O(n2));所有 a i a_i ai 遍历完了之后,使用 O ( n ) O(n) O(n) 的时间从 q d i f f \mathit{qdiff} qdiff 构造 q q q 即可。
求解四元组的个数然后题目问我们四元组 ( i , j , k , l ) (i,j,k,l) (i,j,k,l) 有多少个。实际上我是这样想的,先确定了 i i i 和 k k k,在此基础上想一下 ( j , l ) (j,l) (j,l) 有多少种可能?答案是 π ( i , k ) = s − p i − q i − p k − q k + 1 + I ( a i = a k ) \pi(i,k)=s-p_i-q_i-p_k-q_k+1+\mathbb I(a_i=a_k) π(i,k)=s−pi−qi−pk−qk+1+I(ai=ak)。这里的 I ( ⋅ ) \mathbb I(\cdot) I(⋅) 把一个 bool 值转换为整数,意思是当 a i = a k a_i=a_k ai=ak 时, I ( a i = a k ) = 1 \mathbb I(a_i=a_k)=1 I(ai=ak)=1,否则 I ( a i = a k ) = 0 \mathbb I(a_i=a_k)=0 I(ai=ak)=0。 为什么是这个结果?首先,可以确定 a k a_k ak 是 a i a_i ai 的倍数。如果我们随便填 ( j , l ) (j,l) (j,l),显然有 s s s 种填法。然而这其中有一些会导致四元组有重复元素,要抠掉。一共有四种情况需要抠掉:
j = i j=i j=i:此时 l l l 可以填的数字有 p i p_i pi 种,所以这种情况有 p i p_i pi 种。
j = k j=k j=k:此时 l l l 可以填的数字有 p k p_k pk 种,所以这种情况有 p k p_k pk 种。
j ≠ i ∧ j ≠ k ∧ l = k j\neq i\wedge j\neq k\wedge l=k j=i∧j=k∧l=k:如果是单纯的 l = k l=k l=k,那么 j j j 有 q k q_k qk 种可能,并且这些填法保证 j ≠ k j\neq k j=k。但是由于 a i a_i ai 是 a k a_k ak 的因数,所以这 q k q_k qk 种可能包含 j = i j=i j=i 的情况。那么这种情况就是 q k − 1 q_k-1 qk−1 种。
j ≠ i ∧ j ≠ k ∧ l = i j\neq i\wedge j\neq k\wedge l=i j=i∧j=k∧l=i:如果是单纯的 l = i l=i l=i,那么 j j j 有 q i q_i qi 种可能,并且这些填法保证 j ≠ i j\neq i j=i。
如果 a k a_k ak 也是 a i a_i ai 的因数(考虑到 a i a_i ai 是 a k a_k ak 的因数,等价于 a k = a i a_k=a_i ak=ai),那么这 q i q_i qi 种可能包含 j = k j=k j=k 的情况。如果 a k a_k ak 不是 a i a_i ai 的因数,那么这 q i q_i qi 种可能不包含 j = k j=k j=k 的情况。因此,这种情况有 q i − I ( a i = a k ) q_i-\mathbb I(a_i=a_k) qi−I(ai=ak) 种。
那么回到我们的题目,最终四元组的个数就是遍历所有的二元组 ( i , k ) (i,k) (i,k),并求和 π ( i , k ) \pi(i,k) π(i,k)。可以比表示为 a n s = ∑ ( i , k ) π ( i , k ) = ∑ ( i , k ) [ s − p i − q i − p k − q k + 1 + I ( a i = a k ) ] \mathit{ans}=\sum_{(i,k)}\pi(i,k)=\sum_{(i,k)}[s-p_i-q_i-p_k-q_k+1+\mathbb I(a_i=a_k)] ans=∑(i,k)π(i,k)=∑(i,k)[s−pi−qi−pk−qk+1+I(ai=ak)]。 我们可以把上面的和式拆开:
∑ ( i , k ) ( s + 1 ) \sum_{(i,k)}(s+1) ∑(i,k)(s+1):这也就是 s ( s + 1 ) s(s+1) s(s+1)。 ∑ ( i , k ) I ( a i = a k ) \sum_{(i,k)}\mathbb I(a_i=a_k) ∑(i,k)I(ai=ak):数组 a \boldsymbol a a 中值相同的元素分为一组。对于每一组而言,如果这一组有 x x x 个元素,那么这一组的贡献就是 x ( x − 1 ) x(x-1) x(x−1)。由于同一组的元素在排序后的 a \boldsymbol a a 中必连续,所以可以在 O ( n ) O(n) O(n) 复杂度内解决。 − ∑ ( i , k ) ( p i + q i + p k + q k ) -\sum_{(i,k)}(p_i+q_i+p_k+q_k) −∑(i,k)(pi+qi+pk+qk):这就要求我们重新温故一下二元组的求取过程。之前是每找到一个二元组 ( i , k ) (i,k) (i,k),执行 p i = p i + 1 p_i =p_i+1 pi=pi+1 和 q k = q k + 1 q_k=q_k+1 qk=qk+1。现在我们每找到一个二元组 ( i , k ) (i,k) (i,k),都需要计算一下 − ( p i + q i + p k + q k ) -(p_i+q_i+p_k+q_k) −(pi+qi+pk+qk) 并加入答案。最终,我们可以在 O ( n log  2 n ) O(n\log^2n) O(nlog2n) 的时间复杂度内求得答案。
AC 代码 import os import sys def make_array(length,val): return [val for _ in range(length)] def make_2d_array(rows,cols,val): return [[val for _ in range(cols)] for _ in range(rows)] def make_3d_array(d1,d2,d3,val): return [[[val for _ in range(d3)] for _ in range(d2)] for _ in range(d1)] def read_int(): return int(input()) def read_ints(): return [int(i) for i in input().split()] from bisect import bisect_left,bisect_right n = read_int() a = sorted(read_ints()) m = max(a) p = make_array(n,-1) q = make_array(n,0) sp = 0 qdiff = make_array(n,0) qdiff[0] = -1 qsum = make_array(n + 1,0) psum = make_array(n + 1,0) ans = 0 for i in range(n): for j in range(1,m // a[i] + 1): target = a[i] * j lidx = bisect_left(a,target) if lidx != n and a[lidx] == target: ridx = bisect_right(a,target) p[i] += ridx - lidx qdiff[lidx] += 1 if ridx < n: qdiff[ridx] -= 1 last = -1;count = 0 for i in range(n): if i == 0: q[0] = qdiff[0] else: q[i] = q[i - 1] + qdiff[i] sp += p[i] if a[i] != last: ans += count * (count - 1) last = a[i] count = 1 else: count += 1 # print(p);print(q);print(qdiff);exit() ans += sp * (sp + 1) + count * (count - 1) # print(ans);exit() for i in range(n): qsum[i + 1] = qsum[i] + q[i] psum[i + 1] = psum[i] + p[i] for i in range(n): ans += 2 * (p[i] + q[i]) for j in range(1,m // a[i] + 1): target = a[i] * j lidx = bisect_left(a,target) if lidx != n and a[lidx] == target: ridx = bisect_right(a,target) ans -= (p[i] + q[i]) * (ridx - lidx) ans -= psum[ridx] - psum[lidx] + qsum[ridx] - qsum[lidx] print(ans)上面的代码存在一个漏洞,就是求 s s s 的时候并没有考虑 a i a_i ai 重复的情况,容易被卡。但是蓝桥杯还是比较仁慈的,没有这种恶心数据,最终还是过了。当然在比赛的时候,如果有时间,最好能够考虑这种情况,并且改进代码。
蓝桥杯真题解题思路——因数计数由讯客互联开源代码栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“蓝桥杯真题解题思路——因数计数”
 
               
               
               
               
               
               
               
               
   
   
   
   
   
   
   
   
  ![2023年全球及中国光伏硅片行业产量、市场竞争格局及趋势分析[图]](/0pic/pp_91.jpg) 
   
   
  