题解:洛谷 P8183 Sleeping in Class
【题目来源】
洛谷:P8183 [USACO22FEB] Sleeping in Class B - 洛谷
【题目描述】
奶牛 Bessie 最近很高兴能够重返线下课堂!不幸的是,她的老师 Farmer John 讲课非常无聊,因此她经常在课堂上睡着。
Farmer John 注意到 Bessie 在课堂上没有专心听讲。他让班上的另一位学生 Elsie 记录 Bessie 在每节课上睡着的次数。总共有 \(N\) 节课(\(1 \leq N \leq 10^5\)),Elsie 记录到 Bessie 在第 \(i\) 节课上睡着了 \(a_i\) 次(\(0 \leq a_i \leq 10^6\))。所有课程中 Bessie 睡着的总次数不超过 \(10^6\)。
Elsie 对 Bessie 感到非常竞争,她希望让 Farmer John 觉得 Bessie 在每节课上睡着的次数是一致的——让问题看起来完全是 Bessie 的错,而与 Farmer John 有时无聊的讲课无关。Elsie 修改记录的唯一方式是将两节相邻的课合并。例如,如果 \(a = [1,2,3,4,5]\),那么如果 Elsie 合并第二和第三节课,记录将变为 \([1,5,4,5]\)。
请帮助 Elsie 计算她需要对记录进行的最少修改次数,以使记录中的所有数字相等。
【输入】
每个输入包含 \(T\)(\(1 \leq T \leq 10\))个需要独立解决的测试用例。
第一行包含 \(T\),表示测试用例的数量。接下来的 \(T\) 组测试用例,每组由两行描述。每组的第一行包含 \(N\),第二行包含 \(a_1, a_2, \ldots, a_N\)。
保证每个测试用例中,\(a\) 的所有值之和不超过 \(10^6\)。同时,所有测试用例的 \(N\) 之和不超过 \(10^5\)。
【输出】
请输出 \(T\) 行,每行表示 Elsie 为使记录中的所有数字相等所需的最少修改次数。
【输入样例】
3
6
1 2 3 1 1 1
3
2 2 3
5
0 0 0 0 0
【输出样例】
3
2
0
【核心思想】
-
问题分析:给定 \(N\) 节课的睡觉次数 \(a_1, a_2, \dots, a_N\),每次操作可将相邻两节课合并(值相加)。目标是使所有数字相等,求最少合并次数。合并 \(k\) 次后剩余 \(N - k\) 个数,因此问题转化为:找到最大的 \(m\)(\(1 \leq m \leq N\)),使得可以将序列划分为 \(m\) 个连续子段,每段和相等。
-
算法选择:
- 枚举验证法:枚举最终可能的段数 \(i\)(从 \(N\) 递减到 \(1\)),检查是否能将序列划分为 \(i\) 个和相等的连续子段。第一个满足条件的 \(i\) 即为最大保留段数,答案为 \(N - i\)。
-
关键步骤:
- 计算总和:\(\text{sum} = \sum_{j=1}^{N} a_j\)
- 特判全零:若 \(\text{sum} = 0\),直接输出 \(0\)(已经全部相等)
- 枚举段数 \(i\) 从 \(N\) 到 \(1\):
- 若 \(\text{sum} \bmod i \neq 0\),跳过(无法均分)
- 目标每段和:\(\text{avg} = \text{sum} / i\)
- 贪心验证划分:
- 遍历序列,累加当前段和 \(\text{tot}\)
- 若 \(\text{tot} > \text{avg}\):划分失败,跳出
- 若 \(\text{tot} = \text{avg}\):段数 \(\text{cnt}++\),重置 \(\text{tot} = 0\)
- 若完整遍历后 \(\text{cnt}\) 为有效值,更新 \(\text{maxn} = \max(\text{maxn}, \text{cnt})\)
- 输出答案:\(N - \text{maxn}\)(合并次数 = 原段数 - 最大可保留段数)
-
时间/空间复杂度:
- 时间复杂度:\(O(N^2)\) 最坏情况,枚举 \(N\) 种段数,每种验证 \(O(N)\)
- 空间复杂度:\(O(N)\),存储序列
-
枚举段数 + 贪心验证的核心思想:
- 合并与划分的对偶性:合并相邻元素等价于将序列划分为若干连续子段,每段内的元素被合并为一个值。求最少合并次数等价于求最多可保留的段数。
- 总和整除约束:若划分为 \(i\) 段且每段和相等,则每段和必须为 \(\text{sum}/i\),因此 \(i\) 必须是 \(\text{sum}\) 的约数(或满足 \(\text{sum} \bmod i = 0\))
- 贪心连续性验证:从左到右累加,一旦达到目标值立即切段,若超过则失败。这种贪心正确性基于"连续子段必须恰好匹配目标值"的约束
- 从大到小枚举优化:从 \(N\) 向下枚举段数,第一个满足条件的即为最大保留段数,此时 \(N - i\) 最小
- 适用于序列合并、连续子段划分、均分判定类问题
【解题思路】

【算法标签】
普及 #模拟
【代码详解】
#include <bits/stdc++.h>
using namespace std;
int t, n, avg;
int a[100005];
int main()
{
cin >> t; // 输入t
while (t--) { // 遍历t组数据
cin >> n; // 输入n
long long sum = 0; // 定义sum,因为a最大为10^6,所以设定sum为longloong(定义为int也可以AC)
for (int i=1; i<=n; i++) { // 遍历n个数
cin >> a[i]; // 记录每个数
sum += a[i]; // 并计算所有数之和
}
if (sum==0) { // 特判和为0
cout << 0 << endl; // 则直接输出0
continue; // 继续下组数据
}
int maxn = 0; // 定义最大合并堆数
for (int i=n; i>=1; i--) { // 从n堆开始遍历至1堆
if (sum%i==0) { // 如果可以平均分
int tot = 0; // 定义多个数之和,初始为0
int cnt = 0; // 定义堆数计数器,初始为0
for (int j=1; j<=n; j++) { // 从第一个数开始遍历
tot += a[j]; // 计算数之和
if (tot>sum/i) { // 如果相加之和大于平均数
cnt = 0; // 重置堆数,并从下个堆数i开始计算
break;
} else if (tot==sum/i) { // 如果相加之和等于平均数
cnt++; // 堆数加1
tot = 0; // 并重置数之和,便于计算后面的数
}
}
maxn = max(maxn, cnt); // 计算最大的堆数
}
}
cout << n - maxn << endl; // n减最大的堆数,就是最少的合并次数
}
return 0;
}
【运行结果】
3
6
1 2 3 1 1 1
3
3
2 2 3
2
5
0 0 0 0 0
0
浙公网安备 33010602011771号