G - 考试排名 题解 by Aze

1. 要点

  • 结构体 struct 数组的应用
  • 排序 的应用
  • 多重条件排序
  • %-10s 格式化输出
  • 比较复杂的输入处理

2. 前置知识

  • 结构体数组
    • struct的声明,使用
    • 跟数组的区别
  • 冒泡排序(sort也行,我用的快排但思路不太好讲)

3.题目

C++编程考试使用的实时提交系统,具有即时获得成绩排名的特点。它的功能是怎么实现的呢?

我们做好了题目的解答,提交之后,要么“AC”,要么错误,不管怎样错法,总是给你记上一笔,表明你曾经有过一次错误提交,因而当你一旦提交该题“AC”后,就要与你算一算帐了,总共该题错误提交了几回。

虽然你在题数上,大步地跃上了一个台阶,但是在耗时上要摊上你共花去的时间。

特别是,曾经有过的错误提交,每次都要摊上一定的单位时间分。

这样一来,你在做出的题数上,可能领先别人很多,但是,在做出同样题数的人群中,你可能会在耗时上处于排名的劣势。

例如:

某次考试一共8题(A,B,C,D,E,F,G,H),每个人做的题都在对应的题号下有个数量标记,负数表示该学生在该题上有过的错误提交次数,但到现在还没有AC,正数表示AC所耗的时间,如果正数a跟上一对括号,里面有个整数b,那就表示该学生提交该题AC了,耗去了时间a,同时,曾经错误提交了b次,因此对于下述输入数据:

image-20211115223904305

若每次错误提交的罚分为20分,则其排名从高到低应该是这样的: Josephus 5 376 John 4 284 Alice 4 352 Smith 3 167 Bob 2 325 Bush 0 0

input:

8 20
Smith	  -1	-16	8	0	0	120	39	0
John	  116	-2	11	0	0	82	55(1)	0
Josephus  72(3)	126	10	-3	0	47	21(2)	-2
Bush	  0	-1	-8	0	0	0	0	0
Alice	  -2	67(2)	13	-1	0	133	79(1)	-1
Bob	  0	0	57(5)	0	0	168	-7	0

output:

Josephus    5  376
John        4  284
Alice       4  352
Smith       3  167
Bob         2  325
Bush        0    0

4.分析

输出什么结果?

每个人根据 AC题目数 和 时间 进行排名,输出排名列表

既然我们需要进行排序之后再输出,就必须设计一个数据结构来存储这些信息

并且每个人的 AC题目数 和 时间 都是要跟个人绑定的

所以,struct 是最优选择(大概)

struct student{
    char name[11]; // 题上说最长11个字符
    int AC = 0;
    int time = 0; // 直接初始化为0
}
student stu[100020]; // 创建含100020个结构体的数组

数据结构选好之后,开始考虑如何把数据存进去

第一行 n,m 没问题直接读,n 代表题数,m 代表罚时,也就是说,人数是不知道有多少的

所以 用 while(scanf()) 来读每一行

因为后面题数是跟 n 相关,得用 for 计次循环

结合一下,先用while读名字,再用for读成绩

每次循环读入一个人的成绩

while(scanf("%s",&stu[i].name) != EOF)
{
	for(int j = 0; j < n; j++)
    {
        
    }
    i++; // ++后下次循环 就读入 下一个人的成绩
}

接着来看怎么读成绩,有三种情况

  • 正数:一次作对,AC++
  • 负数:没做出来
  • 正数+括号:有做错的,罚时,AC++

那么time是怎么计算的呢?

抓一个分析分析:

Smith	  -1	-16	8	0	0	120	39	0
//做出来三道题,总时间为 120 + 39 + 8 = 167
//所以很明显,如果没做出来是不计入时间的 

既然如此,当读入一个负数时直接跳过循环,去读下一个数就好

int score;
scanf("%d",&score);
if(score < 0) continue;

然后是处理 正数+括号,因为scanf("%d")碰到非数字会停下,所以读入后可以接 getchar()判断后一个字符是空格还是左括号(,是括号就特殊处理,不然就直接继续循环

int score;
scanf("%d",&score);
if(score < 0) continue;
if(getchar() == '(') // 判断是否有左括号
{
    //.....罚时计算
}
gerchar(); // 吸收右括号

数据是这么读,然后将其处理一下再存入

int score;
scanf("%d",&score);
if(score <= 0) continue;
stu[i].time += score;
if(getchar() == '(') // 判断是否有左括号
{
    int failed;
    scanf("%d",&failed);
    stu[i].time += failed * m; // 加上罚时
}
gerchar(); // 吸收右括号
stu[i].AC++; // 如果能运行到这说明已经不是负数或者0,这题他整出来了

结合一下

int i = 0;
while(scanf("%s",&stu[i].name) != EOF)
{
	for(int j = 0; j < n; j++)
    {
        int score;
        scanf("%d",&score);
        if(score <= 0) continue;
        stu[i].time += score;
        if(getchar() == '(') // 判断是否有左括号
        {
            int failed;
            scanf("%d",&failed); 
            stu[i].time += failed * m; // 加上罚时
        }
        gerchar(); // 吸收右括号
        stu[i].AC++; // 如果能运行到这说明已经不是负数或者0,这题他整出来了
    }
    i++; // ++后进入下次循环 就读入 下一个人的成绩
}

好耶!数据已经完美存进来了,接下来是将其排序,输出

实时排名显然先按AC题数的多少排,多的在前,再按时间分的多少排,少的在前,如果凑巧前两者都相等,则按名字的字典序排,小的在前。

所以是按以下条件排

  • AC题数
    • 当AC题数相等时,按时间排(用时越少排名越高)
      • 当时间也相等时,按名字字典序排

众所周知,显而易见,懂得都懂

冒泡排序的判断条件长这样

if(a[j - 1] < a[j])
{
	//....
}

很好!那么怎样在括号里多塞几个判断条件 而且 还是递进的判断呢

用 && || ! 肯定 8 行,这都是同时判断

那咋办嘛,只能用函数了

bool check(student &a, student &b) // 从后往前
{
	if(a.AC < b.AC) return true; // 如果b的AC数大于a的AC数就将他们交换位置
    else if (a.AC == b.AC)
    {
        if(a.time > b.time) return true; // 同理交换
        else if(a.time == b.time)
        {
            if(strcmp(a.name,b.name) > 0) return true; // 判断字典序,strcmp自己网上搜搜
        }
    }
    return false; // 如果三个 返回true的都没执行,那说明肯定 a 该排 b 前面
}

在判断中替换一下

if(check(stu[j - 1], stu[j])) // 如果函数返回true说明b该排在a前面
{
	student temp = stu[j - 1]; // 交换
	stu[j - 1] = stu[j];
	stu[j] = temp;
}

排序就不多说了,直接套模板

i--; // 注意while循环结尾时i会多++,减去1之后才是总人数
for(int q = 0; q <= i; q++)
{
	for(int j = i; j > q; j--)
    {
        if(check(stu[j - 1], stu[j])) // 如果函数返回true说明b该排在a前面
        {
            student temp = stu[j - 1]; // 交换
            stu[j - 1] = stu[j];
            stu[j] = temp;
        }
    }
}

结束!排序完成,剩下一个输出

每个学生占一行,输出名字(10个字符宽),做出的题数(2个字符宽,右对齐)和时间分(4个字符宽,右对齐)。名字、题数和时间分相互之间有一个空格。

字符宽的话就 %2d 就行,左右对齐则可以加 - + 号,比如 %-10s 左对齐输出10个字符宽的名字

printf("%-10s %2d %4d\n",stu[i].name,stu[i].AC,stu[i].time);

讲完!

5. 完整代码

#include <stdio.h>
#include <string.h> // 用 strcmp 需要
const int MAX = 100020;
struct student {
	int AC = 0;
	char name[11];
	int time = 0;
};
student stu[MAX];
 
bool check(student& a, student& b) // 从后往前
{
	if (a.AC < b.AC) return true; // 如果b的AC数大于a的AC数就将他们交换位置
	else if (a.AC == b.AC)
	{
		if (a.time > b.time) return true; // 同理交换
		else if (a.time == b.time)
		{
			if (strcmp(a.name, b.name) > 0) return true; // 判断字典序,strcmp自己网上搜搜
		}
	}
	return false; // 如果三个 返回true的都没执行,那说明肯定 a 该排 b 前面
}
 
int main()
{
	int n, m;
	scanf("%d%d", &n, &m);
	int i = 0;
	while (scanf("%s", &stu[i].name) != EOF && stu[i].name[0] != '0')
	{
		for (int j = 0; j < n; j++)
		{
			int num;
			scanf("%d", &num);
 
			if (num <= 0)	 continue;
			else stu[i].time += num;
 
			if (getchar() == '(')    // 处理括号
			{
				scanf("%d", &num);
				stu[i].time += num * m;
				getchar();
			}
 
			stu[i].AC++;
		}
		i++;
	}	
	i--; // 注意while循环结尾时i会多++,减去1之后才是最后一个人
	for (int q = 0; q <= i; q++) 
	{
		for (int j = i ; j > q; j--)
		{
			if (check(stu[j - 1], stu[j])) // 如果函数返回true说明b该排在a前面
			{
				student temp = stu[j - 1]; // 交换
				stu[j - 1] = stu[j];
				stu[j] = temp;
			}
		}
	}
 
	for (int j = 0; j <= i; j++)   // 输出
	{
		printf("%-10s %2d %4d\n", stu[j].name, stu[j].AC, stu[j].time);
	}
	return 0;
}