#19486. 具有最小长度约束的分组
具有最小长度约束的分组
太棒了!我们在打牢了“标准划分”和“数量约束”的基础后,现在向更真实的赛题迈进。
在 NOI/CSP 等高级别别的比赛中,出题人极少会给你一个干干净净的标准模型,他们总会加入一些“物理限制”。本题将完全涵盖讲义中 "Handling Constraints" (处理约束条件) 和 "Pruning Loops" (循环剪枝) 的核心知识点。
请准备好草稿纸,重点关注这道题中内外层循环边界的推导,这是本题的灵魂!
题目三:具有最小长度约束的分组 (Partitioning with Minimum Length)
【题目描述】
给定一个包含 个整数的数组 ,以及两个正整数 和 。
你需要将数组 恰好划分为 个非空且连续的子数组,并且规定每个子数组的长度至少为 。
我们定义一个划分方案的“得分”为:这 个子数组的平均值之和。
请你设计一个算法,求出所有合法划分方案中,能够获得的最大得分。如果不存在任何合法的划分方案,请输出 Impossible。
【输入格式】
从标准输入读入数据。 第一行包含三个正整数 、 和 ,分别表示数组的长度、需要划分的段数,以及每个子数组的最短长度限制。 第二行包含 个整数 ,相邻两个数之间用空格隔开。
【输出格式】
输出到标准输出。
如果能够找到合法的划分方案,输出一个实数,表示能获得的最大平均值之和(建议保留 位小数,误差在 以内视为正确)。
如果无论如何都无法满足条件,请输出字符串 Impossible。
【样例输入 1】
5 2 2
9 1 2 3 9
【样例输出 1】
10.000000
【样例 1 说明】
需要将 5 个元素恰好分成 2 段,且每段长度至少为 2。 合法的划分方案只有以下两种:
[9, 1]和[2, 3, 9]:平均值分别为 和 ,总和约为 。[9, 1, 2]和[3, 9]:平均值分别为 和 ,总和为 。 因此,最大得分为 。
【样例输入 2】
4 3 2
1 2 3 4
【样例输出 2】
Impossible
【样例 2 说明】
要求分成 3 段,且每段长度至少为 2,那么总元素个数至少需要 个。当前数组只有 4 个元素,显然无法完成合法的划分。
【数据范围与提示】
💡 教练的赛场指引 (涵盖讲义核心约束处理知识点)
这道题是考察你对 DP 状态转移边界控制能力的“放大镜”。讲义中关于边界剪枝的内容在这里体现得淋漓尽致,请在草稿纸上推演以下数学不等式:
1. 宏观不可达性判定 (The Default Assumption & Constraints)
- 提前终止:讲义提到,如果不满足隐式或显式的约束,状态毫无意义。如果 ,这意味着哪怕每个组都紧贴着最短长度 ,总长度也超出了数组大小。这种情况下连 DP 都不用跑,直接输出
Impossible即可。
2. 精准循环剪枝 (Pruning Loops for Efficiency)
讲义在 Page 30 强调,对 和 的循环边界进行剪枝,不仅能避免越界导致的 Bug,更能大幅提升程序运行效率!
- 外层循环 的下界:
dp(i, j)表示前i个元素分为j段。因为每段长度至少为 ,所以j段所需的最少元素个数是 。因此,计算第j层时,i不再是从j开始,而是必须从 开始。 - 内层分割点 的下界 (Split Point Start):前
j-1个分区需要前缀提供元素,它们的最小长度之和是 。因此,最后一段的起点 (基于 1 开始的索引,即最后一段包含 ),它前面必须至少留足 个元素。故起点 的最小值是 。 - 内层分割点 的上界 (Minimum Length):最后一段(第 段)自身的长度也必须至少为 。它的长度是 。由 可推导出,起点 的最大值不能超过 。
3. 第一列 Base Case 的约束陷阱
- 当只划分 1 段(
j=1)时,dp(i, 1)等于前i个元素的平均值。但请注意,此时整段的长度就是i。如果i < L,连 1 段的最小长度都不满足,此时的dp(i, 1)必须保持为负无穷大 (-1e9),绝不能计算均值!
📐 算法逻辑伪代码 (C++14 风格流程设计)
结合讲义上的循环约束剪枝公式,教练为你画好了严格带有范围控制的流程图。请特别注意菱形判断框中关于 i 和 x 边界的数学表达式。
(注:为避免渲染错误,流程图中的比较操作均使用中文描述)
graph TD
A(开始算法) --> B{N 小于 K 乘 L 吗}
B -- 是 --> C(直接输出 Impossible 并结束)
B -- 否 --> D(初始化前缀和 prefix 并填入数据)
D --> E(初始化二维 DP 表为负无穷大 -1e9)
E --> F(处理基准情况: 循环 i 从 L 到 N)
F --> G(dp 的第 i 行 1 列赋值为: 前 i 个数之和除以 i)
G --> H(循环结束: 此时长度不足 L 的位置依旧是负无穷)
H --> I(外层循环 j: 划分段数从 2 到 K)
I --> J{j 小于等于 K 吗}
J -- 否: 循环结束 --> Z(返回最终答案 dp 的 N 行 K 列)
J -- 是 --> K(中层循环 i: 数组前缀长度 从 j 乘 L 开始递增)
K --> L{i 小于等于 N 吗}
L -- 否: i 循环结束 --> I1(j 增加 1)
I1 --> I
L -- 是 --> M(内层循环 x: 最后一个分段的起点)
M --> N(x 初始化为: j减1 的差乘 L 再加 1)
N --> O{x 小于等于 i减L加1 吗}
O -- 否: x 循环结束 --> K1(i 增加 1)
K1 --> K
O -- 是 --> P(计算最后一段从 x 到 i 的平均值 avg)
P --> Q(状态转移: dp_i_j 尝试更新为 dp_x-1_j-1 加上 avg 的较大者)
Q --> M1(x 增加 1)
M1 --> O
🧠 赛场思考与自我检验
在将上述流程图转为代码之前,请问自己一个问题: “为什么在带有长度限制 的情况下,我不推荐你强行把它压缩成一维滚动数组?”
答案是:在赛场上,边界控制越复杂的 DP,调试难度越大。本题的二维 DP 空间复杂度 对于 而言极小(不到 1MB)。为了保证稳妥(防止在倒序遍历的一维数组中写错 的取值区间导致数据污染),我们应当果断选择二维数组。
等你把这套带有“精确剪枝”的逻辑在脑海中融会贯通后,随时呼叫我,为你提供这道题从暴力到标准的 C++14 实现代码和数据生成器!