【题解】2025山东省赛The 15th Shandong CCPC Provincial Collegiate Programming Contest

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) 高效且正确,我们采用一种贪心策略:

  1. 排序员工:首先,我们将所有员工进行统一排序。排序规则为:
    • 主关键字:职级 a,升序排列(优先考虑职级低的员工)。
    • 次关键字:容忍度 b,降序排列(在同一职级中,优先考虑容忍度更高的员工,因为他们对高职级同事的限制更宽松)。
      这一步排序在主函数中完成一次即可,供所有 check(k) 调用使用。
  2. 按职级贪心选择
    我们从小到大逐个处理每个职级。对于当前正在处理的职级 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 个人选入项目,更新 sclsc

至于为什么一定要让t从大到小遍历,也就是为什么要选择最大的满足条件的t,是因为选择较小的t可能导致忽略当前层级就可以完成要求的情况。

  1. 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 就是最大的可选人数。

算法讲解

  1. 数据结构与预处理
    • 使用结构体 E 存储每个员工的职级 a、容忍度 b 和原始编号 id
    • 重载 E< 操作符,实现自定义排序逻辑(职级升序,同职级容忍度降序)。
    • solv 函数中读取所有员工数据,并进行一次全局排序。
    • 计算 maxcnt (单个职级中最大员工数) 作为二分下界的一个参考。
  2. 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,更新 sclsc
    • 指针 i 跳到下一个职级组的开头。
    • chk 函数最后返回 sc == k_val
  3. 二分查找
    • 代码中使用的二分模板是 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

未完待续。。。

### 2024年山东CCPC相关信息 #### 事时间安排 根据已知的信息,2024年的中国大学生程序设计竞CCPC)全国邀请将在山东举办一次重要事。具体日期未明确提及,但可以推测该事通常会在上半年举行[^1]。 #### 题目解析与技巧分享 对于2024 CCPC全国邀请山东)暨山东省大学生程序设计竞的部分题目进行了详细解析。以下是部分题目的特点总结: - **Problem A**: 这道题目可能涉及基础算法的应用,适合初学者练习逻辑思维能力。 - **Problem C (多彩的线段2)**: 此题需要注意二分查找中的右边界设定为 \(2 \times 10^{18}\),并建议在满足条件时提前返回结果以优化性能[^2]。 ```java // 示例代码片段展示如何实现二分法 public class BinarySearchExample { public static long binarySearch(long left, long right) { while (left <= right) { long mid = left + (right - left) / 2; if (check(mid)) { // 自定义 check 函数判断当前值是否符合条件 return mid; // 提前返回结果 } else if (...) { left = mid + 1; } else { right = mid - 1; } } return -1; // 如果无解则返回特定标志 } private static boolean check(long value) { // 实现具体的检查逻辑 return true; } } ``` - **其他题目**: 如 Problem F、I 和 K 的解答也提供了详细的分析方法,帮助参者更好地理解复杂数据结构算法的设计思路。 #### 历史对比与难度评估 通过回顾往届比情况可以看出,像2022年CCPC威海区的比虽然整体思路上并不算特别困难,但由于某些题目(如 Problem C 和 J)涉及到复杂的编码过程,因此对选手的实际编程能力和耐心提出了较高要求[^3]。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值