青训营X豆包MarsCode 技术训练营第一课 | 豆包MarsCode AI 刷题

107 阅读5分钟

青训营X豆包MarsCode 技术训练营第一课 | 豆包MarsCode AI 刷题

主题:统计班级中的说谎者

一、问题背景

在一个班级中,某学生会“说谎”的条件是:分数小于或等于他的人数超过分数比他高的人数。即,当一个学生认为自己是“多数派”时,却实际上违反逻辑地进行了判断。

问题的核心在于对分数分布进行统计,并根据给定条件判断哪些学生满足“说谎者”的定义。

二、题目分析

输入说明

  • 学生人数 NNN,范围为 1 ≤ NNN ≤ 100。
  • 分数数组 AAA 的每个元素 AiA_iAi​,范围为 0 ≤ AiA_iAi​ ≤ 100。

输出说明
输出班级中满足“说谎”条件的学生数量。

核心逻辑

  • 对于每个学生 iii:

    • 计算 分数小于或等于 AiA_iAi​ 的学生数量
    • 计算 分数大于 AiA_iAi​ 的学生数量
    • 比较两者,如果前者大于后者,则该学生是“说谎者”。

三、解题思路

核心:判断每个学生的分数与班级整体分数分布的关系,并找到满足条件的学生数量。步骤:

1. 统计分数的频率

由于题目中分数的范围是 0 到 100,用一个数组 frequency 来记录每个分数出现的次数,快速获得某个分数有多少人。

2. 构建前缀和数组

利用前缀和思想,构建一个数组 prefixSum,使得 prefixSum[i] 表示分数小于等于 iii 的学生数量。前缀和的构建可以大幅优化统计分数范围的操作。

3. 遍历判断

对于每个学生:

  • 计算分数小于等于 AiA_iAi​ 的学生人数 lessOrEqualCount,可以通过 prefixSum[A_i] 获得。
  • 计算分数比他高的学生人数 greaterCount,等于班级总人数减去 lessOrEqualCount
  • 如果 lessOrEqualCount > greaterCount,则这个学生会说谎。
4. 统计结果

将所有会说谎的学生人数累加,最终输出结果。

四、代码详解

以下是我的完整的 Java 实现代码:

    public static int solution(int[] A) {
        int n = A.length;
        int[] frequency = new int[101]; // 分数范围是 0 到 100
        int[] prefixSum = new int[101];
        
        // 统计每个分数的频率
        for (int score : A) {
            frequency[score]++;
        }
        
        // 构建前缀和数组
        prefixSum[0] = frequency[0];
        for (int i = 1; i <= 100; i++) {
            prefixSum[i] = prefixSum[i - 1] + frequency[i];
        }
        
        int liarCount = 0;
        
        // 判断每个学生是否满足说谎的条件
        for (int score : A) {
            int lessOrEqualCount = prefixSum[score];
            int greaterCount = n - lessOrEqualCount;
            if (lessOrEqualCount > greaterCount) {
                liarCount++;
            }
        }
        
        return liarCount;
    }

    public static void main(String[] args) {
        System.out.println(solution(new int[]{100, 100, 100}) == 3);
        System.out.println(solution(new int[]{2, 1, 3}) == 2);
        System.out.println(solution(new int[]{30, 1, 30, 30}) == 3);
        System.out.println(solution(new int[]{19, 27, 73, 55, 88}) == 3);
        System.out.println(solution(new int[]{19, 27, 73, 55, 88, 88, 2, 17, 22}) == 5);
    }
}

代码解读
  1. 统计频率

    • 通过遍历分数数组 AAA,统计每个分数 AiA_iAi​ 的出现次数,存入 frequency 数组。
  2. 构建前缀和

    • 利用累加的方式构建 prefixSum,使得可以快速查询分数范围内的学生人数。
  3. 逻辑判断

    • 对每个学生的分数,利用前缀和计算 lessOrEqualCountgreaterCount,判断是否满足说谎条件。
时间复杂度
  • 构建频率数组:O(N)
  • 构建前缀和数组:O(M),其中 MMM 为分数范围(101)。
  • 遍历判断:O(N)
  • 总时间复杂度:O(N + M),即 O(N)。
  • 空间复杂度:O(M),即 O(101)。

五、思路图解

以下是解题的可视化过程:

  1. 输入分数

    A=[19,27,73,55,88]A = [19, 27, 73, 55, 88]A=[19,27,73,55,88]

  2. 统计频率和前缀和

    • frequency:统计每个分数出现的次数。 frequency[19]=1, frequency[27]=1, …frequency[19] = 1, \ frequency[27] = 1, \ \ldotsfrequency[19]=1, frequency[27]=1, …
    • prefixSum:计算前缀和。 prefixSum[19]=1, prefixSum[27]=2, …prefixSum[19] = 1, \ prefixSum[27] = 2, \ \ldotsprefixSum[19]=1, prefixSum[27]=2, …
  3. 判断每个学生是否说谎

    • 对每个学生的分数,计算 lessOrEqualCountgreaterCount,并进行比较。

六、测试用例

image.png

七、一些值得记录的Debug

  1. 略分数范围导致算法复杂度过高 直接用嵌套循环来比较每个学生和其他所有学生的分数。这会导致时间复杂度 O(N2)O(N^2)O(N2),当 N=100N = 100N=100 时仍然可行,但对于更大的数据量会显著拖慢运行效率。

解决方法: 利用题目给出的分数范围 0≤Ai≤1000 \leq A_i \leq 1000≤Ai​≤100,预处理分数频率和前缀和数组,通过常数时间复杂度的查表操作快速完成统计。避免直接比较分数。

2.相等分数时没有正确处理 当有多个学生的分数相同时,错误地将“分数比他高的人数”计算为 0.

if (frequency[score] > greaterCount) { // 错误的比较 liarCount++; }

解决方法

考虑相等分数的学生人数(即 frequency[score])对统计的影响:

  • 如果 lessOrEqualCount>greaterCountlessOrEqualCount > greaterCountlessOrEqualCount>greaterCount,相等分数的学生均满足说谎条件。

八、总结

该题是以分数的统计和条件判断为核心,充分利用了数组和前缀和的高效性。 在这道题的解题过程中, 深刻体会到了移到算法题,竟然可以收获到之前不懂的知识点:

  1. 如何统计数据频率并高效分析分布。
  2. 前缀和的实际应用。
  3. 面对约束范围时的优化思路。