当前位置: 首页 > news >正文

蓝桥杯真题解题思路——因数计数

前言

  印象里这题我在 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 n1 个里面有多少个为 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 n1 个里面有多少个为 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 kj+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)=spiqipkqk+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=ij=kl=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 qk1 种。

  • j ≠ i ∧ j ≠ k ∧ l = i j\neq i\wedge j\neq k\wedge l=i j=ij=kl=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) qiI(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)[spiqipkqk+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(x1)。由于同一组的元素在排序后的 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 重复的情况,容易被卡。但是蓝桥杯还是比较仁慈的,没有这种恶心数据,最终还是过了。当然在比赛的时候,如果有时间,最好能够考虑这种情况,并且改进代码。

相关文章:

  • [免费]微信小程序(校园)二手交易系统(uni-app+SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】
  • C++小课堂——构造函数与析构函数
  • 2025 Lakehouse 趋势全景展望:从技术演进到商业重构
  • Linux线程
  • Tauri+React+Ant Design跨平台开发环境搭建指南
  • Maven
  • B3DM转换成PLY
  • Spark之数据倾斜调优
  • 【后端】Flask vs Django vs Node.js 对比分析
  • Linux系统(以Ubuntu为例)安装高版本nodejs
  • 爬虫:一文掌握 Celery 分布式爬虫,及对应实战案例
  • 《AI模型变形记:从绿巨人到Hello Kitty的魔幻减肥营》
  • 【计算机网络——概述】
  • 【3D格式转换SDK】HOOPS Exchange技术概览(二):3D数据处理高级功能
  • 如何合理设置请求间隔?
  • 如何优化百度下拉框?下拉框展示规则是怎样的?
  • JavaEE--计算机是如何工作的
  • 优选算法的智慧之光:滑动窗口专题(二)
  • 如何将一台服务器的pip环境迁移到另一个机器?
  • 【开源-常用C/C++命令行解析库对比】
  • 巴防空系统击落印度无人机,印称巴方违反停火协议
  • 代理销售保险存在误导行为,农业银行重庆市分行相关负责人被罚款0.1万元
  • 马上评|持续对标国际一流,才有22项“全球最优”
  • 证监会主席吴清:我们资本市场最重要的特征是“靠谱”
  • 巴方称印军发动24起袭击,巴境内6处地点遭袭致8人死亡
  • 上海虹桥高铁站拦门事件反转,谁在带偏网友?