题解:洛谷 P14361 社团招新

【题目来源】

洛谷:P14361 [CSP-S 2025] 社团招新 / club(官方数据) - 洛谷

【题目描述】

小 L 是学校算法协会的成员。在今年的学校社团招新中,小 L 一共招收了 \(n\) 个新成员,其中 \(n\)偶数。现在小 L 希望将他们分到协会不同的部门。

算法协会共设有三个部门,其中第 \(i\) (\(1 \leq i \leq n\))个新成员对第 \(j\) (\(1 \leq j \leq 3\))个部门的满意度为 \(a_{i,j}\)。定义一个分配方案的满意度为所有新成员对分配到的部门的满意度之和,也就是说,若将第 \(i\) (\(1 \leq i \leq n\))个新成员分配到了第 \(d_i \in \{1,2,3\}\) 个部门,则该分配方案的满意度为 \(\sum_{i=1}^{n} a_{i,d_i}\)

小 L 不希望某一个部门的新成员数量过多。具体地,他要求在分配方案中,不存在一个部门被分配多于 \(\frac{n}{2}\) 个新成员。你需要帮助小 L 求出,满足他要求的分配方案的满意度的最大值。

【输入】

本题包含多组测试数据。

输入的第一行包含一个正整数 \(t\),表示测试数据组数。

接下来依次输入每组测试数据,对于每组测试数据:

  • 第一行包含一个正整数 \(n\),表示新成员的数量。
  • \(i+1\) (\(1 \leq i \leq n\))行包含三个非负整数 \(a_{i,1}, a_{i,2}, a_{i,3}\),分别表示第 \(i\) 个新成员对第 \(1,2,3\) 个部门的满意度。

【输出】

对于每组测试数据,输出一行一个非负整数,表示满足小 L 要求的分配方案的满意度的最大值。

【输入样例】

3
4
4 2 1
3 2 4
5 3 4
3 5 1
4
0 1 0
0 1 0
0 2 0
0 2 0
2
10 9 8
4 0 0

【输出样例】

18
4
13

【核心思想】

  1. 问题分析:给定 \(n\)(偶数)个新成员,每人对 \(3\) 个部门的满意度 \(a_{i,1}, a_{i,2}, a_{i,3}\)。需将每人分配到一个部门,使得总满意度最大,且每个部门人数不超过 \(\frac{n}{2}\)。这是一个带约束的多选一分组优化问题。

  2. 算法选择

    • 贪心调整法:先让每个人都选择满意度最高的部门(无约束最优),若某部门人数超标,则将部分成员"降级"到次优部门,每次选择损失最小(即最大值与次大值差最小)的成员进行调整
  3. 关键步骤

    • 预处理每人的信息
      • 找出每人三个满意度中的最大值 \(\text{sn}[i].\text{maxx}\) 及其部门索引 \(\text{sn}[i].\text{idx}\)
      • 计算最小损失 \(\text{sn}[i].d = \min(\text{maxx} - a_{i,j})\)\(j \neq \text{idx}\)),即若强制该成员不选最优部门,最小需要损失的满意度
      • 累加无约束最优总和 \(\text{ans} = \sum \text{sn}[i].\text{maxx}\)
      • 统计每个部门作为最优选择的人数 \(\text{cnt}[j]\)
    • 约束检查与调整
      • 找出人数最多的部门 \(\text{pos}\),若 \(\text{cnt}[\text{pos}] \leq \frac{n}{2}\),直接输出 \(\text{ans}\)
      • 否则:需要将该部门的人数降至 \(\frac{n}{2}\),调整数量为 \(\text{num} = \text{cnt}[\text{pos}] - \frac{n}{2}\)
      • 收集所有选择部门 \(\text{pos}\) 的成员的最小损失值 \(d\),排序后取前 \(\text{num}\) 个最小损失从 \(\text{ans}\) 中扣除
    • 输出调整后的总和
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \log n)\),预处理 \(O(n)\),排序差值 \(O(n \log n)\)
    • 空间复杂度:\(O(n)\),存储每人的信息和差值数组
  5. 贪心降级调整的核心思想

    • 先全局最优再局部修复:先不考虑约束求最大值,再通过最小损失调整满足人数上限,利用贪心保证调整代价最小
    • 损失最小化原则:当必须将某成员从最优部门移出时,选择"降级代价"最小的成员(即次优选择与最优选择差距最小的人),确保总满意度损失最少
    • 单部门超标处理:由于 \(n\) 为偶数且上限为 \(\frac{n}{2}\),最多只有一个部门会超标(鸽巢原理:若两个部门都超标,则总人数 \(> n\)),因此只需处理人数最多的部门
    • 排序选最小损失:将超标部门所有成员的损失值排序,取前 \(\text{num}\) 个最小值扣除,保证调整方案最优
    • 适用于带容量约束的分配问题、资源调度、负载均衡类问题

【算法标签】

普及 #贪心

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int N = 100005;  // 定义最大数组长度

// 结构体:存储每个测试用例的信息
struct Node
{
    int maxx;  // 三个数中的最大值
    int idx;   // 最大值所在的索引(1,2,3)
    int d;     // 最大值与次大值的最小差值
} sn[N];       // 存储每个测试用例的数据

int t;         // 测试用例数量
int n;         // 每个测试用例的数据组数
int a[4];      // 临时存储输入的三个数字
int ans;       // 当前测试用例的总和
int cnt[4];    // 统计每个索引作为最大值的次数
int b[N];      // 临时数组,存储需要调整的差值

int main()
{
    // 输入测试用例数量
    cin >> t;

    // 处理每个测试用例
    while (t--)
    {
        // 初始化变量
        ans = 0;
        memset(cnt, 0, sizeof(cnt));  // 清空计数器

        // 输入数据组数
        cin >> n;

        // 处理每组数据
        for (int i = 1; i <= n; i++)
        {
            // 输入三个数字
            cin >> a[1];

            // 初始化当前组的数据
            sn[i].maxx = a[1];  // 假设第一个数是最大值
            sn[i].idx = 1;      // 最大值索引为1
            sn[i].d = 1e9;      // 初始化差值为极大值

            // 输入剩余两个数字并找出最大值
            for (int j = 2; j <= 3; j++)
            {
                cin >> a[j];
                // 更新最大值和索引
                if (a[j] > sn[i].maxx)
                {
                    sn[i].maxx = a[j];
                    sn[i].idx = j;
                }
            }

            // 计算最大值与次大值的最小差值
            for (int j = 1; j <= 3; j++)
            {
                // 跳过最大值本身
                if (j == sn[i].idx)
                {
                    continue;
                }
                // 更新最小差值
                sn[i].d = min(sn[i].d, sn[i].maxx - a[j]);
            }

            // 累加最大值到总和
            ans += sn[i].maxx;

            // 统计该索引作为最大值的次数
            cnt[sn[i].idx]++;
        }

        // 找出出现次数最多的索引
        int pos = 0;  // 存储出现次数最多的索引
        for (int i = 1; i <= 3; i++)
        {
            if (cnt[i] > cnt[0])
            {
                cnt[0] = cnt[i];  // 使用cnt[0]存储最大次数
                pos = i;          // 记录对应的索引
            }
        }

        // 检查是否需要调整(如果某个索引出现次数超过n/2)
        if (cnt[pos] <= n / 2)
        {
            // 不需要调整,直接输出总和
            cout << ans;
        }
        else
        {
            // 需要调整:收集需要减少的差值
            int len = 0;  // 临时数组长度

            // 遍历所有数据,收集需要调整的组的差值
            for (int i = 1; i <= n; i++)
            {
                if (sn[i].idx == pos)
                {
                    len++;
                    b[len] = sn[i].d;  // 存储差值
                }
            }

            // 对差值进行升序排序(最小的差值先调整)
            sort(b + 1, b + 1 + len);

            // 计算需要调整的数量
            int num = cnt[pos] - n / 2;

            // 从总和中减去最小的num个差值
            for (int i = 1; i <= num; i++)
            {
                ans -= b[i];
            }

            // 输出调整后的总和
            cout << ans;
        }

        // 每个测试用例结束后换行
        cout << endl;
    }

    return 0;
}

【运行结果】

3
4
4 2 1
3 2 4
5 3 4
3 5 1
18
4
0 1 0
0 1 0
0 2 0
0 2 0
4
2
10 9 8
4 0 0
13

【算法真传】

从一道 CSP-S 真题出发:聊聊带约束分配的贪心降级策略

posted @ 2026-08-28 15:54  团爸讲算法  阅读(5)  评论(0)    收藏  举报