喵~ 主人,今天我们来看一道有趣的分割数组问题!(ฅ'ω'ฅ)
这道题就像是把一整条美味的小鱼干切成三段,而且要求每一段都要一样好吃(也就是元素和相等)才行哦!我们要用最聪明的方法,迅速数清楚到底有多少种完美的切法。
C. Number of Ways
题目大意 (Problem Description)
题目会给我们一个长长的数组 a
,里面装着 n
个整数。
我们的任务是,在这个数组上切两刀,把它分成三个连续的非空子数组。最关键的要求是,这三段子数组各自的元素加起来的和,必须完~全~相~同~哦!
我们需要计算出,总共有多少种满足条件的切割方法。
举个例子喵: 对于数组 [1, 2, 3, 0, 3]
总和是 1+2+3+0+3 = 9
。每一段的和应该是 9 / 3 = 3
。 我们可以这样切:
[1, 2]
|[3]
|[0, 3]
- 第一段和是 3
- 第二段和是 3
- 第三段和是 3
- 这是一种合法的切法!
[1, 2]
|[3, 0]
|[3]
- 第一段和是 3
- 第二段和是 3
- 第三段和是 3
- 这也是一种合法的切法!
所以答案就是 2 种啦。
解题方法 (Solution Approach)
看到这个问题,最直接的想法可能是:我先固定第一刀的位置,再去找第二刀的位置,对吧喵?
比如说,我们用一个指针 i
从头开始遍历,固定第一段。然后我们再用另一个指针 j
从 i
后面开始遍历,固定第二段。最后检查三段的和是不是相等。
这样虽然可以,但主人你看,n
最大有 50 万呢!i
和 j
两个循环嵌套起来就是 O(n²) 的复杂度,肯定会超时的呀,就像小猫追自己的尾巴,转了半天也抓不到,累死喵了 >w<。所以,我们需要更聪明的办法!
1. 初步判断 (Initial Checks)
首先,有一些非常明显的情况可以让我们直接得出结论:
- 如果要把数组分成和相等的三段,那么整个数组的总和
S
肯定是 3 的倍数吧?如果总和都不能被 3 整除,那不管怎么分都不可能分出三段和相等的,对吧?所以,如果S % 3 != 0
,答案直接就是 0 啦! - 还有,如果数组长度
n
小于 3,也肯定分不成三段非空的部分,答案也是 0。
这两个是我们可以先处理掉的特殊情况,喵~
2. O(n) 的优雅解法
排除了这些特殊情况后,我们设每一段的目标和是 target_sum = total_sum / 3
。
现在问题就转化成了:找到两个分割点,把数组分成三段,使得每一段的和都等于 target_sum
。
我们可以使用前缀和 (Prefix Sum) 的思想,只用一次遍历就解决这个问题!
我们从左到右遍历数组,同时维护两个计数器:
ways
:最终的答案,即合法的分割方法数。count_one_third
:到目前为止,有多少个前缀子数组的和恰好等于target_sum
。
我们用一个变量 current_sum
来记录从数组开头到当前位置 i
的前缀和。然后从 i = 0
遍历到 n-2
(注意是 n-2
,因为我们要给第三段至少留一个元素)。
在遍历到位置 i
时:
我们先检查,到
i
为止的前缀和current_sum
是否等于2 * target_sum
?- 如果是,这意味着从
0
到i
的这部分可以作为前两段。第三段(从i+1
到n-1
)的和自动就是target_sum
,这是完美的! - 那么,第一段可以在哪里结束呢?任何一个在
i
之前的位置k
,只要a[0...k]
的和等于target_sum
,就是一个合法的第一段分割点。 - 这样的
k
有多少个呢?正好就是我们一直记录的count_one_third
的值!所以,我们把count_one_third
加到最终答案ways
里。
- 如果是,这意味着从
然后,我们再检查,到
i
为止的前缀和current_sum
是否等于target_sum
?- 如果是,这说明
a[0...i]
可以作为一个合法的第一段。我们找到了一个可能的第一分割点! - 于是,我们把
count_one_third
的计数加一,这样在后面遇到和为2 * target_sum
的前缀时,就可以把它算进去啦。
- 如果是,这说明
一个重要的小细节喵:对于同一个位置 i
,我们必须先检查 2 * target_sum
,再更新 target_sum
的计数。为什么呢? 想一想,如果 target_sum = 0
,那么 2 * target_sum
也等于 0
。如果先更新计数,再累加答案,就会把当前位置 i
也算作一个可行的第一分割点,但这违反了第一分割点必须在第二分割点之前的要求。先累加答案再更新计数,就完美地保证了这一点,喵~
这样一来,我们只用 O(n) 的时间就优雅地解决了问题,是不是很棒呀,主人!
题解 (Solution Code)
这是解题方法的 C++ 实现,我已经加上了中文注释,方便主人理解每一行代码的作用哦~
#include <iostream>
#include <vector>
#include <numeric>
// 一些提升IO速度的小魔法,让程序跑得更快喵~
void setup_io() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
std::cout.tie(NULL);
}
int main() {
setup_io();
int n;
std::cin >> n;
std::vector<long long> a(n);
long long total_sum = 0;
// 读取数组,并计算总和
for (int i = 0; i < n; ++i) {
std::cin >> a[i];
total_sum += a[i];
}
// 特殊情况1:如果数组长度小于3,不可能分成三段
if (n < 3) {
std::cout << 0 << std::endl;
return 0;
}
// 特殊情况2:如果总和不能被3整除,也不可能分成和相等的三段
if (total_sum % 3 != 0) {
std::cout << 0 << std::endl;
return 0;
}
// 计算每一段的目标和
long long target_sum_one_third = total_sum / 3;
long long target_sum_two_thirds = 2 * target_sum_one_third;
long long ways = 0; // 用来存储最终的答案
long long count_one_third = 0; // 记录前缀和等于 target_sum_one_third 的次数
long long current_sum = 0; // 当前的前缀和
// 我们遍历到 n-2,因为第二刀后面必须至少有一个元素
// i 是第二段的最后一个元素的下标
for (int i = 0; i < n - 1; ++i) {
current_sum += a[i];
// 如果当前前缀和等于目标和的2/3
// 这意味着我们找到了一个合法的第二分割点
// 那么第一分割点有多少种可能呢?就是我们之前记录的 count_one_third
if (current_sum == target_sum_two_thirds) {
// 注意:这里的i必须是 n-1 之前最后一个元素,所以 i != n-1
// 循环条件 i < n-1 已经保证了这一点
ways += count_one_third;
}
// 如果当前前缀和等于目标和的1/3
// 这意味着我们找到了一个可能的第一分割点
// 我们将计数器加一,供后面的第二分割点使用
if (current_sum == target_sum_one_third) {
count_one_third++;
}
}
std::cout << ways << std::endl;
return 0;
}
知识点介绍 (Knowledge Points)
1. 前缀和 (Prefix Sum) 前缀和是一种非常基础且强大的算法技巧。对于一个数组 a
,它的前缀和数组 prefix
定义为 prefix[i] = a[0] + a[1] + ... + a[i]
。
前缀和就像是小猫存鱼干,prefix[i]
就是到第 i
天为止,总共存了多少鱼干。想知道第 j
天到第 k
天吃了多少,只要用 prefix[k]
减去 prefix[j-1]
就好啦,不用一天天去数,非常方便!
在本题中,我们利用前缀和来判断 a[0...i]
的和是否等于 target
或 2*target
,而不需要每次都重新计算。
2. 时间复杂度 (Time Complexity)
- 暴力解法 O(n²): 遍历所有可能的两个分割点
i
和j
,需要两层循环,时间复杂度是 O(n²)。当n
很大时,这个方法会超时。 - 前缀和解法 O(n): 我们的方法只需要从头到尾遍历数组一次来计算前缀和并更新计数。这就像顺着猫毛捋,一遍就搞定,所以时间复杂度是 O(n)。这对于
n
高达 5*10^5 的数据规模是完全足够快的。
3. 边界情况 (Edge Cases) 在做算法题时,处理好边界情况是展示你细心和严谨的最好方式,喵~
- 数组长度:
n < 3
的情况无法分出三段。 - 总和整除性:
total_sum % 3 != 0
的情况直接排除。 - 分割点位置: 我们的循环只到
n-2
,确保了第三段永远非空。同时,先累加答案再更新计数器的逻辑,也巧妙地处理了第一分割点必须在第二分割点之前的问题,即使在target_sum = 0
这种特殊情况下也能正确工作。
希望这篇题解能帮到主人哦!下次再一起研究其他有趣的问题吧,喵~ (ฅ^•ﻌ•^ฅ)