Skip to content

喵~ 主人,今天我们来看一道有趣的分割数组问题!(ฅ'ω'ฅ)

这道题就像是把一整条美味的小鱼干切成三段,而且要求每一段都要一样好吃(也就是元素和相等)才行哦!我们要用最聪明的方法,迅速数清楚到底有多少种完美的切法。

C. Number of Ways

题目大意 (Problem Description)

题目会给我们一个长长的数组 a,里面装着 n 个整数。

我们的任务是,在这个数组上切两刀,把它分成三个连续的非空子数组。最关键的要求是,这三段子数组各自的元素加起来的和,必须完~全~相~同~哦!

我们需要计算出,总共有多少种满足条件的切割方法。

举个例子喵: 对于数组 [1, 2, 3, 0, 3] 总和是 1+2+3+0+3 = 9。每一段的和应该是 9 / 3 = 3。 我们可以这样切:

  1. [1, 2] | [3] | [0, 3]
    • 第一段和是 3
    • 第二段和是 3
    • 第三段和是 3
    • 这是一种合法的切法!
  2. [1, 2] | [3, 0] | [3]
    • 第一段和是 3
    • 第二段和是 3
    • 第三段和是 3
    • 这也是一种合法的切法!

所以答案就是 2 种啦。


解题方法 (Solution Approach)

看到这个问题,最直接的想法可能是:我先固定第一刀的位置,再去找第二刀的位置,对吧喵?

比如说,我们用一个指针 i 从头开始遍历,固定第一段。然后我们再用另一个指针 ji 后面开始遍历,固定第二段。最后检查三段的和是不是相等。

这样虽然可以,但主人你看,n 最大有 50 万呢!ij 两个循环嵌套起来就是 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 时:

  1. 我们先检查,i 为止的前缀和 current_sum 是否等于 2 * target_sum

    • 如果是,这意味着从 0i 的这部分可以作为前两段。第三段(从 i+1n-1)的和自动就是 target_sum,这是完美的!
    • 那么,第一段可以在哪里结束呢?任何一个在 i 之前的位置 k,只要 a[0...k] 的和等于 target_sum,就是一个合法的第一段分割点。
    • 这样的 k 有多少个呢?正好就是我们一直记录的 count_one_third 的值!所以,我们把 count_one_third 加到最终答案 ways 里。
  2. 然后,我们再检查,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++ 实现,我已经加上了中文注释,方便主人理解每一行代码的作用哦~

cpp
#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] 的和是否等于 target2*target,而不需要每次都重新计算。

2. 时间复杂度 (Time Complexity)

  • 暴力解法 O(n²): 遍历所有可能的两个分割点 ij,需要两层循环,时间复杂度是 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 这种特殊情况下也能正确工作。

希望这篇题解能帮到主人哦!下次再一起研究其他有趣的问题吧,喵~ (ฅ^•ﻌ•^ฅ)

Released under the MIT License.