招聘系统模拟

66 阅读8分钟

招聘系统模拟

背景介绍

您正在为一家大公司设计一套自动化的招聘系统。系统需要根据多个部门的不同用人标准,从一批应聘者中筛选出最合适的人选。整个招聘过程分为“常规录取”和“补录”两个阶段,每个阶段都有明确的选人规则。您的任务是精确模拟这个过程,并返回最终的录用结果。

核心实体

  1. 部门 (Department):

    • 每个部门有一个唯一的编号(即其在输入数组中的索引)。

    • 招聘目标 (employTarget): 计划招聘的人数。

    • 分数门槛:

      • progmThd: 机考最低分要求。
      • techThd: 技面最低分要求。
  2. 应聘者 (Candidate):

    • 每个应聘者有一个唯一的编号(即其在输入数组中的索引)。

    • 能力值:

      • progmGrade: 机考分数。
      • techGrade: 技面分数。

招聘流程

整个招聘流程分为两个主要阶段:

第一阶段:常规轮询录取

此阶段的目标是让每个部门都尽量达到其招聘目标 employTarget

  • 轮询机制:

    • 系统会进行多轮招聘。
    • 在每一轮中,系统会按部门编号从小到大的顺序,让每个尚未招满的部门选择一位应聘者。
    • 如果某个部门在本轮中找不到符合要求的应聘者,则本轮该部门轮空。
    • 一轮结束后,如果还有部门未招满且还有候选人,则开始下一轮轮询。
  • 结束条件: 当所有部门都招满,或者所有剩余的应聘者都不符合任何一个未招满部门的要求时,常规录取阶段结束。

第二阶段:补录

在常规录取结束后,为了吸纳与已录用人才能力相当的候选人,每个部门有一次补录机会。

  • 补录顺序: 同样按部门编号从小到大的顺序进行。

  • 补录资格:

    • 只有那些能力值(即机考和技面分数均完全相同)与该部门在常规录取阶段最后一名录用者完全一样的候选人,才有资格进入该部门的补录环节。
  • 补录选择:

    • 从所有符合补录资格的候选人中,按照下文的【选人规则】挑选一人进行补录。
    • 注意: 一个部门在补录阶段最多只录用一人。即使成功补录了一人,该部门的招聘也彻底结束;如果找不到符合补录资格的人,该部门的招聘也同样结束。

通用【选人规则】

无论是常规录取还是补录,在从一批候选人中挑选最佳人选时,都遵循以下三级优先序规则:

  1. 门槛要求: 候选人的机考和技面分数必须同时大于或等于部门的分数门槛要求。
  2. 技面优先: 在满足门槛的候选人中,优先录取技面分数 (techGrade) 最高的。
  3. 机考次之: 如果技面分数相同,则录取机考分数 (progmGrade) 最高的。
  4. 编号保底: 如果机考和技面分数都相同,则录取应聘者编号最小的。

解答要求

  • 时间限制: 1000ms
  • 内存限制: 256MB

输入格式

  1. 第一个参数 deptDemands:

    • 二维数组,deptDemands[i] 代表部门 i 的要求 [employTarget, progmThd, techThd]
    • 1 <= deptDemands.length <= 10
  2. 第二个参数 candidates:

    • 二维数组,candidates[j] 代表应聘者 j 的分数 [progmGrade, techGrade]
    • 1 <= candidates.length <= 10000

输出格式

  • 一个列表,其元素也是列表。外层列表的索引对应部门编号,内层列表按录取先后顺序包含该部门录用的应聘者编号
  • 如果某部门没有录取任何人,则对应的内层列表为空 []

样例 1 解读

输入:

  • deptDemands: [[2, 130, 120], [1, 150, 150]]
  • candidates: [[150, 100], [160, 190], [150, 200], [200, 190], [160, 190], [160, 190]]

输出: [[2, 1, 4], [3]]

招聘流程详解:

阶段轮次/部门决策分析结果
常规录取第 1 轮
部门 0 (目标2)在所有未录用者中,满足门槛 [130, 120] 的有 {1, 2, 3, 4, 5}。其中技面分最高(200)的是应聘者 2部门0 录取 2。已录:[2]
部门 1 (目标1)在剩余未录用者中,满足门槛 [150, 150] 的有 {1, 3, 4, 5}。其中技面分最高(190)的有 {1, 3, 4, 5}。在他们之中,机考分最高(200)的是应聘者 3部门1 录取 3。已录:[3]
部门1招满,退出轮询
第 2 轮
部门 0 (目标2, 还差1)在剩余未录用者 {0, 1, 4, 5} 中,满足门槛 [130, 120] 的有 {1, 4, 5}。他们的技面分(190)和机考分(160)都相同。按规则,录取编号最小的应聘者 1部门0 录取 1。已录:[2, 1]
部门0招满,常规录取结束
补录阶段部门 0部门0常规录取的最后一人是 1 ([160, 190])。在剩余应聘者 {0, 4, 5} 中,能力值相同的有 {4, 5}。在 {4, 5} 中按选人规则,选编号最小的 4部门0 补录 4。最终录取:[2, 1, 4]
部门 1部门1常规录取的最后一人是 3 ([200, 190])。剩余应聘者 {0, 5} 中没有能力值相同的。部门1 补录失败。最终录取:[3]
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 模拟一个复杂的招聘策略系统.
 * 该系统包含常规轮询录取和特殊补录两个阶段,
 * 并遵循一个多层次的优先录取规则。
 */
public class RecruitmentSystem {

    /**
     * 内部类,用于表示一个应聘者.
     * 封装了应聘者的所有信息,包括其原始编号(ID)、分数和是否已被录用的状态。
     */
    private static class Candidate {
        int id;           // 原始编号 (即在输入数组中的索引)
        int progmGrade;   // 机考分数
        int techGrade;    // 技面分数
        boolean isHired;  // 状态:是否已被录用

        public Candidate(int id, int progmGrade, int techGrade) {
            this.id = id;
            this.progmGrade = progmGrade;
            this.techGrade = techGrade;
            this.isHired = false;
        }
    }

    /**
     * 内部类,用于表示一个招聘部门.
     * 封装了部门的需求、门槛以及已录用的人员列表。
     */
    private static class Department {
        int id;             // 部门编号 (即在输入数组中的索引)
        int employTarget;   // 招聘目标人数
        int progmThd;       // 机考最低分要求
        int techThd;        // 技面最低分要求
        List<Candidate> hiredCandidates; // 已录用人员列表 (按录取顺序)

        public Department(int id, int employTarget, int progmThd, int techThd) {
            this.id = id;
            this.employTarget = employTarget;
            this.progmThd = progmThd;
            this.techThd = techThd;
            this.hiredCandidates = new ArrayList<>();
        }
    }

    /**
     * 主方法,执行整个招聘流程.
     * @param deptDemands 部门需求数组
     * @param candidates  应聘者信息数组
     * @return 按部门编号升序排列的录取结果列表
     */
    public List<List<Integer>> processRecruitment(int[][] deptDemands, int[][] candidates) {

        // --- 准备阶段:将输入数组转换为面向对象的模型 ---

        // 创建所有应聘者对象
        List<Candidate> allCandidates = new ArrayList<>();
        for (int i = 0; i < candidates.length; i++) {
            allCandidates.add(new Candidate(i, candidates[i][0], candidates[i][1]));
        }

        // 创建所有部门对象
        List<Department> allDepartments = new ArrayList<>();
        for (int i = 0; i < deptDemands.length; i++) {
            allDepartments.add(new Department(i, deptDemands[i][0], deptDemands[i][1], deptDemands[i][2]));
        }

        // 定义核心的“选人规则”比较器
        // 1. 技面分数降序
        // 2. 机考分数降序
        // 3. 编号升序
        Comparator<Candidate> selectionComparator = (c1, c2) -> {
            if (c1.techGrade != c2.techGrade) {
                return Integer.compare(c2.techGrade, c1.techGrade);
            }
            if (c1.progmGrade != c2.progmGrade) {
                return Integer.compare(c2.progmGrade, c1.progmGrade);
            }
            return Integer.compare(c1.id, c2.id);
        };


        // --- 阶段一:常规录取 ---
        // 使用一个标志位来控制轮询是否继续。只要在一轮中有任何一个部门招到了人,就可能需要下一轮。
        boolean anyDeptHiredInRound = true;
        while (anyDeptHiredInRound) {
            anyDeptHiredInRound = false; // 每轮开始前,假设本轮不会有任何录取发生

            // 按部门编号从小到大轮询
            for (Department dept : allDepartments) {
                // 如果部门尚未招满
                if (dept.hiredCandidates.size() < dept.employTarget) {
                    
                    // 1. 筛选出所有符合该部门门槛且尚未被录用的应聘者
                    List<Candidate> eligibleCandidates = new ArrayList<>();
                    for (Candidate c : allCandidates) {
                        if (!c.isHired && c.progmGrade >= dept.progmThd && c.techGrade >= dept.techThd) {
                            eligibleCandidates.add(c);
                        }
                    }

                    // 2. 如果有符合条件的应聘者
                    if (!eligibleCandidates.isEmpty()) {
                        // 3. 按照选人规则排序,找到最优人选
                        eligibleCandidates.sort(selectionComparator);
                        Candidate bestCandidate = eligibleCandidates.get(0);

                        // 4. 录用最优人选
                        bestCandidate.isHired = true;
                        dept.hiredCandidates.add(bestCandidate);
                        
                        // 标记本轮有录取发生,需要继续轮询
                        anyDeptHiredInRound = true; 
                    }
                }
            }
        } // 常规录取循环结束


        // --- 阶段二:补录 ---
        // 常规录取结束后,按部门编号顺序进行一次性补录
        for (Department dept : allDepartments) {
            // 补录的前提是部门在常规阶段至少招到了一人
            if (dept.hiredCandidates.isEmpty()) {
                continue;
            }

            // 获取常规录取阶段最后一名被录用的人员
            Candidate lastHired = dept.hiredCandidates.get(dept.hiredCandidates.size() - 1);

            // 1. 筛选出所有与 "lastHired" 能力完全相同且尚未被录用的应聘者
            List<Candidate> supplementaryCandidates = new ArrayList<>();
            for (Candidate c : allCandidates) {
                if (!c.isHired && c.progmGrade == lastHired.progmGrade && c.techGrade == lastHired.techGrade) {
                    supplementaryCandidates.add(c);
                }
            }

            // 2. 如果有符合补录条件的应聘者
            if (!supplementaryCandidates.isEmpty()) {
                // 3. 同样按照选人规则排序,找到最优人选
                supplementaryCandidates.sort(selectionComparator);
                Candidate bestCandidate = supplementaryCandidates.get(0);

                // 4. 补录最优人选(最多一人)
                bestCandidate.isHired = true;
                dept.hiredCandidates.add(bestCandidate);
            }
        } // 补录结束


        // --- 阶段三:格式化输出结果 ---
        List<List<Integer>> result = new ArrayList<>();
        // 按部门原始顺序(0, 1, 2...)生成结果
        for (Department dept : allDepartments) {
            // 将部门录用列表中的 Candidate 对象转换为他们的 ID
            List<Integer> hiredIds = dept.hiredCandidates.stream()
                                         .map(c -> c.id)
                                         .collect(Collectors.toList());
            result.add(hiredIds);
        }

        return result;
    }
}