1. 题目数据 (Problem Metadata)

2. 题意简述 (Problem Summary)

给定 nnnn 为偶数,2n1052 \le n \le 10^5)个新成员,每人对 33 个部门的满意度 ai,j[0,2×104]a_{i,j} \in [0, 2 \times 10^4]。为每个成员选择恰好一个部门 di{1,2,3}d_i \in \{1,2,3\},最大化满意度之和 i=1nai,di\sum_{i=1}^{n} a_{i,d_i},且每个部门分配人数不超过 n2\frac{n}{2}。多组测试数据,t5t \le 5

3. 朴素解法 (Brute-Force)

最直接的想法是枚举每个成员的部门选择,共 3n3^n 种方案,对每种方案检查容量约束并计算满意度。n=10n = 10 时约 6×1046 \times 10^4 种,可过测试点 141 \sim 4n30n \ge 303302×10143^{30} \approx 2 \times 10^{14},完全不可行。

另一种思路是 DP:设 dp[i][c1][c2]dp[i][c_1][c_2] 表示前 ii 人中部门 11c1c_1 人、部门 22c2c_2 人时的最大满意度(部门 33 人数为 ic1c2i - c_1 - c_2)。状态数 O(n3)O(n^3)n=105n = 10^5 时约 101510^{15},同样不可行。瓶颈在于容量约束使得状态空间与 nn 的幂次绑定,无法线性递推。

4. 核心解法 (Main Solution)

特殊性质

本题的关键观察在于 33 个部门的容量约束具有特殊的结构。设 cntj\text{cnt}_j 为部门 jj 分配到的人数,则 cnt1+cnt2+cnt3=n\text{cnt}_1 + \text{cnt}_2 + \text{cnt}_3 = n,每个 cntjn2\text{cnt}_j \le \frac{n}{2}

至多一个部门超员:若两个部门同时超员(各 >n2> \frac{n}{2}),则总人数 >n> n,矛盾。因此,无约束贪心解中至多有一个部门违反容量约束。

关键突破

3n3^n 的枚举瓶颈出发,利用「至多一个部门超员」这一性质,将问题分解为两步:

  1. 贪心:忽略容量约束,每个成员选满意度最高的部门,得到无约束最优解 sum\text{sum}
  2. 反悔:若唯一超员部门 mmcntm>n2\text{cnt}_m > \frac{n}{2},将 cntmn2\text{cnt}_m - \frac{n}{2} 人从部门 mm 改派到各自的次优部门,使容量满足约束。

推导过程

下面推导反悔策略的正确性。

改派人数:部门 mm 超员 cntmn2\text{cnt}_m - \frac{n}{2} 人,需恰好移出这么多。不能多移(不必要损失),不能少移(仍超员)。

改派目标:每个被移出的人应去自己的次优部门(满意度第二高的部门),而非第三优部门。因为次优满意度 \ge 第三优满意度,改派到次优的损失 \le 改派到第三优的损失。

次优部门不会超员:设其余两个部门共有 ncntmn - \text{cnt}_m 人。最坏情况下所有被改派的人都去同一个次优部门,该部门人数上界为:

(ncntm)+(cntmn2)=nn2=n2(n - \text{cnt}_m) + \left(\text{cnt}_m - \frac{n}{2}\right) = n - \frac{n}{2} = \frac{n}{2}

恰好等于容量上界,不会超员。

选损失最小的人改派:每个被改派成员 ii 的损失为 ai,maxai,suba_{i, \max} - a_{i, \text{sub}}(最优满意度与次优满意度之差)。将超员部门的成员按此损失升序排序,取前 cntmn2\text{cnt}_m - \frac{n}{2} 个改派,总损失最小。

最终答案为:

ans=sum改派的 k 人(ai,maxai,sub)\text{ans} = \text{sum} - \sum_{\text{改派的 } k \text{ 人}} \left(a_{i, \max} - a_{i, \text{sub}}\right)

其中 k=cntmn2k = \text{cnt}_m - \frac{n}{2},取损失最小的 kk 人。

5. 正确性证明 (Proof of Correctness)

下面我们总结一下贪心策略的证明要点,共证明了三点:贪心解的无约束最优性、反悔策略的充分性、次优部门的安全性。

引理 1(贪心最优):无容量约束时,每个成员独立选择满意度最高的部门,满意度之和取到全局最大。这是因为 i=1nai,di\sum_{i=1}^{n} a_{i,d_i} 中各项独立,maxdiai,di=maxdiai,di\max_{d_i} \sum a_{i,d_i} = \sum \max_{d_i} a_{i,d_i}

引理 2(至多一个超员部门):若部门 jjjj' 同时超员,cntj+cntj>n\text{cnt}_j + \text{cnt}_{j'} > n,但 cnt1+cnt2+cnt3=n\text{cnt}_1 + \text{cnt}_2 + \text{cnt}_3 = n,矛盾。故至多一个部门超员。

引理 3(次优部门安全):改派 k=cntmn2k = \text{cnt}_m - \frac{n}{2} 人到次优部门后,任一次优部门的人数上界为 n2\frac{n}{2}(见 §4 推导),不超员。

定理(反悔最优):在全局贪心解的基础上,从唯一超员部门 mm 移出恰好 kk 人,每人移到次优部门,选损失最小的 kk 人,所得方案是容量约束下的最优解。

设贪心解为 SS^*(无约束最优),任一合法解 TT 的满意度 S\le S^*(因为 SS^* 是无约束最大值)。合法解必须在 SS^* 基础上调整超员部门 mm 的分配:将至少 kk 人从部门 mm 移出。每人移出后,去次优部门的损失 \le 去第三优部门的损失(ai,subai,thirda_{i, \text{sub}} \ge a_{i, \text{third}}),故最优调整中每人都去次优部门。在所有「移 kk 人到次优部门」的方案中,选损失最小的 kk 人总损失最小。由引理 3,次优部门不超员,方案合法。因此该反悔策略得到的解是合法解中的最优。

综上所述,算法正确。

6. 复杂度分析 (Complexity)

  • 时间复杂度O(nlogn)O(n \log n)。每人对 33 个满意度排序 O(1)O(1)(常数大小),收集超员部门成员 O(n)O(n),按损失排序 O(nlogn)O(n \log n)n105n \le 10^5 时约 1.7×1061.7 \times 10^6 次运算,1s 内轻松通过。

  • 空间复杂度O(n)O(n)。存储 nn 个成员的满意度数组与临时向量,约几 MB,远低于 512 MB 限制。

7. 实现细节与避坑指南 (Implementation Details)

坑点 说明
整数范围 sum\text{sum} 最大 n×maxai,j=105×2×104=2×109n \times \max a_{i,j} = 10^5 \times 2 \times 10^4 = 2 \times 10^9int 上限约 2.147×1092.147 \times 10^9,够用。
多组数据清空 cntdeltasolve() 内部声明,每次调用自动初始化,无需手动清空。全局数组 s[] 每次被读入覆盖,也不会残留。
pair 排序的稳定性 代码用 pii{满意度, 部门号} 存储,sortpairfirst 升序、first 相同时按 second 升序。当满意度相同时,部门号小的排前面,a[2]second 为部门号较小的次优部门。这一行为确定性的,不影响正确性(满意度相同时去哪个次优部门损失相同)。
n=2n = 2 的边界 n2=1\frac{n}{2} = 1,每部门最多 11 人。若两人最优部门相同,需改派 11 人到次优部门。逻辑自然处理。
特殊性质 A(仅部门 1 有满意度) 所有成员 ai,2=ai,3=0a_{i,2} = a_{i,3} = 0,贪心全选部门 1,cnt1=n>n2\text{cnt}_1 = n > \frac{n}{2}。改派损失 =ai,10=ai,1= a_{i,1} - 0 = a_{i,1},取损失最小的 n2\frac{n}{2} 人改派,其余留部门 1。正确。

8. 参考代码 (Reference Code)

下面是我用 pair 排序同时解决「选最优部门」和「找次优部门」的写法。每人将 33 个满意度连同部门号存为 pii,排序后 a[3] 即最优、a[2] 即次优,改派时只需取 a[3].first - a[2].first 作为损失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <bits/stdc++.h>
using namespace std;
// #define int long long

const int N = 1e5 + 5;
using pii = pair<int, int>;
using vi = vector<int>;

struct student
{
pii a[4];
} s[N];

int n;

int solve()
{
int sum = 0;
vi cnt(4, 0);
cin >> n;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= 3; j++)
cin >> s[i].a[j].first, s[i].a[j].second = j;
sort(s[i].a + 1, s[i].a + 4);
sum += s[i].a[3].first;
cnt[s[i].a[3].second]++;
}
int max_ci = max_element(cnt.begin(), cnt.end()) - cnt.begin(), max_cv = *max_element(cnt.begin(), cnt.end());
if (max_cv <= n / 2)
return sum;

int ans = sum;
vector<student> delta;
for (int i = 1; i <= n; i++)
if (s[i].a[3].second == max_ci)
delta.push_back(s[i]);

sort(delta.begin(), delta.end(), [&](student s1, student s2)
{ return s1.a[3].first - s1.a[2].first < s2.a[3].first - s2.a[2].first; });
for (int i = 0; i < (max_cv - n / 2); i++)
ans -= delta[i].a[3].first - delta[i].a[2].first;

return ans;
}

signed main()
{
ios::sync_with_stdio(false);
#ifdef DEBUG
clock_t t0 = clock();
freopen("club2.in", "r", stdin);
freopen("data.out", "w", stdout);
#endif

// Don't stop. Don't hide. Follow the light, and you'll find tomorrow.

int t;
cin >> t;
while (t--)
cout << solve() << endl;

#ifdef DEBUG
cerr << "Time used:" << clock() - t0 << "ms" << endl;
#endif
return 0;
}

9. 补充说明 (Additional Notes)

  • 题目渊源:本题出自 CSP-S 2025 第二轮,是「贪心 + 反悔」的经典入门题。这类「先贪心求无约束最优,再对违反约束的部分做最小代价调整」的思路在竞赛中很常见,核心识别信号是约束结构简单(如至多一个维度违约)、且调整代价可排序取最小。本题 33 个部门与 n2\frac{n}{2} 限制的设定使得「至多一个超员」这一性质很容易发现,是反悔策略成立的基础。
  • 数据范围与测试点:测试点按 nn 规模分档,最小 n=2n = 2(测试点 1),最大 n=105n = 10^5(测试点 12 起)。特殊性质 A(仅部门 1 有满意度)和 B(仅部门 1、2 有满意度)是贪心 + 反悔的退化场景,C 为随机数据。O(nlogn)O(n \log n) 的排序解法可过全部测试点。

其他版本

下面是一个不依赖 pair 排序的写法,直接用比较找最优和次优部门。思路相同,但省去了排序 33 个元素的步骤,常数更小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 5;

struct student
{
int a[4]; // a[1..3] = 满意度
int best, sub; // 最优/次优部门号
} s[N];

int n;

int solve()
{
int sum = 0;
int cnt[4] = {0};
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> s[i].a[1] >> s[i].a[2] >> s[i].a[3];
// 找最优和次优部门
if (s[i].a[1] >= s[i].a[2]) { s[i].best = 1; s[i].sub = (s[i].a[2] >= s[i].a[3]) ? 2 : 3; }
else { s[i].best = 2; s[i].sub = (s[i].a[1] >= s[i].a[3]) ? 1 : 3; }
// 修正:需保证 best 是最大的
if (s[i].a[s[i].sub] > s[i].a[s[i].best]) swap(s[i].best, s[i].sub);
sum += s[i].a[s[i].best];
cnt[s[i].best]++;
}
int max_ci = max_element(cnt + 1, cnt + 4) - cnt;
int max_cv = *max_element(cnt + 1, cnt + 4);
if (max_cv <= n / 2)
return sum;

// 收集超员部门成员的改派损失
vector<int> loss;
for (int i = 1; i <= n; i++)
if (s[i].best == max_ci)
loss.push_back(s[i].a[s[i].best] - s[i].a[s[i].sub]);
sort(loss.begin(), loss.end());
for (int i = 0; i < max_cv - n / 2; i++)
sum -= loss[i];
return sum;
}

signed main()
{
ios::sync_with_stdio(false);
int t;
cin >> t;
while (t--)
cout << solve() << '\n';
return 0;
}

两种写法的复杂度相同,均为 O(nlogn)O(n \log n)pair 排序写法利用 sort 同时确定最优与次优部门,代码更紧凑;显式比较写法不依赖排序,但需要处理比较逻辑。本题每人只有 33 个元素,排序常数极小,两种写法的实际运行时间差异可忽略。我觉得用 pair 排序的写法更清晰,因为它把「选最优」和「找次优」统一到了一次排序中。


本站由 zaochen 使用 Stellar 1.33.1 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
全站访问量 - 次 · 访客数 - 人 · 本页面浏览 -