题解:洛谷 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

【核心思想】

  1. 问题分析:给定 \(N\) 节课的睡觉次数 \(a_1, a_2, \dots, a_N\),每次操作可将相邻两节课合并(值相加)。目标是使所有数字相等,求最少合并次数。合并 \(k\) 次后剩余 \(N - k\) 个数,因此问题转化为:找到最大的 \(m\)\(1 \leq m \leq N\)),使得可以将序列划分为 \(m\) 个连续子段,每段和相等。

  2. 算法选择

    • 枚举验证法:枚举最终可能的段数 \(i\)(从 \(N\) 递减到 \(1\)),检查是否能将序列划分为 \(i\) 个和相等的连续子段。第一个满足条件的 \(i\) 即为最大保留段数,答案为 \(N - i\)
  3. 关键步骤

    • 计算总和\(\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}\)(合并次数 = 原段数 - 最大可保留段数)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N^2)\) 最坏情况,枚举 \(N\) 种段数,每种验证 \(O(N)\)
    • 空间复杂度:\(O(N)\),存储序列
  5. 枚举段数 + 贪心验证的核心思想

    • 合并与划分的对偶性:合并相邻元素等价于将序列划分为若干连续子段,每段内的元素被合并为一个值。求最少合并次数等价于求最多可保留的段数。
    • 总和整除约束:若划分为 \(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
posted @ 2026-08-29 09:40  团爸讲算法  阅读(3)  评论(0)    收藏  举报