问题描述
在小C的班级里,有 N 个学生,每个学生的成绩是 A_i。小C发现了一件有趣的事:当且仅当某个学生的成绩小于或等于自己的有更多人时,这个学生会说谎。换句话说,如果分数小于等于他的学生数量大于比他分数高的学生数量,则他会说谎。
现在,小C想知道班里有多少个学生会说谎。
测试样例
样例1:
输入:
A = [100, 100, 100]
输出:3
样例2:
输入:
A = [2, 1, 3]
输出:2
样例3:
输入:
A = [30, 1, 30, 30]
输出:3
样例4:
输入:
A = [19, 27, 73, 55, 88]
输出:3
样例5:
输入:
A = [19, 27, 73, 55, 88, 88, 2, 17, 22]
输出:5
题目分析
目标:判断有多少个学生在班级里会说谎。
一个学生会说谎的条件是:当且仅当该学生的成绩 ≤ 自己的有更多人时,他才会说谎。
换句话说,若学生的成绩小于等于自己的人数大于比自己分数高的学生数量,那么他就会说谎。
思路
为了高效地判断哪些学生会说谎,我们可以通过以下几个步骤来实现:
- 统计每个分数的出现次数:
- 为了计算每个学生是否说谎,首先需要统计每个成绩出现的次数。可以利用一个计数数组
count,其中count[i]表示成绩为i的学生人数。
- 为了计算每个学生是否说谎,首先需要统计每个成绩出现的次数。可以利用一个计数数组
- 计算前缀和数组:
- 前缀和是为了快速统计某个学生的成绩小于或等于给定分数的学生数量。
prefixSum[i]存储了所有成绩小于等于i的学生数量- 即
prefixSum[i] = count[0] + count[1] + ... + count[i]。可以快速得出某个学生的成绩小于等于该学生成绩的人数。
- 判断学生是否说谎:
- 对于每个学生的成绩
A[i],可以通过prefixSum[A[i]]快速计算出成绩小于等于A[i]的学生数量lessOrEqual, - 总学生数减去
lessOrEqual即为比该学生分数高的学生数量greater。 - 如果
lessOrEqual大于greater,则该学生会说谎。
- 对于每个学生的成绩
- 遍历所有学生:
- 遍历所有学生,检查每个学生是否会说谎,并统计说谎的学生数量。
完整代码
public class Main {
public static int solution(int[] A) {
int N = A.length;
int[] count = new int[101]; // 统计每个分数的出现次数
int[] prefixSum = new int[101]; // 前缀和数组
// 统计每个分数的出现次数
for (int score : A) {
count[score]++;
}
// 计算前缀和
for (int i = 1; i <= 100; ++i) {
prefixSum[i] = prefixSum[i - 1] + count[i];
}
int liarCount = 0;
// 判断每个学生是否说谎
for (int score : A) {
int lessOrEqual = prefixSum[score];
int greater = N - lessOrEqual;
if (lessOrEqual > greater) {
liarCount++;
}
}
return liarCount;
}
public static void main(String[] args) {
// Add your test cases here
// 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);
}
}
具体做法
- 计算分数出现的次数:
count数组用于统计每个成绩出现的次数。例如,如果A = [30, 1, 30, 30],那么count[30] = 3,count[1] = 1。
int[] count = new int[101]; // 统计每个分数的出现次数
// 统计每个分数的出现次数
for (int score : A) {
count[score]++;
}
-
计算前缀和数组:
prefixSum[i]存储了成绩小于等于i的学生数量。例如,prefixSum[30]存储了成绩小于等于 30 的学生总数。- 通过累加
count数组来得到前缀和数组。prefixSum[i] = prefixSum[i - 1] + count[i]。
// 计算前缀和 for (int i = 1; i <= 100; ++i) { prefixSum[i] = prefixSum[i - 1] + count[i]; } -
判断是否说谎:
- 对于每个学生的成绩
score,通过prefixSum[score]获取小于等于该成绩的学生数量lessOrEqual。 - 通过
N - lessOrEqual计算比该成绩高的学生数量greater。 - 如果
lessOrEqual > greater,则该学生会说谎,增加说谎的学生数量。
int liarCount = 0; // 判断每个学生是否说谎 for (int score : A) { int lessOrEqual = prefixSum[score]; int greater = N - lessOrEqual; if (lessOrEqual > greater) { liarCount++; } } - 对于每个学生的成绩
-
返回结果:
- 最终返回
liarCount,即说谎的学生数量。
- 最终返回
知识点
Java方面
1. 前缀和(Prefix Sum)
前缀和是一种优化技术,通过对数组中元素的累加,能够在常数时间内计算出任意子数组的和。
在本题中,我们使用前缀和来计算“小于等于某个值的学生数量”。通过构造一个前缀和数组 prefixSum,我们可以在 O(1) 的时间复杂度内得到任何成绩 score 的前缀和,从而有效判断每个学生是否说谎。
前缀和的关键步骤:
- 通过遍历数组计算累加和。
- 使用前缀和数组可以快速回答某个区间的求和问题(在本题中是“成绩小于等于某个值的学生数量”)。
- 例子:本题中,计算前缀和的代码:
// 计算前缀和 for (int i = 1; i <= 100; ++i) { prefixSum[i] = prefixSum[i - 1] + count[i]; }
2. 计数数组(Counting Array)
计数数组 是用来统计某些特定数值出现次数的数组。
在本题中,我们使用 count 数组统计每个分数出现的次数。通过计数数组,我们能够快速知道某个分数出现了多少次,进而计算前缀和。步骤如下:
- 定义一个大小合适的数组(本题中是
count[101],因为成绩范围是 0 到 100)。 - 遍历输入数据并统计各个元素出现的次数。
3. Java 数组(Array)
- 数组初始化:
int[] array = new int[101];用来创建一个长度为 101 的整数数组。 - 数组遍历:可以通过
for循环 或 增强型for循环遍历数组,for (int i : array)是一种简洁的遍历数组的方式。
4. Java 中的 for 循环
- 传统的
for循环:在此问题中我们使用for (int i = 1; i <= 100; ++i)来遍历和构建前缀和数组。 - 增强型
for循环:如for (int score : A),可以简洁地遍历数组。
算法思想
- 贪心策略:
- 对于每个学生,我们通过比较“成绩小于等于该学生的学生数”和“成绩大于该学生的学生数”来判断该学生是否说谎。这一判断通过前缀和计算的方式得以优化。
- 预处理技术:
- 通过计数和前缀和的预处理,使得我们能够在
O(1)时间内查询某个成绩小于等于的学生数量,避免了暴力求解的O(N^2)时间复杂度。
- 通过计数和前缀和的预处理,使得我们能够在
- 优化问题的解决方法:
- 通过前缀和和计数数组相结合,优化了时间复杂度,使得算法在大数据规模下仍然能够高效执行。
感受
贵在总结和整理