【算法题解】分组讨论活跃度最大值问题
一、题目原文
班主任计划将班级里的 n 名同学划分为若干个学习小组,每名同学都需要分入某一个学习小组中。班级里的同学依次以 1,2,...,n 编号,第 i 名同学有其发言积极度 cᵢ。
如果一个学习小组中恰好包含编号为 p₁,p₂,...,pₖ 的 k 名同学,则该学习小组的基础讨论积极度 为 aₖ,综合讨论积极度 为:
aₖ + max{cₚ₁, cₚ₂, ..., cₚₖ} - min{cₚ₁, cₚ₂, ..., cₚₖ}
即基础讨论积极度加上小组内同学的最大发言积极度与最小发言积极度之差。
给定基础讨论积极度 a₁,a₂,...,aₙ,请你计算将这 n 名同学划分为学习小组的所有可能方案中,综合讨论积极度之和的最大值。
输入格式
- 第一行,一个正整数 n,表示班级人数。
- 第二行,n 个非负整数 c₁,c₂,...,cₙ,表示每位同学的发言积极度。
- 第三行,n 个非负整数 a₁,a₂,...,aₙ,表示不同人数学习小组的基础讨论积极度。
输出格式
输出一行,一个整数,表示所有划分方案中,学习小组综合讨论积极度之和的最大值。
输入样例
|-------------------|
| 4 2 1 3 2 1 5 6 3 |
输出样例
|----|
| 12 |
二、题目解析
核心题意拆解
我们需要将 n 个同学划分成若干小组,每个小组的收益由两部分组成:
- 基础收益 :由小组人数决定,k 人小组的基础收益为 aₖ。
- 额外收益 :小组内发言积极度的最大值与最小值之差 max(c) - min(c)。
- 目标:找到所有划分方案中,收益总和的最大值。
关键观察与优化
直接枚举所有分组方案的复杂度是指数级的,无法处理 n ≤ 300 的数据规模。我们需要找到一个可以用动态规划解决的模型,核心优化点在于:
- 排序的必要性 :为了最大化额外收益 max(c) - min(c),最优分组方案一定是将发言积极度相近的同学分在同一组。因此,我们可以先将所有同学的 c 值从小到大排序。
- 排序后,任意区间 [l, r] 的 max(c) - min(c) 等于 c[r] - c[l],这极大地简化了额外收益的计算。
- 分组的最优策略 :排序后,我们可以将 j 组的第 j 个小组视为从两端向中间 "截取" 的区间。第 j 组的 min 是 c[j],max 是 c[n-j+1],差值即为 c[n-j+1] - c[j]。
三、模型抽象
动态规划状态定义
我们定义二维状态:
- f[j][k]:表示将 k 个同学划分成 j 个小组时,能获得的最大综合讨论积极度。
状态转移方程
考虑最后一步:我们新增一个人数为 i 的小组。
- 转移来源:f[j-1][k-i],即 j-1 个小组,用掉了 k-i 个同学。
- 新增收益:a[i] + (c[n-j+1] - c[j])。
- 其中,a[i] 是基础收益,c[n-j+1] - c[j] 是当前小组的额外收益。
因此,状态转移方程为:
f[j][k] = max(f[j][k], f[j-1][k-i] + a[i] + (c[n-j+1] - c[j]))
边界条件与答案
- 边界:f[0][0] = 0,表示 0 个小组、0 个同学时收益为 0。
- 答案:max{f[j][n]},其中 j 从 1 到 n,表示将 n 个同学全部分配完的所有方案中的最大值。
四、动态规划核心思路详解
循环顺序的设计
本题的额外收益 c[n-j+1] - c[j] 严格依赖小组编号 j ,因此 j 必须正序枚举 ,才能保证第 j 组能正确取到两端的 min 和 max。
最终采用的循环顺序为:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| for (int i = n; i >= 1; i--) // i:当前小组人数(倒序枚举,类似01背包避免重复使用) for (int j = 1; j <= n; j++) // j:小组数量(正序枚举,保证额外收益计算正确) for (int k = i; k <= n; k++) // k:已用总人数(正序枚举) |
额外收益的特殊处理
- 当小组人数 i = 1 时,max(c) = min(c),额外收益为 0,无需计算差值。
- 当 i > 1 时,额外收益为 c[n-j+1] - c[j]。
五、完整代码提供
以下为通过官方评测的标准 AC 代码,与题目参考程序完全一致:
cpp
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 305;
int n;
int c[N], a[N];
int f[N][N];
int ans;
int main()
{
int i, j, k;
scanf("%d", &n);
for (i = 1; i <= n; i++)
scanf("%d", &c[i]);
for (i = 1; i <= n; i++)
scanf("%d", &a[i]);
sort(c + 1, c + n + 1);
for (i = n; i >= 1; i--)
for (j = 1; j <= n; j++)
for (k = i; k <= n; k++)
{
int diff = 0;
if (i > 1)
diff = c[n - j + 1] - c[j];
f[j][k] = max(f[j][k], f[j - 1][k - i] + a[i] + diff);
if (k == n)
ans = max(ans, f[j][k]);
}
printf("%d\n", ans);
return 0;
}
样例运行过程
输入:
|-------------------|
| 4 2 1 3 2 1 5 6 3 |
排序后 c = [1, 2, 2, 3],a = [1, 5, 6, 3]。
最优方案:划分为 2 个 2 人小组。
- 第 1 组:基础收益 a[2] = 5,额外收益 3-1=2,总收益 7。
- 第 2 组:基础收益 a[2] = 5,额外收益 2-2=0,总收益 5。
- 最终总和:7 + 5 = 12,与样例输出一致。
六、总结
解题关键要点
- 排序是前提 :通过排序将问题转化为区间取 max-min 的问题,大幅简化额外收益计算。
- 动态规划状态定义 :f[j][k] 清晰地刻画了分组过程,避免了重复计算。
- 循环顺序的重要性 :j 必须正序枚举,才能保证额外收益 c[n-j+1] - c[j] 的正确性,这是本题与普通背包题的核心区别。
- 复杂度分析 :三重循环的时间复杂度为 O(n³),对于 n ≤ 300 来说是完全可接受的。
同类问题拓展
这类带区间收益的分组问题,核心思路通常是:
- 排序后利用区间性质简化收益计算。
- 设计合适的 DP 状态,用分组数和总人数作为维度。
- 注意额外收益对循环顺序的特殊要求,避免生搬硬套普通背包的倒序写法。