GESP2026年9月认证C++六级( 第三部分编程题(1、数组划分))精讲


GESP C++ 六级编程题第一题:《数组划分》


一、故事时间:魔法师的数组切割任务

想象一下,你是一名魔法师。

面前有一排数字:

复制代码
1  2  -3  4

你的任务是把它们切割成若干个非空的连续子段

什么叫连续子段呢?

例如,我们可以这样切:

复制代码
1 | 2  -3 | 4

这就分成了三个子段:

  • 第一段:1

  • 第二段:2 -3

  • 第三段:4


注意两个关键词:

关键词1:连续。

不能把不相邻的数字随意放到同一段中。

例如,不能把 1-3 单独挑出来组成一段,因为它们中间还有数字 2


关键词2:非空。

每一段至少要有一个数字,不能切出空段。

那么,切割之后,我们应该怎样评价这个方案呢?

这就要看看题目规定的计分规则了。


二、理解计分规则:每一段的和,要平方!

题目规定:

一个子段的偏差值 = 这个子段中所有整数之和的平方。

整个划分方案的偏差值,就是所有子段偏差值加起来。

举个例子。

对于数组:

复制代码
1  2  -3  4

我们尝试一种切割方法:

复制代码
1 | 2  -3 | 4

逐段计算:

子段 子段内数字之和 偏差值(和的平方)
1 1 1² = 1
2 -3 -1 (-1)² = 1
4 4 4² = 16

所以,这种划分方案的总偏差值为:

1+1+16=18

但是,18 不一定是最小值!


我们还可以尝试其他切法。

例如:

复制代码
1  2  -3 | 4

第一段的和为:

1+2−3=0

第一段的偏差值为:

0^2 = 0

第二段的偏差值为:

4^2=16

总偏差值为:

0+16=16

这比刚才的 18 小。

那么,能不能找到更小的呢?

这就是本题真正的任务:

尝试所有合法的连续划分方案,找出偏差值最小的那个。

不过,如果我们每次都把所有切法列出来,可能会产生大量重复计算。

因此,我们需要请出今天的主角------动态规划!


三、第一步:把大问题拆成小问题

我们先问自己一个问题:

如果已经知道前面一部分数字的最优答案,能不能利用它,计算出后面数字的最优答案?

答案是:可以!

我们定义:

dpi = 前 i 个数字划分后的最小偏差值

例如:

复制代码
数组:1  2  -3  4
下标:1  2   3  4

那么:

  • dp[1]:数字 1 的最小偏差值。

  • dp[2]:数字 1、2 的最小偏差值。

  • dp[3]:数字 1、2、-3 的最小偏差值。

  • dp[4]:整个数组的最小偏差值。

最终,我们要求的答案就是:

dpn

这就像闯关游戏:

  • 第一关解决前 1 个数字。

  • 第二关解决前 2 个数字。

  • 第三关解决前 3 个数字。

  • ......

  • 最后一关解决全部数字。

每一关都可以借助前面已经计算好的结果。


四、第二步:寻找状态转移方程

这是整道题最关键的地方!

假设我们现在要计算 dp[i]

也就是说,我们要解决前 i 个数字的最优划分问题。

我们可以想象:

最后一段究竟从哪里开始?

假设最后一段从第 j+1 个数字开始,一直延伸到第 i 个数字。

那么数组就被分成了两部分:

复制代码
前面已经划分好的部分 | 最后一段

1  2  ...  j | j+1  ...  i

其中:

  • 前面 j 个数字的最小偏差值是 dp[j]

  • 最后一段包含第 j+1 到第 i 个数字。

  • 最后一段的偏差值,就是这些数字之和的平方。

所以:

但是,最后一段的起点并不固定!

它可以从第 1 个数字开始,也可以从第 2 个数字开始,还可以从第 3 个数字开始......

因此,我们要让 j0 一直枚举到 i-1

每一种 j 都代表一种可能的最后一段。

我们把所有方案都比较一遍,取其中的最小值:

这就是本题的核心状态转移方程!


用小学生能理解的话再说一遍

假设你已经站在第 i 个数字的位置。

你可以问:

"如果最后一段只包含第 i 个数字,答案是多少?"

接着问:

"如果最后一段包含第 i-1、第 i 个数字呢?"

再接着问:

"如果最后一段包含第 i-2、第 i-1、第 i 个数字呢?"

......

把这些可能性全部计算出来,选择总偏差值最小的方案。

动态规划的智慧,就是把一个大问题拆成已经解决的小问题,再从所有可能的选择中找到最优答案。


五、第三步:前缀和闪亮登场!

刚才的状态转移方程有一个小麻烦:

每次计算最后一段的数字之和,都要把这一段的数字重新加一遍。

如果数组很长,这样会浪费很多时间。

有没有办法快速求出任意一段的和呢?

当然有!

这就需要使用我们以前学习过的知识:前缀和

1. 什么是前缀和?

定义:

也就是前 i 个数字的总和。

例如:

复制代码
原数组 a:

下标:  1   2   3   4
数字:  1   2  -3   4

计算前缀和:

i ai prei
0 --- 0
1 1 1
2 2 3
3 -3 0
4 4 4

注意:

pre0=0

这是一个非常重要的边界条件。


2. 如何利用前缀和求区间和?

假设我们要求第 j+1 个数字到第 i 个数字的和。

我们可以用:

为什么?

因为:

而:

两者相减,前面的数字就全部抵消了!

最后剩下:

因此,原来的状态转移方程就可以改写成:

这样,每次计算最后一段的和,只需要一次减法!


六、第四步:用例子手动推导 DP 数组

我们继续使用:

复制代码
a = [1, 2, -3, 4]

前缀和为:

复制代码
pre[0] = 0
pre[1] = 1
pre[2] = 3
pre[3] = 0
pre[4] = 4

第 1 关:计算 dp1

只有一个数字 1

只能划分为:

复制代码
1

因此:

dp1=1^2=1


第 2 关:计算 dp2

现在有:

复制代码
1  2

最后一段有两种选择。

选择一:最后一段从第 2 个数字开始。

复制代码
1 | 2

偏差值:

选择二:最后一段从第 1 个数字开始。

复制代码
1  2

偏差值:

取最小值:


第 3 关:计算 dp3

现在有:

复制代码
1  2  -3

最后一段可以有三种选择。

j 前面部分 最后一段 总偏差值
0 1 2 -3 0
1 1 2 -3 2
2 1 2 -3 14

所以:

这里出现了一个很有意思的现象!

因为:

1+2−3=0

所以把前三个数字放在同一段,偏差值就是:

0^2=0


第 4 关:计算 dp4

现在要处理整个数组:

复制代码
1  2  -3  4

我们枚举最后一段的起点:

j 前面部分的最优值 最后一段 总偏差值
0 0 1 2 -3 4 16
1 1 2 -3 4 10
2 5 -3 4 6
3 0 4 16

取最小值:

对应的划分方案是:

复制代码
1 | 2 | -3  4

各段偏差值为:

所以这个例子的最小偏差值为 6


七、第五步:把算法变成 C++ 程序

理解了故事、前缀和与状态转移方程之后,我们就可以开始写代码了!

参考程序:

复制代码
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 2005;

int n;
int a[N];
long long pre[N];
long long dp[N];

int main() {
    cin >> n;

    // 第一步:读入数组,并计算前缀和
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        pre[i] = pre[i - 1] + a[i];
    }

    // 第二步:动态规划
    dp[0] = 0;

    for (int i = 1; i <= n; i++) {
        // 假设最后一段从第1个数字开始
        dp[i] = (pre[i] - pre[0]) * (pre[i] - pre[0]);

        // 枚举最后一段之前有多少个数字
        for (int j = 1; j < i; j++) {
            long long sum = pre[i] - pre[j];

            dp[i] = min(dp[i], dp[j] + sum * sum);
        }
    }

    // 第三步:输出答案
    cout << dp[n] << endl;

    return 0;
}

八、逐段理解代码

1. 为什么要定义前缀和数组?
复制代码
long long pre[N];

它就像一本已经记录好答案的账本。

pre[i] 记录前 i 个数字的总和。

以后想知道一段数字的和,就不用重新累加了。


2. 为什么要定义 dp 数组?
复制代码
long long dp[N];

它是我们的闯关记录表。

dp[i] 记录前 i 个数字能够获得的最小偏差值。

注意,这里使用 long long,是为了给较大的数值留出更充足的存储范围。


3. 为什么外层循环从 1 开始?
复制代码
for (int i = 1; i <= n; i++)

因为我们要依次计算:

复制代码
dp[1]
dp[2]
dp[3]
...
dp[n]

在计算 dp[i] 时,之前的 dp[0]dp[i-1] 都已经计算好了。

这正是动态规划的计算顺序。


4. 为什么内层循环从 1 开始?
复制代码
for (int j = 1; j < i; j++)

j 表示最后一段之前有多少个数字。

j=0 时,意味着整个数组都属于最后一段。

我们已经通过下面这行代码处理了这种情况:

复制代码
dp[i] = (pre[i] - pre[0]) * (pre[i] - pre[0]);

因此,内层循环只需要考虑 j=1i-1


5. 为什么要使用 min?
复制代码
dp[i] = min(dp[i], dp[j] + sum * sum);

因为我们要找的是最小偏差值。

每发现一种新的划分方案,就把它和目前的最优答案进行比较。

如果新方案更小,就更新答案。


九、算法复杂度分析

假设数组长度为 n

1、时间复杂度

前缀和只需要遍历一次数组:

O(n)

动态规划中:

  • 外层循环执行约 n 次。

  • 内层循环最多执行约 n 次。

因此,总时间复杂度为:


2、空间复杂度

我们使用了数组:

  • a[N]

  • pre[N]

  • dp[N]

每个数组的大小都与 n 成正比。

因此,空间复杂度为:


十、这道题真正考查了什么?

这道题并不是简单地让我们把数组切开,而是考查了几个重要的算法知识。

知识点 在本题中的作用
连续子段 明确每一段必须由连续数字组成
前缀和 快速计算任意连续区间的数字之和
动态规划 利用已经求出的最优答案解决更大的问题
枚举 枚举最后一段的所有可能起点
最优化 从所有划分方案中选择偏差值最小的方案
long long 为较大的计算结果提供更充足的存储范围

特别提醒:负数并不会破坏前缀和

本题中出现了 -3 这样的负数。

有些同学可能会想:

"如果数组中有负数,前缀和还能用吗?"

当然可以!

前缀和并不要求数组中的数字都是正数。

无论数字是正数、负数还是零,都可以使用:

prei−prej

求出对应区间的和。

不过,不能因为数组里有负数,就认为偏差值也可能是负数。

因为偏差值是区间和的平方:

所以每一段的偏差值都不会小于零。


十一、送给同学们的 DP 记忆口诀

遇到类似的连续划分问题,可以记住下面这段口诀:

先定义状态,再寻找最后一段;

前缀和来帮忙,区间求和不用慌;

枚举所有分界点,前后答案加起来;

每种方案都比较,最小答案留下来。

最后,我们再把本题最重要的公式记牢:

这就是《数组划分》的核心。

当同学们学会从"最后一段"思考问题时,就掌握了许多连续划分类动态规划问题的关键思路。


相关推荐
千里码aicood1 小时前
基于PLC与机器视觉的智能分拣控制系统设计
c++·自动化·plc
漂流瓶jz1 小时前
UVA-1442 洞穴 题解答案代码 算法竞赛入门经典第二版
c++·算法·题解·aoapc·算法竞赛入门经典·uva
朽棘不雕1 小时前
进一步了解模板
c++
汉克老师4 小时前
CSP-J 2026 初赛试题解析(第一部分:选择题(8-15))精讲
c++·csp-j·小学生·学c++编程
wabs6665 小时前
关于二叉树【力扣572.另一棵树的子树的思考】
数据结构·c++·算法·leetcode·二叉树
风合星语6 小时前
2026 ROS 2 Lyrical C++ 入门(五):Action 实战——任务反馈、取消与超时处理
c++·机器人·ros2·异步编程·lyrical
91刘仁德7 小时前
C++ 继承和多态 设计模式
c语言·c++·笔记
库玛西7 小时前
哈夫曼树与前缀编码:数据压缩的贪心核心
c语言·c++·笔记·考研
别动我齐刘海7 小时前
ROS2 Jazzy + C++ 实战路线——进阶学习3
c++·人工智能·vscode·python·算法·机器学习·机器人