news 2026/8/22 17:16:06

蓝桥杯国赛真题解析:和与乘积问题的O(n)算法与双指针技巧

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
蓝桥杯国赛真题解析:和与乘积问题的O(n)算法与双指针技巧

1. 项目概述:从一道国赛真题看算法思维的深度

最近在复盘蓝桥杯历届国赛的经典题目,2021年第十二届国赛的“和与乘积”这道题让我印象尤为深刻。它不像一些纯考数据结构的题目那样直接,也不像某些动态规划题那样有明确的套路。这道题更像是一个精巧的数学谜题,披着简单题目的外衣,实则对选手的逻辑分析、数学归纳和边界处理能力提出了相当高的要求。很多朋友初次接触时,可能会觉得题目描述清晰,数据范围也不大,但一上手编码就发现处处是坑,要么超时,要么答案不对。今天,我就结合自己多次解题和教学的经验,把这题从里到外彻底拆解一遍,不仅告诉你“怎么做”,更要讲清楚“为什么这么做”,以及那些在标准题解里不会写的调试心得和思维误区。

简单来说,题目给定一个长度为n的整数数组,数组中的元素均为正整数。我们需要计算这个数组的所有非空连续子数组中,满足“子数组元素之和等于子数组元素之积”的个数。初看这个条件“和等于积”,在正整数范围内,感觉非常苛刻,似乎只有全1序列或者包含1的特定序列才能满足。这恰恰是题目的第一个思维陷阱,引导我们过早地陷入对数学性质的猜测,而忽略了题目给定的数据范围(n≤ 2×10^5)所暗示的算法复杂度要求——我们必须找到一个低于 O(n²) 的解法。直接暴力枚举所有子数组是 O(n²) 的,在 n=200000 时必然超时。因此,核心挑战在于如何利用“和等于积”这个强约束条件,设计出高效的查找或计数方法。

2. 核心思路解析:化乘为加与双指针滑动

面对“和等于积”这个条件,最直接的数学洞察是:对于一组大于1的正整数,它们的乘积会以极快的速度超过它们的和。例如,[2, 3],和为5,积为6,已经不相等;[2, 2, 2],和为6,积为8。只有当序列中包含足够多的“1”时,乘积的爆炸性增长才会被抑制,因为乘以1不会改变积,但会增加和。这提示我们,满足条件的子数组很可能大量集中在包含1的区段。

2.1 关键数学性质与问题转化

基于上述观察,我们可以进行一个关键的问题转化:对于一个不含1的子数组,如果它的长度超过一个很小的阈值(比如2或3),其积几乎必然大于和。我们来严格论证一下: 假设一个子数组不含1,最小元素为2。设其长度为 k。

  • 其和 sum ≥ 2k。
  • 其积 product ≥ 2^k。
  • 当 k=2 时,2^2=4, 2*2=4,边界情况[2,2]满足和等于积。
  • 当 k=3 时,2^3=8, 2*3=6,积已大于和。
  • 随着k增大,积将以指数级速度远超和。

因此,任何不含1且长度超过2的子数组,都不可能满足“和等于积”。唯一的例外是长度恰好为1的子数组(单个元素),此时和与积显然相等,只要该元素是正整数就成立。所以,所有长度为1的子数组都天然是答案的一部分,这部分的数量就是数组长度 n。

现在,问题简化为了:如何高效地找出所有长度大于等于2且满足条件的子数组?根据以上性质,这样的子数组必然包含至少一个“1”。我们可以把原数组按照非1的元素进行“切割”,得到一个由连续1组成的段(简称“1段”)和非1的“枢纽数”交替出现的结构。

例如,数组[2, 1, 1, 3, 1, 2, 1]可以被视为:枢纽2,接着一个长度为2的1段,枢纽3,长度为1的1段,枢纽2,长度为1的1段。 我们的搜索范围就可以限定在:以某个非1的“枢纽数”为核心,向左右两侧扩展其相邻的连续1段,所构成的所有子数组。因为一旦子数组包含了两个非1的枢纽数,且中间没有1间隔(或者1的个数不够多),其乘积会迅速膨胀,导致条件无法满足。

2.2 算法框架设计:以非1元素为锚点

基于这个认知,我们可以设计出算法的主框架:

  1. 初始化答案ans = n。因为所有单个元素都满足条件。
  2. 遍历数组中每一个大于1的元素(记为a[i]),将其作为潜在子数组的“核心”或“起点”。
  3. 对于每个核心a[i],我们分别向左右两个方向扩展,吸收连续的1,构造出以a[i]为唯一非1元素的候选子数组。同时,我们也需要考虑以两个相邻的非1元素为核心的情况(这是满足条件的最大可能长度)。
  4. 在扩展过程中,动态计算当前子数组的和与积。由于积的增长极快,我们需要在积超过一个合理上限(比如所有元素之和,这是一个明确上界)时停止扩展,避免数值溢出和无效计算。
  5. 检查当前子数组是否满足“和等于积”,若满足则计数。

这个算法的复杂度如何?因为我们在遍历每个非1元素时,向其左右扩展。由于乘积的快速增长,扩展的长度是有限的(通常很短)。更精确地说,在正整数且大部分数不太大的情况下,扩展的深度是 O(log(Sum)) 级别的,其中Sum是数组总和。因此,整体复杂度近似于 O(n log M),其中M与数据范围相关,完全可以应对 n=2e5 的规模。

3. 实现细节与双指针技巧

理论清晰后,我们进入实现环节。这里最大的难点是如何优雅地处理“向左右扩展连续1”的过程,并高效计算子数组的和与积。

3.1 预处理连续1段

一个非常实用的技巧是预处理两个数组leftOnes[i]rightOnes[i]

  • leftOnes[i]表示位置 i 左边连续1的个数(不包括 i 本身)。
  • rightOnes[i]表示位置 i 右边连续1的个数(不包括 i 本身)。

这样,当我们以a[i](a[i] > 1) 为核心时,可以立即知道它左边有L = leftOnes[i]个连续的1,右边有R = rightOnes[i]个连续的1。那么,所有以a[i]为唯一非1元素的子数组,就可以描述为:从左边选择l个1 (0 ≤ l ≤ L),从右边选择r个1 (0 ≤ r ≤ R),与a[i]共同组成的子数组[1,...,1, a[i], 1,...,1]

对于这样的子数组:

  • 长度len = l + 1 + r
  • sum = l * 1 + a[i] + r * 1 = l + a[i] + r
  • product = 1^l * a[i] * 1^r = a[i]
  • 满足“和等于积”的条件简化为:l + a[i] + r = a[i],即l + r = 0。 这意味着,对于一个非1元素为核心,左右只扩展1的子数组,要想和等于积,必须左右都不扩展1。也就是子数组就是[a[i]]本身。而这我们已经计入答案了(长度为1的情况)。所以,对于单一非1核心的情况,长度大于1的子数组不可能成立。

这个结论非常重要!它告诉我们,满足条件的、长度大于1的子数组,必须至少包含两个非1元素

3.2 双核心情形与滑动窗口

因此,我们需要将搜索目标调整为:考察每一对相邻的非1元素(a[i], a[j]),以及它们中间可能存在的连续1,和它们各自左右两边的连续1。

假设我们有两个相邻的非1元素,下标分别为ij(i < j),且它们之间没有其他非1元素(即a[i] > 1,a[j] > 1,且对于所有 i < k < j,有a[k] = 1)。 设它们之间有midOnes = j - i - 1个1。 设a[i]左边有L个连续1,a[j]右边有R个连续1。

现在,我们考虑一个子数组,它包含a[i]a[j],以及它们之间所有的1,并且可以向左右两侧再扩展若干连续的1。设从左边扩展了l个1 (0 ≤ l ≤ L),从右边扩展了r个1 (0 ≤ r ≤ R)。那么这个子数组的结构是:[1...1(共l个), a[i], 1...1(共midOnes个), a[j], 1...1(共r个)]

对于这个子数组:

  • sum = l + a[i] + midOnes + a[j] + r
  • product = a[i] * a[j]。(因为所有1相乘仍为1)
  • 条件sum == product转化为:l + r + (a[i] + midOnes + a[j]) == a[i] * a[j]

我们可以将a[i] + midOnes + a[j]视为一个固定值fixed_sum。那么条件变为:l + r == a[i] * a[j] - fixed_sum。 令target = a[i] * a[j] - fixed_sum。 我们需要找到所有满足0 ≤ l ≤ L,0 ≤ r ≤ R,且l + r == target的整数对(l, r)。每一对这样的(l, r)就对应一个满足条件的子数组。

3.3 高效计算满足条件的 (l, r) 对数

如何计算这个对数呢?这变成了一个简单的组合问题:

  • 如果target < 0,显然无解。
  • 如果target == 0,只有一种情况:l=0r=0
  • 如果target > 0,那么l可以从max(0, target - R)取到min(L, target)。因为r = target - l,必须满足0 ≤ r ≤ R。 所以,有效的l的取值范围是:[max(0, target - R), min(L, target)]。 如果这个区间存在,那么满足条件的(l, r)对数就是这个区间的长度:count = min(L, target) - max(0, target - R) + 1,当然这个值需要和0取最大值,避免负值。

这样,对于每一对相邻的非1元素(i, j),我们都可以在 O(1) 时间内计算出以其为核心,并能向左右扩展1的、满足条件的子数组数量。

3.4 算法步骤总结

  1. 输入与初始化:读入数组a,长度n。初始化答案ans = n(所有单元素子数组)。
  2. 预处理连续1段:计算leftOnesrightOnes数组。
  3. 提取非1元素索引:遍历数组,将所有值大于1的元素的下标记录到一个列表pos中。
  4. 遍历相邻非1元素对
    • 对于pos列表中每一对相邻的下标(p[k], p[k+1]),令i = p[k],j = p[k+1]
    • 计算midOnes = j - i - 1
    • 计算fixed_sum = a[i] + midOnes + a[j]
    • 计算product = a[i] * a[j]这里必须使用long long类型,防止溢出
    • 计算target = product - fixed_sum
    • 如果target < 0,跳过。
    • 获取L = leftOnes[i],R = rightOnes[j]
    • 计算l_min = max(0LL, target - R)l_max = min((long long)L, target)
    • 如果l_min <= l_max,则增加答案:ans += (l_max - l_min + 1)
  5. 输出答案

注意:上述步骤只处理了包含恰好两个非1元素的子数组。根据之前的数学性质,包含三个或以上非1元素的子数组,其乘积会远大于和,在数据范围有限且元素为正整数的情况下,不可能满足条件,无需考虑。但严谨起见,可以在计算product时,如果发现product已经大于数组所有元素之和(一个绝对上界),可以提前终止对该核心对的进一步扩展(虽然我们这里只扩展到两个核心,但如果是更一般的扩展算法,这个剪枝很重要)。

4. 代码实现与关键点注释

下面给出基于上述思路的C++实现代码,并附上关键注释。

#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int n; cin >> n; vector<int> a(n); long long total_sum = 0; // 数组总和,用于潜在剪枝 for (int i = 0; i < n; ++i) { cin >> a[i]; total_sum += a[i]; } // 1. 预处理每个位置左右连续1的个数 vector<int> leftOnes(n, 0), rightOnes(n, 0); for (int i = 1; i < n; ++i) { if (a[i - 1] == 1) { leftOnes[i] = leftOnes[i - 1] + 1; } else { leftOnes[i] = 0; } } for (int i = n - 2; i >= 0; --i) { if (a[i + 1] == 1) { rightOnes[i] = rightOnes[i + 1] + 1; } else { rightOnes[i] = 0; } } // 2. 记录所有大于1的元素的位置 vector<int> pos; for (int i = 0; i < n; ++i) { if (a[i] > 1) { pos.push_back(i); } } // 3. 初始答案:所有长度为1的子数组 long long ans = n; // 4. 遍历所有相邻的大于1的元素对 for (size_t k = 0; k + 1 < pos.size(); ++k) { int i = pos[k]; int j = pos[k + 1]; // 计算中间1的个数 int midOnes = j - i - 1; // 计算固定部分的和 long long fixed_sum = a[i] + midOnes + a[j]; // 计算乘积,注意使用long long防止溢出 long long product = 1LL * a[i] * a[j]; // 关键剪枝:如果乘积已经超过总和,那么即使左右扩展再多的1(增加和),和也追不上积。 // 实际上,对于两个非1数,如果product > total_sum,那么product - fixed_sum > total_sum - fixed_sum, // 而l+r最大为 leftOnes[i] + rightOnes[j],这个值通常远小于total_sum。 // 这里为了逻辑清晰先保留计算,也可以直接判断 product > total_sum + leftOnes[i] + rightOnes[j] 时跳过。 // 我们先计算target,如果target太大,自然会在后续判断中过滤。 long long target = product - fixed_sum; if (target < 0) { continue; // 和已经大于积,即使不扩展1也不满足,扩展1(增加和)更不满足 } int L = leftOnes[i]; int R = rightOnes[j]; // 计算l的可行范围 long long l_min = max(0LL, target - R); long long l_max = min((long long)L, target); if (l_min <= l_max) { ans += (l_max - l_min + 1); } } cout << ans << endl; return 0; }

5. 边界情况、常见错误与调试心得

即使思路正确,实现时也极易掉入陷阱。下面分享几个我踩过的坑和调试经验。

5.1 数值溢出问题

这是本题最大的“坑点”。数组元素最大可达 10^9,两个这样的数相乘会达到 10^18,远超 32 位 int 的范围(约 2×10^9)。因此,任何涉及乘法或可能累加出大数的地方,都必须使用long long(64位整数)。

  • product = a[i] * a[j];这行代码,如果a[i]a[j]int,相乘的结果会先以int计算导致溢出,然后再赋值给long long变量,为时已晚。必须写成1LL * a[i] * a[j](long long)a[i] * a[j],确保计算在64位下进行。
  • target,l_min,l_max这些由乘积推导出的变量,也必须使用long long
  • 答案ans本身也可能超过int范围,因为子数组数量最多约为 n + n*(log(n))? 量级,对于 n=2e5,使用long long是安全的。

5.2 边界条件处理

  1. 没有非1元素或只有一个非1元素:我们的算法主体是遍历相邻的非1元素对。如果pos.size() < 2,那么这个循环不会执行,答案就是最初的ans = n。这是正确的,因为如果数组全是1,那么任何子数组的和等于长度,积等于1,只有长度为1的子数组满足条件。如果只有一个非1元素,根据3.1节的推导,长度大于1的子数组也不满足条件。
  2. target的计算与范围target = product - fixed_sum可能非常大,导致l_minl_max的计算出现负数或异常。我们通过max(0LL, target - R)min((long long)L, target)来约束,并最终判断l_min <= l_max来确保有效性。这是正确的。
  3. 连续1段的边界leftOnes[0]rightOnes[n-1]通过预处理循环被正确地初始化为0。

5.3 算法正确性验证:构造测试用例

要验证代码,需要构造多种类型的测试数据:

  • 纯1数组[1,1,1,1],答案应为4。
  • 无1数组[2,3,4],答案应为3(只有三个单元素)。
  • 单个非1元素[5,1,1,1],答案应为4(四个单元素)。
  • 两个非1元素紧邻[2,3]fixed_sum=5, product=6, target=1。L和R均为0。l_min = max(0,1-0)=1,l_max = min(0,1)=0l_min>l_max,无解。答案=2。正确,因为[2,3]的和是5,积是6。
  • 两个非1元素间有1[2,1,3]fixed_sum=2+1+3=6, product=6, target=0。L和R取决于上下文,假设左右无其他1,则L=0,R=0。l_min = max(0,0-0)=0,l_max = min(0,0)=0,有解,对应l=0,r=0,即子数组[2,1,3]本身。答案需要加上1。需要手动验证:和=6,积=6,确实满足。
  • 可向左右扩展1的情况:例如数组[1,1,2,1,3,1,1],以23为核心。i=2, j=4a[2]=2, a[4]=3midOnes=1fixed_sum=2+1+3=6product=6target=0L = leftOnes[2] = 2(左边两个1),R = rightOnes[4] = 2(右边两个1)。l_min = max(0, 0-2)=0l_max = min(2, 0)=0。所以只有l=0, r=0一组解,对应子数组[2,1,3]。但如果我们考虑[1,2,1,3]呢?它的和是1+2+1+3=7,积是6,不满足。[2,1,3,1]和也是7,积是6。可见确实只有核心部分满足。这个例子说明我们的计算是准确的。
  • 更复杂的扩展案例:需要构造target>0的情况。例如[1, 1, 4, 1, 1, 5, 1],以4和5为核心。fixed_sum = 4 + 2 + 5 = 11(中间两个1)。product=20target=9。左边L=2个1,右边R=1个1。我们需要l+r=9,且0<=l<=2,0<=r<=1。显然r最大为1,则l至少为8,超出了L的范围,无解。说明没有这样的子数组。可以尝试调整数值,让target小一些。

5.4 性能分析

  • 时间复杂度:预处理连续1数组 O(n)。遍历非1元素索引 O(n)。遍历相邻非1元素对 O(m),其中m是非1元素的个数。每个元素对的计算是O(1)。整体复杂度 O(n),非常高效。
  • 空间复杂度:使用了leftOnes,rightOnes,pos等额外数组,均为 O(n)。

这道“和与乘积”的题目,从暴力枚举的 O(n²) 到最终 O(n) 的解法,跨越的关键在于对问题性质的深度挖掘。它要求我们跳出“枚举所有子数组”的惯性思维,通过数学分析将搜索空间缩小到只关注包含1的、且非1元素个数不超过2的特定子数组上,并利用预处理和组合数学公式进行快速计数。在竞赛中,能够迅速完成这种问题转化,是区分普通选手和顶尖选手的重要标志。解决这类问题,没有捷径,唯有多思考、多总结,理解每一个优化步骤背后的“为什么”,才能举一反三。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/22 17:15:26

三步把 NCM 转成 MP3:ncmdump 免费本地无损转换教程

三步把 NCM 转成 MP3&#xff1a;ncmdump 免费本地无损转换教程 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 车载 U 盘里塞满网易云下载的 .ncm 文件&#xff0c;插上车却一个都放不了&#xff1f;问题不在歌&#xff0c;在格式。…

作者头像 李华
网站建设 2026/8/22 17:14:57

智能汽车软件安全设计1

基于失效-安全的系统安全设计说明与示例 基于SoC芯片的系统无法直接满足要求&#xff0c;需要在架构方案上进行功能安全考虑和设计。基于功能安全母标准IEC61508的定义&#xff0c;功能安全领域常用的安全架构有一取一&#xff08;1 out of 1&#xff0c;1oo1&#xff09;架构、…

作者头像 李华
网站建设 2026/8/22 17:14:40

病媒防控新利器:蚊子种类检测数据集(含YOLOv11n实战)

病媒防控新利器&#xff1a;蚊子种类检测数据集&#xff08;含YOLOv11n实战&#xff09; 蚊虫是疟疾、登革热、寨卡病毒等多种传染病的重要传播媒介。传统蚊虫种类鉴定依赖人工形态学观察&#xff0c;效率低且对专业人员要求高。今天介绍的蚊子种类检测数据集&#xff0c;结合深…

作者头像 李华
网站建设 2026/8/22 17:14:38

2026 LLM大模型权威排行榜评测网站指南(持续更新)

面对不同的大模型排行榜&#xff0c;真正困难的不是找到一个名次&#xff0c;而是判断这个名次反映的是用户偏好、标准化测试、软件工程能力&#xff0c;还是价格与速度. 如果只想快速了解当前大模型的综合表现&#xff0c;优先查看 Arena 和 Artificial Analysis&#xff1a;前…

作者头像 李华
网站建设 2026/8/22 17:14:34

【AI接入大模型SDK】云端接入和本地部署模型的区别

&#x1f3ac; 个人主页&#xff1a;艾莉丝努力练剑❄专栏传送门&#xff1a;《C语言》《数据结构与算法》《C/C干货分享&学习过程记录》 《Linux操作系统编程详解》《笔试/面试常见算法&#xff1a;从基础到进阶》《Python干货分享》⭐️为天地立心&#xff0c;为生民立命…

作者头像 李华