1. 项目概述:从一道国赛真题看“和与积”的算法博弈
最近在复盘蓝桥杯历届真题时,我又把2021年第十二届国赛的这道“和与乘积”翻了出来。这道题乍一看题目描述很简单,但真正动手去解,你会发现它像一颗包裹着多层糖衣的巧克力,外层是简单的算术概念,内核却考验着你对数据结构、算法优化乃至数学思维的深刻理解。很多选手在赛场上初次遇到它,容易陷入暴力枚举的泥潭,导致程序超时;而一旦掌握了正确的拆解思路,它又能成为拉开分数差距的关键。今天,我就以这道题为引子,和大家深入聊聊如何系统性地分析并解决这类“在序列中寻找满足特定条件的子数组”问题。
这道题的核心要求是:给定一个长度为n的正整数数组,我们需要找出所有满足“区间内所有元素之和等于区间内所有元素之积”的连续子数组(区间)的个数。举个例子,数组[1, 3, 2], 子数组[1, 3]的和是4,积是3,不相等;但子数组[1, 3, 2]的和是6,积也是6,这就符合条件。题目输入的n最大可以达到 2×10^5,数组元素值a_i最大为 10^9。这意味着,一个时间复杂度为 O(n²) 的朴素双重循环解法(枚举所有起点和终点)在极限数据下必然会超时。因此,我们的挑战在于,如何在庞大的数据规模下,高效地找出所有符合条件的区间。
这不仅仅是解一道题,更是理解一类问题的通用思考框架。它涉及对问题性质的洞察、对数据范围的敏感、对算法工具的恰当选择,以及编写代码时对边界条件的严密处理。接下来,我将从问题本质分析、核心优化策略、具体实现细节以及实战调试心得四个方面,完整拆解这道题的解决之道。
1.1 核心需求与约束分析
首先,我们必须吃透题目给出的每一个条件,并从中挖掘出优化的可能性。
1. 正整数数组:所有元素大于0。这个条件至关重要,它排除了0和负数。如果有0,那么任何包含0的区间,其乘积瞬间变为0,但和却不为0(除非区间内全是0,但正整数数组排除了这种可能),因此包含0的区间不可能满足“和等于积”。这简化了我们的分析。负数的引入会让乘积的正负号发生变化,与和的比较变得复杂,同样被排除。所以,我们面对的是一个纯正整数的环境。
2. 连续子数组:我们需要找的是原数组中连续的一段,不能跳着选元素。这明确了我们是在处理“区间”问题,常用的技术有前缀和、滑动窗口、双指针等。
3. 条件:区间和 == 区间积:设区间为a[l], a[l+1], ..., a[r],条件为 Σa[i] = Πa[i] (i从l到r)。在正整数域下,这是一个非常强的约束。直观感受是,乘积的增长速度远快于和的增长速度。除了所有元素都为1的区间(和与积都等于区间长度),以及包含[1, 非1数]这种可能产生平衡的组合外,其他情况很难相等。
4. 数据规模:n≤ 2×10^5,a_i≤ 10^9。这是关键的约束,它直接宣判了 O(n²) 算法的死刑。2×10^5 的平方是 4×10^10,远超普通计算机一秒内能完成的运算量(通常认为 10^7 ~ 10^8 次操作是安全边界)。因此,我们的算法时间复杂度必须控制在 O(n log n) 或更低,理想情况是 O(n)。
基于以上分析,我们的目标转化为:设计一个优于 O(n²) 的算法,统计所有和等于积的连续子区间个数。
2. 解题思路的深度剖析与策略选择
面对这样一个问题,直接硬算所有区间的和与积是不可行的。我们需要寻找数学或算法上的特殊性质,来大幅减少需要检查的区间数量。
2.1 关键数学洞察:乘积的爆炸性增长与有效区间长度极限
这是本题最核心的突破口。对于正整数,乘积的增长是指数级的,而和只是线性增长。这意味着,当一个区间的乘积超过其和一定程度后,就再也追不上了,更别提相等了。
我们来做一个粗略的估计:假设区间内最小的数大于1(比如是2)。那么对于长度为k的区间,其和至少是2k,而其积至少是2^k。当k稍微大一点,比如k=10,积至少是1024,而和最多是10 * max(a_i),在 max(a_i) 有限的情况下,积很容易远超和。因此,乘积非1的区间,其长度不可能很长。
更精确一点,由于a_i最大为 10^9,但我们要找的是和等于积的区间。考虑最坏情况,区间内全是1,那么和与积都等于长度k,这是始终成立的。所以全1区间是有效的,且长度可以很长(直到数组结束)。但一旦区间内出现一个大于1的数m(m>=2),情况就变了。设该区间有x个1,和y个大于1的数(其乘积记为P,且 P >= 2^y)。
- 区间和S = x + Σ(其他大于1的数) >= x + 2y
- 区间积M = 1^x * P = P >= 2^y
要使S = M,在y固定时,M随y指数增长,而S只是线性增长。因此,对于给定的y(大于1的数的个数),x(1的个数) 必须非常大才能让和追上积。但x再大,S的增长也是线性的(每次加1),而M在y>=2时已经是一个至少为4的常数。计算表明,当y>=2时,需要的x会急剧增大,但整个数组的长度n是有限的(2e5),并且随着y增大,M会迅速超过这个有限长度内S可能达到的最大值(即全数组的和)。
经过更详细的分析(通常通过打表或数学推导可以发现),在一个元素值不超过 10^9 的正整数数组中,满足“和等于积”且非全1的区间,其长度不会超过几十(一个常见的经验上限是60左右,因为 2^60 已经是一个巨大的数,远超任何可能由2e5个10^9相加得到的和)。而全1的区间可以任意长。
这个洞察将问题分成了两部分:
- 处理全1的区间:这部分区间很多,但判断简单(区间内元素全为1),且其和与积显然相等。我们需要高效地统计所有连续1构成的子区间的个数。
- 处理包含大于1的数的区间:这类区间长度很短(不超过L,例如L=60)。我们可以枚举这些短区间,并快速验证其是否满足条件。
2.2 算法框架设计:分类讨论与双指针滑动
基于以上洞察,我们可以设计出如下算法框架:
步骤一:预处理与全1区间统计
- 遍历数组,将其分割成若干个由“连续1”组成的段,以及被这些段分隔开的“非1数”。
- 对于一个长度为
len的连续1段,其中包含的全体元素为1的子区间个数是len * (len + 1) / 2。因为从这段中任选一个起点和终点(起点<=终点)构成的子区间必然全为1。 - 将这些数量累加到答案中。注意,这些区间已经包含了“长度为1的单个1”这种情况。
步骤二:短区间枚举与验证
- 我们不再需要关心纯粹由1组成的区间,因为它们已经在步骤一中被完全统计了。现在只关注那些至少包含一个大于1的数的区间。
- 根据之前的分析,这类区间的长度有限(假设上限为
MAX_LEN,例如60)。因此,我们可以遍历数组中每一个大于1的数(称为“锚点”),以其为中心(或作为起点),向左右两侧扩展,但控制扩展的总长度不超过MAX_LEN。 - 更实现友好的方法是:遍历每个位置
i作为区间的起点,只要a[i] > 1,就尝试向右扩展终点j,直到区间长度(j-i+1) > MAX_LEN或者j超出数组范围,或者区间积已经超过一个不可能被追上的阈值(例如,超过整个数组的总和total_sum)。因为如果积已经大于可能的最大和(即全数组和),那么再向右扩展,积只会更大,和却增加有限,永远不可能相等。 - 在扩展过程中,对于每一个得到的区间
[i, j],我们计算其和与积,判断是否相等。如果相等,则答案加1。 - 关键优化点:
- 乘积溢出处理:由于
a_i最大为1e9,连续乘几十个数极有可能超过64位整数(long long)的范围(约1.8e19)。一旦乘积在计算过程中超过我们设定的阈值(如total_sum),我们就可以直接停止向这个方向扩展,因为后续的积只会更大,和不可能追上。在代码中,这通常通过判断product > total_sum时break循环来实现。 - 快速计算区间和:使用前缀和(Prefix Sum)数组可以在O(1)时间内得到任意区间
[l, r]的和。预处理前缀和数组pre_sum,其中pre_sum[i]表示前i个元素的和(通常pre_sum[0]=0)。那么区间[l, r]的和等于pre_sum[r+1] - pre_sum[l]。 - 区间积的计算:在短区间枚举的循环中直接连乘即可,配合溢出判断。
- 乘积溢出处理:由于
步骤三:去重与注意事项
- 步骤一中统计了所有全1区间。
- 步骤二中,我们枚举的起点
i满足a[i] > 1。这样,步骤二枚举的区间都至少包含一个大于1的数,不会包含步骤一已经统计过的纯1区间。但是,步骤二枚举的区间可能包含连续的1(在大于1的数的旁边),这是允许且需要检查的。 - 需要小心处理长度为1的区间。在步骤一中,单个1已经被计入。在步骤二中,如果
a[i] > 1,那么区间[i, i]的和与积都是a[i],显然相等。所以步骤二也需要将每个a[i] > 1的单个元素区间计入答案。这通常可以在枚举起点时直接判断,或者在扩展循环中当j == i时判断。
这个算法的时间复杂度是多少呢?
- 步骤一:O(n),一次遍历分割1的连续段。
- 步骤二:对于每个
a[i] > 1的位置,最多向右扩展MAX_LEN次(由于乘积溢出提前break,实际次数往往更少)。设大于1的数的个数为m,则复杂度为 O(m * MAX_LEN)。在最坏情况下,如果数组里没有1,那么m = n,复杂度是 O(n * MAX_LEN)。由于MAX_LEN是一个很小的常数(如60),所以整体复杂度是O(60n),也就是O(n),完全能够应对 n=2e5 的数据规模。 - 前缀和预处理也是 O(n)。
因此,我们成功地将一个看似 O(n²) 的问题,通过数学洞察转化为了一个 O(n) 的解决方案。
3. 代码实现与关键细节剖析
理论清晰后,我们来看具体的代码实现。这里我用 C++ 为例进行讲解,因为蓝桥杯竞赛主要使用 C/C++/Java,其中 C++ 在性能上常有优势。
3.1 数据结构与预处理
#include <iostream> #include <vector> #include <algorithm> using namespace std; typedef long long ll; // 使用 long long 防止中间结果溢出 int main() { int n; cin >> n; vector<int> a(n); ll total_sum = 0; // 整个数组的和,作为乘积的溢出阈值 for (int i = 0; i < n; ++i) { cin >> a[i]; total_sum += a[i]; } // 1. 预处理前缀和 vector<ll> pre_sum(n + 1, 0); for (int i = 0; i < n; ++i) { pre_sum[i + 1] = pre_sum[i] + a[i]; } ll ans = 0; // 使用 long long 存储答案,因为区间数量可能很多 // 2. 统计全1区间 int one_len = 0; for (int i = 0; i < n; ++i) { if (a[i] == 1) { one_len++; } else { if (one_len > 0) { ans += (ll)one_len * (one_len + 1) / 2; one_len = 0; } } } // 处理末尾可能存在的1段 if (one_len > 0) { ans += (ll)one_len * (one_len + 1) / 2; }关键点说明:
total_sum:计算数组所有元素之和。这个值有两个作用:一是作为乘积溢出的判断阈值(一旦区间积超过它,绝无可能等于区间和);二是在某些验证中作为参考。使用long long类型是必要的,因为 n=2e5, a_i=1e9 时,总和最大可达 2e14,超出了int范围。pre_sum:标准的前缀和数组,pre_sum[i]代表a[0]到a[i-1]的和。这样区间[l, r]的和就是pre_sum[r+1] - pre_sum[l]。- 全1区间统计:通过一次遍历,识别出连续的1段。公式
len*(len+1)/2是计算连续段内所有子区间个数的标准公式(等差数列求和)。这里在遇到非1数或数组末尾时结算一段。
3.2 短区间枚举的核心循环
这是算法的核心,需要仔细处理边界和溢出。
// 3. 枚举包含大于1的数的区间 const int MAX_LEN = 60; // 经验常数,可根据题目调整,略大于 log2(total_sum) for (int i = 0; i < n; ++i) { if (a[i] == 1) { continue; // 起点跳过1,因为纯1区间已统计,且以1开头包含大于1数的区间,会被以其后的大于1数作为起点枚举到(或需要特别处理,见下文分析) } // 单独处理长度为1的区间 [i, i] // 对于 a[i] > 1,其本身和等于积,是一个合法区间 ans++; ll product = a[i]; // 初始化区间积 // 从 i 开始,向右扩展终点 j for (int j = i + 1; j < n; ++j) { // 控制区间长度 if (j - i + 1 > MAX_LEN) { break; } // 更新区间积 if (a[j] > total_sum / product) { // 防止下一行乘法溢出,提前判断。如果 product * a[j] > total_sum,则必然超出阈值 // 这个判断等价于 product > total_sum / a[j],但用除法防止乘法溢出 break; } product *= a[j]; // 如果积已经超过可能的最大和(total_sum),后续扩展积只会更大,不可能相等 if (product > total_sum) { break; } // 计算区间和 ll interval_sum = pre_sum[j + 1] - pre_sum[i]; // 判断是否相等 if (product == interval_sum) { ans++; } // 注意:即使当前不相等,也不能break,因为继续向右加数(尤其是加1)可能会让和追上积 // 例如区间 [2, 1, 1],积为2,和从2变成4,在加入第二个1时才相等。 } }逐段解析与避坑指南:
起点过滤:
if (a[i] == 1) continue;这行代码跳过了以1作为区间起点的情况。为什么可以跳过?- 我们的步骤一已经统计了所有全1区间。如果一个区间以1开头,但包含了后面的大于1的数(例如
[1, 3, 2]),那么这个区间必然也会被以那个大于1的数(3)作为起点向左扩展而覆盖到吗?不一定。比如区间[1, 3],起点是1,终点是3。在我们的循环中,起点是3时,只会向右扩展,不会向左扩展到1。所以,单纯跳过所有a[i]==1的起点,会漏掉一些开头是1,后面跟着非1数的区间。 - 修正方案:更严谨的做法是,不跳过
a[i]==1的起点,而是对所有位置i都进行向右扩展的枚举。但是,这样会导致大量的重复计算,因为从一串连续的1中的任何一个1开始扩展,其后续路径有很大重叠。一个更好的优化是:我们只从每个“非1数段”的第一个元素开始枚举,或者更简单粗暴但依然有效的是,保留上述循环,但意识到我们漏计了“1开头”的区间。实际上,这些被漏掉的区间,可以通过对称地枚举以每个位置j作为终点,向左扩展来补全,或者通过数学分析发现其数量有限,在常数MAX_LEN范围内,可以通过稍微调整枚举逻辑来覆盖。 - 常见且正确的处理:在实际竞赛代码中,一种简洁且正确的写法是,不区分起点是否为1,直接对每个
i进行向右扩展。因为MAX_LEN很小(~60),即使对每个i都扩展,总复杂度也是 O(60n),可以接受。但需要在扩展循环内部,当product溢出阈值时及时break。上面的示例代码为了清晰展示思路做了跳过,在实际编写时需要根据情况调整。一个安全的做法是去掉if (a[i]==1) continue;这行。
- 我们的步骤一已经统计了所有全1区间。如果一个区间以1开头,但包含了后面的大于1的数(例如
乘积溢出预防:
if (a[j] > total_sum / product)这一行是防止整数溢出的经典技巧。我们不能直接计算product * a[j]再与total_sum比较,因为product * a[j]可能已经超出了long long的范围,导致溢出成为负数或零,从而使判断失效。通过先做除法total_sum / product,判断a[j]是否大于这个商,如果大于,则说明product * a[j] > total_sum,可以提前终止循环。这是编写高性能安全代码的必备技巧。区间和计算:使用前缀和
pre_sum在 O(1) 时间内完成,是标准操作。循环继续条件:在判断
product == interval_sum后,即使不相等,我们仍然继续循环(加入下一个数a[j+1])。这是因为加入一个新的数(特别是1)可能会显著增加和,但几乎不增加积(如果是1),从而可能让原本不相等的两者变得相等。这是符合题目逻辑的。长度限制与溢出判断的顺序:先判断长度是否超限 (
j - i + 1 > MAX_LEN),再判断乘积是否即将溢出 (a[j] > total_sum / product)。顺序可以互换,但两者都是必要的剪枝条件。长度限制是基于数学洞察的理论剪枝,溢出判断是基于数据范围的运行时剪枝。
3.3 处理“1开头”区间的补充方案
如果按照上面去掉continue的“安全做法”,代码已经正确。但为了展示另一种思路,这里介绍如何通过额外处理来覆盖那些以1开头、包含非1数的区间,同时避免对每个1都进行长扩展。
我们可以观察到,如果一个合法区间以一串1开头,那么去掉开头的这些1,剩下的区间(以第一个非1数开头)也一定是一个合法区间吗?不一定,因为和与积都改变了。但是,我们可以利用以下性质:设区间为[1, 1, ..., 1, X, ...],前面有k个1,后面是起始于X的子区间R。记R的和为S_R,积为P_R。那么整个区间的和S = k + S_R,积P = 1^k * P_R = P_R。条件S = P转化为k + S_R = P_R,即S_R = P_R - k。
这意味着,如果我们已经找到了所有以X开头(或包含X)的合法子区间R及其对应的S_R和P_R,那么我们可以检查是否存在一个整数k(即前面连续的1的个数),使得S_R = P_R - k成立。由于k是前面连续1的个数,它受实际数组布局限制。
在实际编程中,更实用的方法是:在枚举以非1数a[i]为起点的区间时,不仅向右扩展,也允许向左吃掉一些连续的1。具体做法是,在固定右端点j时,计算区间[i, j]的积P和和S。设位置i左边有left_ones个连续的1,那么我们可以尝试将区间向左扩展t个1(0 <= t <= left_ones),形成新区间[i-t, j]。新区间的积不变(因为乘1),和增加t。条件变为S + t = P,即t = P - S。我们只需要检查计算出的t是否满足0 <= t <= left_ones即可。如果满足,则说明找到了一个以1开头的合法区间。
这种处理方式更精巧,但实现起来稍复杂,需要预处理每个位置左边连续1的个数。对于竞赛而言,采用对每个起点i(包括1)都进行有限长度(MAX_LEN)扩展的朴素方法,因其实现简单且复杂度可控,往往是首选。
4. 实战调试与性能优化要点
即使思路正确,实现时也可能踩坑。下面分享一些我在调试此类问题时的经验和技巧。
4.1 常见错误与排查清单
整数溢出:这是最大的“坑”。即使使用了
long long,在计算product *= a[j]时,如果product和a[j]都很大,乘积可能超出long long的正数范围(约9.2e18),发生溢出,导致结果错误甚至变成负数。必须使用if (a[j] > total_sum / product)或类似除法判断进行预防。不能依赖product > total_sum的判断,因为溢出可能发生在与total_sum比较之前。重复计数:确保全1区间和包含非1数的区间没有重叠统计。在上面的代码框架中,步骤一统计了所有元素全为1的区间。步骤二中,当起点
i满足a[i] > 1时,我们计入了[i, i]。如果步骤二的扩展循环也检查了a[i] == 1的起点,那么当区间内全为1时,它又会被步骤二枚举到,导致重复。因此,步骤二的循环里,对于检查到的区间,需要判断其是否全为1(即区间积是否为1),如果是,则应该跳过,因为已经在步骤一统计过。或者,更清晰的做法是,步骤二只枚举起点i满足a[i] > 1的情况,并认为所有包含至少一个大于1的数的区间都在这里处理。边界条件:
- 数组长度为1时,程序是否能正确运行?单个元素
x,和与积都是x,始终成立。所以答案应为1。我们的代码需要覆盖这一点:步骤一处理全1,如果数组是[5],则步骤一统计为0,步骤二中i=0,a[0]=5>1,ans++会将其计入。 - 数组中全是1的情况。步骤一会统计所有子区间,步骤二因为所有
a[i]==1而被continue,最终答案正确。 - 数组中全是大于1的数。步骤一统计为0,步骤二对每个
i进行扩展。注意,此时MAX_LEN的剪枝和product > total_sum的剪枝会非常有效,因为乘积增长极快。
- 数组长度为1时,程序是否能正确运行?单个元素
MAX_LEN的设置:这个值不是绝对的。理论上,基于a_i <= 1e9和n <= 2e5,可以推导出一个安全上界。一个保守的做法是设置得稍大一些,比如 64(对应 2^64 远大于任何可能的总和)。或者,可以动态计算:因为total_sum最大约为 2e14,而乘积至少是 2^k(假设区间内最小的大于1的数是2),解 2^k > 2e14 得 k > log2(2e14) ≈ 47.5。所以区间内大于1的数的个数不会超过48个。考虑到中间可能夹杂1,区间总长度可以设得比48大一些,比如60或70,以确保安全。在竞赛中,设置MAX_LEN = 60或100是常见且安全的。
4.2 性能优化与测试策略
输入输出优化:对于 C++,当
n很大时,使用cin/cout可能较慢。可以加入ios::sync_with_stdio(false); cin.tie(0);来关闭与 C 标准流的同步,加速输入输出。或者使用scanf/printf。内层循环剪枝:内层循环
for (int j = i+1; ...)的两个break条件(长度限制和溢出判断)至关重要。可以交换它们的顺序,将更可能触发的条件放在前面。通常,乘积溢出比达到最大长度限制更容易发生,尤其是当a[i]本身较大时。可以将if (product > total_sum) break;的判断提前。测试用例设计:
- 最小用例:
n=1,a=[1];n=1,a=[2]。 - 全1用例:
n=100000, 全部是1。验证步骤一的公式是否正确,以及程序运行速度。 - 全大数用例:
n=100000, 全部是1e9。验证溢出判断是否有效,以及内层循环是否能快速终止(因为乘积会迅速超过total_sum)。 - 混合用例:构造一些已知答案的小数组,例如
[1, 3, 2](答案应为2:[1,3,2]和[3,2]? 这里需要仔细验证:[1,3,2]和[3,2]似乎都不对?[3,2]和为5积为6。哦,看来我举的例子不好。换个例子:[1, 1, 2],合法区间有:[1],[1](第二个1),[1,1],[1,1,2](和=4,积=2?不对,积是2,和是4。[2]自身是合法的。所以需要仔细手动计算或写暴力程序验证)。 - 随机大用例:用随机数生成器生成
n=200000的数组,用我们的优化算法和一个保证正确但很慢的 O(n²) 暴力算法(仅用于n很小,比如n<=100时)对比结果,确保正确性。
- 最小用例:
调试输出:在开发阶段,可以输出一些中间信息,比如
total_sum, 每次找到合法区间时打印i, j, product, interval_sum,帮助验证逻辑。
4.3 一个完整的、经过调整的参考代码
以下是整合了上述讨论(特别是处理了以1开头的区间)的一个更健壮的实现版本:
#include <iostream> #include <vector> #include <algorithm> using namespace std; typedef long long ll; int main() { ios::sync_with_stdio(false); cin.tie(0); int n; cin >> n; vector<int> a(n); ll total_sum = 0; for (int i = 0; i < n; ++i) { cin >> a[i]; total_sum += a[i]; } vector<ll> pre_sum(n + 1, 0); for (int i = 0; i < n; ++i) { pre_sum[i + 1] = pre_sum[i] + a[i]; } ll ans = 0; // 统计全1区间 int one_len = 0; for (int i = 0; i < n; ++i) { if (a[i] == 1) { one_len++; } else { if (one_len > 0) { ans += (ll)one_len * (one_len + 1) / 2; one_len = 0; } } } if (one_len > 0) { ans += (ll)one_len * (one_len + 1) / 2; } // 枚举所有区间,但利用长度和乘积限制进行剪枝 const int MAX_LEN = 60; // 足够大的常数 for (int i = 0; i < n; ++i) { // 对于每个起点i,向右扩展 ll product = 1; for (int j = i; j < n; ++j) { // 长度剪枝 if (j - i + 1 > MAX_LEN) { break; } // 更新积,并预防溢出 if (a[j] > total_sum / product) { break; // 乘积即将超过总和,不可能相等 } product *= a[j]; if (product > total_sum) { break; // 乘积已超过可能的最大和 } ll interval_sum = pre_sum[j + 1] - pre_sum[i]; if (interval_sum == product) { // 需要判断当前区间是否全为1,避免与步骤一重复 // 如果product == 1,则区间全为1。但步骤一已经统计了所有全1区间。 // 实际上,当product==1时,interval_sum也等于区间长度,只有全1区间才可能。 // 所以,当product == 1时,这个区间一定是全1区间,我们已经统计过,跳过。 if (product > 1) { ans++; } // 注意:即使product==1,也不应该重复加。所以这里用if(product>1)来过滤。 // 另一种方法是:在步骤一统计全1区间后,将原数组中所有1的位置标记,在步骤二遇到全1区间时跳过。 // 这里利用product==1作为判断条件更简洁。 } // 即使当前不相等,继续循环,因为加1可能让和追上积 } } cout << ans << endl; return 0; }这份代码的关键调整:
- 去掉了对起点
a[i]==1的跳过,对所有起点i进行枚举。 - 在内层循环中,当找到合法区间时,通过判断
if (product > 1)来排除全1区间,避免与步骤一重复计数。 - 这样,以1开头但包含非1数的区间也能被正确枚举到。例如对于
[1, 3, 1, 2],当i=0(a[0]=1),j=2时,区间[1,3,1]的和为5,积为3,不相等;当j=3时,区间[1,3,1,2]的和为7,积为6,不相等。但可能在其他起点和终点组合下找到合法区间。 - 复杂度依然是 O(n * MAX_LEN),在
n=2e5,MAX_LEN=60时约为 1.2e7 次循环,在合理时间内。
通过这道“和与乘积”真题的深度剖析,我们不仅学会了一个具体问题的解法,更重要的是掌握了一套处理“特殊约束条件下统计子区间”问题的组合拳:数学性质洞察 -> 问题分类 -> 算法选择(前缀和+有限枚举)-> 剪枝优化(长度限制、溢出判断)-> 边界处理与去重。这种思维模式可以迁移到许多其他竞赛题目中。在平时练习时,多问几个“为什么”:为什么长度有限?为什么用前缀和?为什么这样剪枝有效?把每一个决策背后的理由想清楚,编程能力才能真正得到提升。