专栏文章

题解:P14361 [CSP-S 2025] 社团招新 / club(民间数据)

P14361题解参与者 1已保存评论 0

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
0 条
当前快照
1 份
快照标识符
@minfjxaa
此快照首次捕获于
2025/12/02 01:35
3 个月前
此快照最后确认于
2025/12/02 01:35
3 个月前
查看原文

P14361 [CSP-S 2025] 社团招新 / club 题解

题目大意

nn 个新成员分配到 3 个部门,每个成员对每个部门有不同的满意度 ai,ja_{i,j}。要求:
  • 每个部门人数不超过 n2\frac{n}{2}
  • 最大化总满意度

算法思路

核心思想:反悔贪心

这道题的关键在于先贪心分配,再调整优化

算法流程

第一步:贪心初始分配

对每个成员,直接分配到满意度最高的部门:
CPP
for (int i = 0; i < n; i++) {
    int best = 0;
    if (a[i][1] > a[i][best]) best = 1;
    if (a[i][2] > a[i][best]) best = 2;

    assign[i] = best;
    ans += a[i][best];
    cnt[best]++;
}
此时可能某些部门人数超过 n2\frac{n}{2},需要调整。

第二步:反悔调整

策略:对于人数超过限制的部门,找到转移损失最小的成员,调整到其他部门。
关键问题:如何快速找到转移损失最小的成员?
解决方案:使用**优先队列(小根堆)**维护每个部门的成员,按照转移到其他部门的最小损失排序。
CPP
// 对每个部门维护一个优先队列
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq[3];

// 初始化时计算每个人的转移损失
for (int i = 0; i < n; i++) {
    // ... 分配到best部门 ...

    // 计算从best转移到其他部门的最小损失
    int minLoss = INT_MAX;
    for (int j = 0; j < 3; j++) {
        if (j != best) {
            minLoss = min(minLoss, a[i][best] - a[i][j]);
        }
    }
    pq[best].push({minLoss, i});
}

第三步:执行调整

当某个部门人数超限时:
  1. 找到人数最多的部门 maxDept
  2. 从该部门的优先队列取出转移损失最小的成员
  3. 将该成员转移到未满的部门
  4. 更新答案和计数器
CPP
while (cnt[0] > limit || cnt[1] > limit || cnt[2] > limit) {
    // 找到人数最多的部门
    int maxDept = ...;

    // 取出转移损失最小的成员
    auto [loss, idx] = pq[maxDept].top();
    pq[maxDept].pop();

    // 检查该成员是否还在原部门(延迟删除)
    if (assign[idx] != maxDept) continue;

    // 找到可转移的目标部门
    int target = 找到未满的部门且转移损失最小;

    // 执行转移
    ans -= realLoss;
    cnt[maxDept]--;
    assign[idx] = target;
    cnt[target]++;

    // 更新优先队列
    pq[target].push({新的转移损失, idx});
}

正确性证明

为什么贪心是正确的?

  1. 初始分配最优性:每个人选择自己最满意的部门,获得局部最优解
  2. 调整最小化损失:当需要调整时,我们选择损失最小的调整方案
  3. 全局最优性:由于每次调整都是最优的,最终结果接近全局最优

关键观察

  • 如果初始分配满足约束,则已经是最优解
  • 如果不满足约束,调整时选择损失最小的成员移动,保证总损失最小
  • 每个成员最多被调整常数次,复杂度可控

复杂度分析

时间复杂度

  • 初始分配:O(n)O(n)
  • 建堆:O(nlogn)O(n \log n)
  • 调整阶段:每个成员最多进出队列 O(1)O(1) 次,每次操作 O(logn)O(\log n)
  • 总复杂度O(nlogn)O(n \log n)

空间复杂度

  • 三个优先队列:O(n)O(n)
  • 辅助数组:O(n)O(n)
  • 总复杂度O(n)O(n)

优化技巧

1. 延迟删除

优先队列不支持高效删除指定元素。使用 assign[idx] 记录当前分配,取出元素时检查是否还在原部门。
CPP
if (assign[idx] != maxDept) continue; // 已经被调整过

2. 优先队列维护

每个部门维护一个小根堆,存储 {转移损失, 成员编号},快速找到最小损失。

3. 实时更新

成员被调整到新部门后,重新计算转移损失并加入新部门的优先队列。

代码实现

CPP
#include<bits/stdc++.h>

using namespace std;

#define endl '\n'
#define file(x) freopen(#x".in", "r", stdin), freopen(#x".out", "w", stdout)
#define fclose fclose(stdin), fclose(stdout)
#define debug(x) cerr << #x << " = " << (x) << endl

void solve() {
    int n;
    cin >> n;

    vector<array<int, 3>> a(n);
    for (int i = 0; i < n; i++) {
        cin >> a[i][0] >> a[i][1] >> a[i][2];
    }

    int limit = n / 2;

    // 先给每个人分配最优部门(贪心)
    long long ans = 0;
    vector<int> assign(n);

    // 用优先队列维护每个部门中的人,按照转移损失排序
    // pq[i] 存储分配到部门i的人,按照转移到其他部门的最小损失排序(小根堆)
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq[3];

    vector<int> cnt(3, 0);

    for (int i = 0; i < n; i++) {
        // 选择满意度最大的部门
        int best = 0;
        if (a[i][1] > a[i][best]) best = 1;
        if (a[i][2] > a[i][best]) best = 2;

        assign[i] = best;
        ans += a[i][best];
        cnt[best]++;

        // 计算从best转移到其他部门的最小损失
        int minLoss = INT_MAX;
        for (int j = 0; j < 3; j++) {
            if (j != best) {
                minLoss = min(minLoss, a[i][best] - a[i][j]);
            }
        }
        pq[best].push({minLoss, i});
    }

    // 调整:如果有部门超过limit,需要把人调走
    while (cnt[0] > limit || cnt[1] > limit || cnt[2] > limit) {
        // 找到人数最多的部门
        int maxDept = 0;
        for (int i = 1; i < 3; i++) {
            if (cnt[i] > cnt[maxDept]) maxDept = i;
        }

        if (cnt[maxDept] <= limit) break;

        // 从优先队列中取出转移损失最小的人
        while (!pq[maxDept].empty()) {
            auto [loss, idx] = pq[maxDept].top();
            pq[maxDept].pop();

            // 检查这个人是否还在maxDept(可能已经被调整过了)
            if (assign[idx] != maxDept) continue;

            // 找到可以转移的目标部门
            int target = -1;
            int realLoss = INT_MAX;
            for (int j = 0; j < 3; j++) {
                if (j != maxDept && cnt[j] < limit) {
                    int curLoss = a[idx][maxDept] - a[idx][j];
                    if (curLoss < realLoss) {
                        realLoss = curLoss;
                        target = j;
                    }
                }
            }

            if (target == -1) continue;

            // 执行调整
            ans -= realLoss;
            cnt[maxDept]--;
            assign[idx] = target;
            cnt[target]++;

            // 重新计算并加入优先队列
            int newMinLoss = INT_MAX;
            for (int j = 0; j < 3; j++) {
                if (j != target) {
                    newMinLoss = min(newMinLoss, a[idx][target] - a[idx][j]);
                }
            }
            pq[target].push({newMinLoss, idx});
            break;
        }
    }

    cout << ans << endl;
}

signed main() {
    //file(club);
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int T = 1;
    cin >> T;
    while (T--) {
        solve();
    }
    //fclose;
    return 0;
}

样例分析

样例 1-1

输入:
CPP
4
4 2 1
3 2 4
5 3 4
3 5 1
初始贪心分配
  • 成员 1:最大 4(部门 1)
  • 成员 2:最大 4(部门 3)
  • 成员 3:最大 5(部门 1)
  • 成员 4:最大 5(部门 2)
部门人数:[2, 1, 1],满足约束,答案 = 4+4+5+5 = 18

样例 1-2

输入:
CPP
4
0 1 0
0 1 0
0 2 0
0 2 0
初始贪心分配
  • 成员 1-2:最大 1(部门 2)
  • 成员 3-4:最大 2(部门 2)
部门人数:[0, 4, 0],部门 2 超限!
调整
  • 成员 1-2 转移损失 1-0=1
  • 成员 3-4 转移损失 2-0=2
  • 选择成员 1-2 转移到部门 1
最终:部门人数 [2, 2, 0],答案 = 0+0+2+2 = 4

总结

这道题的核心是反悔贪心算法:
  1. 先贪心选择局部最优
  2. 用优先队列维护调整代价
  3. 当约束不满足时,选择代价最小的调整方案

温馨提示

注意事项:
  1. 比赛的时候一定要检查有没有加 freopen, 如果没加的话就加上,加上了之后写代码时先注释掉,最后取消注释的时候一定要再编译一遍!(我同学已经因为这个吃亏了)
  2. 比赛写到最后1小时的时候,如果别的题没有思路,就先给已经写过的题做数据分层,根据不同范围分析暴力枚举,深度优先搜索(Depth-First Search, DFS)等算法
解题技巧
  1. 可以根据数据范围来猜测算法 时间/空间 复杂度 然后再根据复杂度选取合适的算法
  2. 要多观察特殊性质,有时可以通过特殊性质推导出正解,也可以通过特殊性质进行混分,比赛总是分越多拿奖概率越高
(润色,表格,与样例相关内容由AI完成,其余均由作者本人完成)

评论

0 条评论,欢迎与作者交流。

正在加载评论...