[week2]模拟OJ成绩排名系统(简易版)

浪子不回头ぞ 提交于 2020-03-06 13:40:28

题意

【题面宛如小作文233】
程序设计思维作业和实验使用的实时评测系统,具有及时获得成绩排名的特点,那它的功能是怎么实现的呢?
我们千辛万苦怼完了不忍直视的程序并提交以后,评测系统要么返回AC,要么是返回各种其他的错误,不论是怎样的错法,它总会给你记上一笔,表明你曾经在这儿被坑过,而当你历经千辛终将它AC之后,它便会和你算笔总账,表明这题共错误提交了几次。
在岁月的长河中,你通过的题数虽然越来越多,但通过每题时你所共花去的时间(从最开始算起,直至通过题目时的这段时间)都会被记录下来,作为你曾经奋斗的痕迹。特别的,对于你通过的题目,你曾经的关于这题的每次错误提交都会被算上一定的单位时间罚时,这样一来,你在做出的题数上,可能领先别人很多,但是在做出同样题数的人中,你可能会因为罚时过高而处于排名上的劣势。
例如某次考试一共八道题(A,B,C,D,E,F,G,H),每个人做的题都在对应的题号下有个数量标记,负数表示该学生在该题上有过的错误提交次数但到现在还没有AC,正数表示AC所耗的时间,如果正数a跟上了一对括号,里面有个正数b,则表示该学生AC了这道题,耗去了时间a,同时曾经错误提交了b次。例子可见下方的样例输入与输出部分。

Input

输入数据包含多行,第一行是共有的题数n(1≤n≤12)以及单位罚时m(10≤m≤20),之后的每行数据描述一个学生的信息,首先是学生的用户名(不多于10个字符的字串)其次是所有n道题的得分现状,其描述采用问题描述中的数量标记的格式,见上面的表格。

Output

根据这些学生的得分现状,输出一个实时排名。实时排名显然先按AC题数的多少排,多的在前,再按时间分的多少排,少的在前,如果凑巧前两者都相等,则按名字的字典序排,小的在前。每个学生占一行,输出名字(10个字符宽),做出的题数(2个字符宽,右对齐)和时间分(4个字符宽,右对齐)。名字、题数和时间分相互之间有一个空格。数据保证可按要求的输出格式进行输出。

输入样例

在这里插入图片描述
【可复制⬇️】

8 20
GuGuDong 96 -3 40(3) 0 0 1 -8 0
hrz 107 67 -3 0 0 82 0 0
TT 120(3) 30 10(1) -3 0 47 21(2) -2
OMRailgun 0 -99 -8 0 -666 -10086 0 -9999996
yjq -2 37(2) 13 -1 0 113(2) 79(1) -1
Zjm 0 0 57(5) 0 0 99(3) -7 0

输出样例

在这里插入图片描述

分析

阅读完小作文之后,会发现这个题目的关键是分析输入流,而分析输入流首先就要考虑如何读取输入流。

  • 读取输入流

根据样例可知,程序需要将每一行数据中的单个数据进行分析,而每个数据之间由空格隔开,每个数据还可能会包含非数字字符。
接下来分别分析下我之前WA的做法和修改后AC的两种做法。

  1. 将整行数据的视作string,逐个字符读入并分析(WA
	string s;			//将一行数据视作string类型
//    char name[20];
    
    Student a;
           
    cin>>n>>fine;
    
    while( cin>>a.name )
    {
//        cout<<name<<" ^^ "<<endl;
        
//        a.name=name;
        a.information.first=n;      //完成题数
        a.information.second=0;     //总时间
		getline(cin,s);         //获取一行数据(所有分数信息)

        for( int i = 0 ; i < s.size() ; i++ )
        {
            if( s[i] >= '0' && s[i] <= '9' )    //若当前字符为数字,则进行转换
            {
                number=s[i]-'0'+number*10;	//将字符转换为数字
                
                if( number == 0 )       //若时间为0,完成数-1
                    a.information.first--;
            
            }
            else                            //若当前为字符
            {
                switch (s[i])
                {
                    case ' ':               //若为空格,则对之前的时间进行结算,并将number清零
                        time+=number;
                        number=0;
                        break;
                    case '(':               //若为左括号
                    {
                        time+=number;       //将括号前时间先加入总时间中,并将number清零
                        number=0;
                        i++;
                        
                        while( s[i] != ')' )    //将括号中的数字进行转换
                        {
                            number=s[i]-'0'+number*10;
                            i++;
                        }
                        
                        number*=fine;       //得到总罚时
            
                        break;
                    }
                    case '-':               //若为负号
                    {
                        a.information.first--;          //完成数-1
                        
                        while( s[i] != ' ' && i < s.size()-1 )    //跳过该数
                            i++;
                        break;
                    }
                    default:
                        break;
                }
            }
            
            if( i == s.size()-1 )
                time+=number;

这种做法有点效仿快读的意思,利用getline一次性读入一行数据到string中,将这一行中的数字和非数字以及空格都视作字符,遍历字符串对每个字符依次进行处理。

  • 若当前字符为数字字符,则转换为数字。若转换后为0,对应学生的完成题数-1。
    (从字符串中读取字符时总是先读取完整数字的高位,因此不论是读取到该数的第几位数字,转换后所得中间数都不会为0,除非该完整数为0)
  • 若当前字符为空格,将之前转换所得数纳入总时间
    (因为输入的一行数据中每个单项数据结束后一定是空格)
  • 若当前字符为左括号,先将之前转换所得数纳入总时间。接着继续遍历,将右括号前的所有数字字符进行转换。当遍历到右括号时,结束遍历,并将转换后数字乘以罚时,加入总时间数。之后直接从右括号开始向后遍历。
  • 若当前字符为负号,完成题数-1,并让下一次遍历直接从该负号所属的单项数据之后的空格开始。确保下次遍历开始的下标合法。因此若该项数据为整行数据中的最后一个数据,会直接结束遍历。
  • 确保遇到任何情况时,若当前遍历到字符串最后一位,都将当前未加入的转换数纳入到总时间中。

从逻辑上讲这种做法似乎没有问题,但是很明显这比较繁琐,是一个笨办法。尽管我反复调试多次,也仍然WA。说明这种笨办法在一些特殊情况下会出现错误(但是我到现在都不知道到底是什么情况😕)。不过这也反映了我对各种输入流处理操作的不熟悉,在长久调试无果的尴尬境况下,只有改变处理方式。

【小tip:若像我一样长时间调试无果,建议重新审视和调整思路和算法,否则可能会浪费很多时间也得不到分】

  • 将数字和非数字字符分开单个处理(AC

  • cin从缓冲区中读入数据,当遇到不可见字符时停止或过滤。
    利用cin的这个特性,可以一次从输入数据中读入一个int型数据(必须是有符号数,否则负数中的负号又需要单独处理)。样例中一行数据中除了带括号的单项数据,其余都可以作为int型,根据其正负性进行分别处理。

  • getchar()从缓冲区一次读入一个字符(非数字字符以及空格)。
    样例中一行数据中,数字之后是空格或者左括号。若数字之后为空格,则getchar()无法获取任何字符,下一次cin读入空格后的下一个数字。若getchar()取出左括号时,则用cin将其之后的数字读出,单独对其进行处理,最后还应用getchar取掉右括号。

【小tip:数据在输入后会被存在一个缓冲区内,因此读取输入流的操作都是对缓冲区进行操作,不需要从键盘输入获得。cin无法取出空格,而getchar()可以取出空格。但此处cin会自动过滤空格,因此不需要用getchar()取掉空格,下一次cin会直接读入空格之后的数据。】

格式化输出

这个输出搞了半天【手动再见】,在此总结一下。

  1. setw(int x)
    头文件:#include < iomanip >
    控制输出宽度,也就是输出一个数据时一共会输出x个字符的宽度。若字符长度小于x,则由空格补齐;若大于x,将字符完整输出。

【小tip:
1、setw(int x)默认右对齐!
2、只对其后紧接的一次输出有效。
比如:cout<<setw(3)<<a<<b<<endl; 其中只有a的输出受setw(3)影响

2.左对齐/右对齐

  • setiosflags()
    头文件:#include < iomanip >
    右对齐setiosflags(ios::right)、左对齐setiosflags(ios::left):输出数据在本域宽范围内向右/左对齐,一般与setw()一起使用。
    setiosflags()将某个输出格式标志置为 1,而最终的输出格式会以最后一次标识为1的格式为准。

【小tip:
1、若cout<<setiosflags(ios::left)<<setw(2)<<a<<setiosflags(ios::right)<<setw(3)<<b<<endl; 只使用一次,a和b将分别按左对齐和右对齐输出;但若反复使用该操作输出a和b,之后的a不会再按左对齐输出,而是按右对齐输出。
2、只对其后紧接的一次输出有效。

  • cout<<left
    左对齐,在宽度不足时将填充字符添加到右边,一般与setw()一起使用。

【小tip:
1、不会出现setiosflags()中无法重复使用的情况。
2、只对其后紧接的一次输出有效。
3、cout<<right 是c++输出流的默认格式。

总结

1.如果比较熟悉哪一种输入输出(c/c++),就要熟知该种输入输出的各种处理操作以及它们的区别,然后熟用。
【cin、cin.get()、getline()、cin.getline()、getchar()、gets()】
【cout各种格式化输出】
2. 长久调试仍然WA的情况需要及时改变思路或算法🤝

代码

//
//  main.cpp
//  lab2
//
#include <iostream>
#include <map>
#include <string>
#include <cstring>
#include <string.h>
#include <vector>
#include <algorithm>
#include <iomanip>
using namespace std;

struct Student
{
    pair<int, int> information;         //做题信息
    char name[20];                        //姓名
    
    Student(){}

    bool operator < (const Student &s) const      //重载<
   {
       if( information.first != s.information.first )           //首先看完成题数
           return information.first > s.information.first;
       
       if( information.second != s.information.second )         //其次看用时
           return information.second < s.information.second;
           
        return ( strcmp(name, s.name) < 0 );

    }
           
};

vector<Student> ranking;        //学生数组

int main()
{
    int n = 0,fine = 0,number = 0;
    string s;
//    char name[20];
    
    Student a;
           
    cin>>n>>fine;
    
    while( cin>>a.name )
    {
//        cout<<name<<" ^^ "<<endl;
        
//        a.name=name;
        a.information.first=n;      //完成题数
        a.information.second=0;     //总时间
      
        for( int i = 0 ; i < n ; i++ )      //进行n次计算(n道题)
        {
            cin>>number;            //输入数字
            
            if( number <= 0 )               //若小于0则代表未ac,等于0表示没有提交
            {
                a.information.first--;      //完成数-1
                continue;                   //直接进行下一次输入
            }
            
            a.information.second+=number;       //计算总时间
            
            if( getchar() == '(' )          //从输入中读取字符,若为左括号
            {
                cin>>number;            //再接着读入括号后的数字
                
                number*=fine;           //计算总罚时
                a.information.second+=number;       //讲罚时加入到总时间内
                
                getchar();          //取掉右括号
            }
        }
        
        ranking.push_back(a);       //将当前学生信息插入数组
//        cout<<a.information.first<<" ** "<<a.information.second<<endl;
    }
    
//        cout<<"----------"<<endl;
        
       sort(ranking.begin(), ranking.end());
       
       for( int i = 0 ; i < ranking.size() ; i++ )
       {
           cout<<left<<setw(10)<<ranking[i].name<<" ";
           //如果用setiosflags(ios::left)会在第二行数据输入的时候就失效,仍然变成右对齐,setw默认右对齐
           cout<<setiosflags(ios::right)<<setw(2)<<ranking[i].information.first<<" ";
           cout<<setw(4)<<ranking[i].information.second<<endl;
       }
    
    
    return 0;
}

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!