The 15th Shandong CCPC Provincial Collegiate Programming Contest
A. Project Management
题意
- 有
n
n
n名员工,第
i
i
i 名员工的职级为
a
i
a_i
ai。现在需要选择尽量多
的人,限制条件为,如果选择了员工 i i i,则被选中的所有人
里至多只能有 b i b_i bi 个人职级大于 a i a_i ai。 - n n n ≤ 2 × 10 5 10^5 105。
思路
问题的核心是选择尽可能多的员工加入项目,同时满足每个被选员工的约束条件:项目中职级高于他的人数不能超过他个人的容忍度。
这是一个典型的“最大化某个值,同时满足一堆约束”的问题,通常可以考虑二分答案。我们二分最终能选入项目的人数,称之为 k
。然后,我们需要一个 check(k)
函数来判断是否真的能选出 k
个员工满足所有约束。
check(k)
函数的设计:
check(k)
函数的目标是确定是否存在一种选择 k
名员工的方案,使得所有约束都得到满足。为了使 check(k)
高效且正确,我们采用一种贪心策略:
- 排序员工:首先,我们将所有员工进行统一排序。排序规则为:
- 主关键字:职级
a
,升序排列(优先考虑职级低的员工)。 - 次关键字:容忍度
b
,降序排列(在同一职级中,优先考虑容忍度更高的员工,因为他们对高职级同事的限制更宽松)。
这一步排序在主函数中完成一次即可,供所有check(k)
调用使用。
- 主关键字:职级
- 按职级贪心选择:
我们从小到大逐个处理每个职级。对于当前正在处理的职级R
:- 设
lsc
为从所有职级严格低于R
的员工中已经选入项目的人数。 - 设
sc
为到目前为止(包括处理职级R
之前)总共选入项目的人数。 - 我们的目标是,从职级
R
的员工中选出一些人(称之为t
人),加入到项目中,使得总人数尽量接近(但不超过)我们二分的目标k
,并且这t
个人自身的约束得到满足。 - 关键约束:如果从职级
R
中选了t
个人,那么为了凑齐总共k
个人,还需要从更高职级(职级> R
)中选出h_nd = k - lsc - t
个人(如果h_nd < 0
,则取0,表示不需要更高职级的人了)。这t
个被选中的职级R
的员工,他们每个人都必须能够容忍这h_nd
个更高职级的同事。由于我们按容忍度b
降序排列了当前职级的员工,所以我们只需要检查这t
个人中b
值最小的那个人是否满足h_nd <= b_min
。 - 确定本职级可选人数
best_t
:我们尝试从当前职级R
中选t
个人(从可选的最大人数开始,即min(当前职级总人数, k - sc)
(逐渐减少t
)。对于每个尝试的t
,我们计算出对应的h_nd
,并找到这t
个人中b
值最小的那个(即第t
个员工,因为已经按b
降序排过)。如果h_nd
小于等于这个最小的b
值,说明选t
个人是可行的。我们选择最大的、满足此条件的t
作为best_t
。 - 将这
best_t
个人选入项目,更新sc
和lsc
。
- 设
至于为什么一定要让t从大到小遍历,也就是为什么要选择最大的满足条件的t,是因为选择较小的t可能导致忽略当前层级就可以完成要求的情况。
check(k)
返回值:如果在遍历完所有职级后,总共选出的人数sc
正好等于我们尝试的k
,则check(k)
返回true
,表示可以选择k
个人。否则返回false
。
二分答案主流程:
- 二分查找的范围是
[0, n]
(或一个更紧的下界如maxcnt-1
,其中maxcnt
是单个职级中最多的人数,因为至少选0个人是可以的)。 - 如果
check(mid)
为真,说明mid
个人可以被选,我们尝试选更多的人,所以更新答案并调整二分范围的下界 (l = mid
)。 - 如果
check(mid)
为假,说明选不了mid
个人,需要减少人数,调整二分范围的上界 (r = mid
)。 - 二分结束后得到的值
l
就是最大的可选人数。
算法讲解
- 数据结构与预处理:
- 使用结构体
E
存储每个员工的职级a
、容忍度b
和原始编号id
。 - 重载
E
的<
操作符,实现自定义排序逻辑(职级升序,同职级容忍度降序)。 - 在
solv
函数中读取所有员工数据,并进行一次全局排序。 - 计算
maxcnt
(单个职级中最大员工数) 作为二分下界的一个参考。
- 使用结构体
chk(k_val)
函数详解:- 该函数接收一个目标人数
k_val
。 - 初始化
sc = 0
(已选总数) 和lsc = 0
(已选的更低职级人数)。 - 使用指针
i
遍历全局排序后的员工列表es_g
,每次处理一个完整的职级组。 - 内部循环 (通过指针
j
) 将当前职级的所有员工收集到临时向量cre
。 - 对于
cre
(当前职级的员工,已按b
降序):- 计算
mcph
:当前职级最多可以选多少人,以使得总人数不超过k_val
。 - 从
t = mcph
向下循环到 1,尝试从cre
中选t
个人。 - 计算
h_nd = k_val - lsc - t
(需要从更高职级补充的人数)。 - 检查
h_nd <= cre[t-1].b
(这t
人中b
最小的那个能否容忍)。 - 如果满足,则
best_t = t
是当前职级能贡献的最佳人数,跳出内层循环。
- 计算
- 将选出的
best_t
个人的id
存入tsel_g
,更新sc
和lsc
。 - 指针
i
跳到下一个职级组的开头。 chk
函数最后返回sc == k_val
。
- 该函数接收一个目标人数
- 二分查找:
- 代码中使用的二分模板是
l = maxcnt - 1, r = n_g + 1; while (l != r - 1) ...
。 - 当
chk(mid)
为真时,l = mid
,表示mid
是一个潜在的解,尝试在[mid, r)
区间继续搜索。 - 当
chk(mid)
为假时,r = mid
,表示mid
太大,解在[l, mid)
区间。 - 循环结束时,
l
将是满足chk(l)
为真的最大值。
- 代码中使用的二分模板是
复杂度分析:
- 排序: O ( N log N ) O(N \log N) O(NlogN),其中 N N N 是员工总数。
chk(k)
函数:外层while
循环按职级分组,最多遍历N
个员工一次。内层确定best_t
的循环,其迭代次数是当前职级的人数。由于所有职级人数之和为N
,所以chk(k)
的总复杂度是 O ( N ) O(N) O(N)。- 二分答案:调用
chk(k)
函数 O ( log N ) O(\log N) O(logN) 次。 - 总时间复杂度: O ( N log N ) O(N \log N) O(NlogN)。
- 空间复杂度: O ( N ) O(N) O(N),主要用于存储员工信息。
#include <bits/stdc++.h> // 万能头
using namespace std;
// 全局变量
int n_g; // 当前测试用例的员工总数
vector<struct E> es_g; // 全局员工列表 (排序后)
vector<int> tsel_g; // chk函数使用的临时选择ID列表
vector<int> fsel_g; // 最终选择的员工ID列表
struct E {
int a, b, id; // a: 职级, b: 容忍度, id: 原始编号
// 重载 < 操作符用于排序
// 按职级 a 升序,若职级相同,则按容忍度 b 降序
bool operator<(const E& other) const {
if (a != other.a) {
return a < other.a;
}
return b > other.b; // 注意这里是 >,因为希望容忍度大的排前面
}
};
// chk函数:判断是否能选出 k_val 个人满足所有约束
bool chk(int k_val) {
if(k_val == 0) return true;
tsel_g.clear(); // 清空临时选择列表
int sc = 0; // selected count: 当前已选的总人数
int lsc = 0; // lower-rank selected count: 所有更低职级已选的人数
size_t i = 0; // 主遍历指针,指向 es_g 中当前处理的员工的起始位置
// 循环处理每个职级组,直到选够 k_val 人或遍历完所有员工
while (i < es_g.size() && sc < k_val) {
// 1. 收集当前职级的所有员工
vector<E> cre; // current rank employees: 存储当前职级的员工
size_t j = i; // 辅助指针,用于收集同职级员工
while (j < es_g.size() && es_g[j].a == es_g[i].a) {
cre.push_back(es_g[j]);
j++;
}
// 此处 cre 中的员工已按 b 降序(因为 es_g 是这样排序的)
// 2. 确定 best_t: 当前职级 cre 最多能选多少人
int best_t = 0; // 本职级最终确定选的人数
// 本职级最多还能贡献的人数,不能超过 (k_val - sc)
int mcph; // max can pick here
mcph = min((int)cre.size(), k_val - sc);
// 从“最多可选人数”往下尝试,找到第一个满足条件的 t
for (int t = mcph; t >= 1; --t) {
// 假设当前职级选 t 个人 (即 cre[0] 到 cre[t-1])
// 这 t 个人中,容忍度最低的是 cre[t-1].b
// 计算:如果当前职级贡献 t 个人,为了凑齐 k_val 人,还需要从更高职级选多少人
int h_nd = k_val - lsc - t; // higher needed
if (h_nd < 0) h_nd = 0; // 如果已经超了或够了,就不需要更高职级的人
// 判断约束:这 t 个人中 b 最小的 (cre[t-1]) 是否能容忍 h_nd 个更高职级的人
if (h_nd <= cre[t-1].b) {
best_t = t; // 记录当前职级可以选 t 个人
break; // 找到了最大的可行 t,退出循环
}
}
// 3. 将本职级选出的 best_t 个人加入选择列表,并更新计数器
for (int idx = 0; idx < best_t; ++idx) {
tsel_g.push_back(cre[idx].id);
}
sc += best_t;
lsc += best_t; // 为下一个职级组更新 lsc (之前所有职级选的人数)
i = j; // 更新主遍历指针到下一个职级组的起始位置
}
return sc == k_val; // 如果最终选的人数恰好是 k_val,则返回 true
}
// solv函数:处理单个测试用例
void solv() {
cin >> n_g; // 读取员工总数
es_g.assign(n_g, E()); // 初始化全局员工列表
vector<int> lcnt(n_g + 1, 0); //对每个等级人数进行计数, 为二分做准备
int maxcnt = 0; //记录单个等级最大人数
for (int i = 0; i < n_g; ++i) {
cin >> es_g[i].a >> es_g[i].b;
es_g[i].id = i + 1; // 记录原始编号 (1-based)
lcnt[es_g[i].a]++;
maxcnt = max(maxcnt, lcnt[es_g[i].a]);
}
sort(es_g.begin(), es_g.end()); // 排序
fsel_g.clear(); // 清空最终选择列表
// 二分查找最大的 l
int l = maxcnt - 1, r = n_g + 1;
while (l != r - 1) {
int mid = (r + l) / 2;
if (chk(mid)) {
fsel_g = tsel_g; // 更新选择方案
l = mid;
} else {
r = mid;
}
}
cout << l << "\n"; // 输出最大可选人数
for (int i = 0; i < l; ++i) { // 输出选择的员工ID
cout << fsel_g[i] << (i == l - 1 ? "" : " ");
}
cout << "\n";
}
int main() {
// 优化输入输出速度
ios_base::sync_with_stdio(false);cin.tie(NULL);
int tc; cin >> tc;//用例数量
while (tc--) {
solv(); // 处理每个测试用例
}
return 0;
}
B. Pinball
未完待续。。。