[week9]东东学打牌

题意

最近,东东沉迷于打牌。所以他找到 HRZ、ZJM 等人和他一起打牌。由于人数众多,东东稍微修改了亿下游戏规则:

所有扑克牌只按数字来算大小,忽略花色。
每张扑克牌的大小由一个值表示。A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K 分别指代 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13。
每个玩家抽得 5 张扑克牌,组成一手牌!(每种扑克牌的张数是无限的,你不用担心,东东家里有无数副扑克牌)
理所当然地,一手牌是有不同类型,并且有大小之分的。

举个栗子,现在东东的 “一手牌”(记为 α),瑞神的 “一手牌”(记为 β),要么 α > β,要么 α < β,要么 α = β。

那么这两个 “一手牌”,如何进行比较大小呢?首先对于不同类型的一手牌,其值的大小即下面的标号;对于同类型的一手牌,根据组成这手牌的 5 张牌不同,其值不同。下面依次列举了这手牌的形成规则:

大牌:这手牌不符合下面任一个形成规则。如果 α 和 β 都是大牌,那么定义它们的大小为组成这手牌的 5 张牌的大小总和。

对子:5 张牌中有 2 张牌的值相等。如果 α 和 β 都是对子,比较这个 “对子” 的大小,如果 α 和 β 的 “对子” 大小相等,那么比较剩下 3 张牌的总和。

两对:5 张牌中有两个不同的对子。如果 α 和 β 都是两对,先比较双方较大的那个对子,如果相等,再比较双方较小的那个对子,如果还相等,只能比较 5 张牌中的最后那张牌组不成对子的牌。

三个:5 张牌中有 3 张牌的值相等。如果 α 和 β 都是 “三个”,比较这个 “三个” 的大小,如果 α 和 β 的 “三个” 大小相等,那么比较剩下 2 张牌的总和。

三带二:5 张牌中有 3 张牌的值相等,另外 2 张牌值也相等。如果 α 和 β 都是 “三带二”,先比较它们的 “三个” 的大小,如果相等,再比较 “对子” 的大小。

炸弹:5 张牌中有 4 张牌的值相等。如果 α 和 β 都是 “炸弹”,比较 “炸弹” 的大小,如果相等,比较剩下那张牌的大小。

顺子:5 张牌中形成 x, x+1, x+2, x+3, x+4。如果 α 和 β 都是 “顺子”,直接比较两个顺子的最大值。

龙顺:5 张牌分别为 10、J、Q、K、A。

作为一个称职的魔法师,东东得知了全场人手里 5 张牌的情况。他现在要输出一个排行榜。排行榜按照选手们的 “一手牌” 大小进行排序,如果两个选手的牌相等,那么人名字典序小的排在前面。

不料,此时一束宇宙射线扫过,为了躲避宇宙射线,东东慌乱中清空了他脑中的 Cache。请你告诉东东,全场人的排名

Input

输入包含多组数据。每组输入开头一个整数 n (1 <= n <= 1e5),表明全场共多少人。
随后是 n 行,每行一个字符串 s1 和 s2 (1 <= |s1|,|s2| <= 10), s1 是对应人的名字,s2 是他手里的牌情况。

Output

对于每组测试数据,输出 n 行,即这次全场人的排名。

输入样例

3
DongDong AAA109
ZJM 678910
Hrz 678910

输出样例

Hrz
ZJM
DongDong

提示


分析

重点是读小作文题目,以及根据题目给出约束条件建立解题构架。

  • 题目分析

根据题目可以得到:

  • 牌面大小规定
  • 牌数
  • 牌型规定
  • 牌型比较方法
  • 玩家排序方法

总结题目需要实现的目标,就是分析所有玩家手牌,根据手牌输出玩家排序。

到这里,读完题并完全理解后,我们知道我们需要做的事情是:

  • 获得玩家名称
  • 获得玩家手牌
  • 整理手牌
  • 比较手牌
  • 根据比较结果给玩家排序
  • 输出玩家排序

  • 代码实现和分析

那么接下来就根据需要完成的目标来逐步建立解题结构:

1. 玩家类型

根据目标可以发现,我们需要获取一个玩家的名称、手牌。因此在自定义的玩家结构体中首先自然是要包含这两个基本数据域。

但是,什么样的玩家结构才能让之后的比较和排序更加容易呢?

首先作为基本的比较条件,我们需要记录玩家的手牌牌型。其次,在每个相同牌型的比较中,还需要通过一些其他特殊牌面大小来完成,例如对子值、三个值、炸弹值、顺子最大值…

根据总结可以发现,除去两对需要比较三次之外,所有的相同牌型比较都最多需要两个值。因此根据这个特点,我们可以在结构体中加入三个数据域来记录这些特殊值,方便快速比较排序。(在代码中我是用的pair加一个int)

2. 整理手牌

当确认了所有玩家和他们的手牌后,第一步自然是整理手牌,确认它们的牌型以及特殊值,才能进行下一步的比较和排序。

1)字符转换为大小
因为输入的名称和手牌都是字符串类型。因此要确保手牌字符串中的每个字符都能转换为正确的int值。所以可以用map建立一个char到int的映射。

但是有个问题,数字10是两位数,拥有两个字符,该如何转换?我处理的方式很简单,因为每次遍历最先扫描到的一定是“1”,而所有出现的面值中除了“10”外所有面值没出现“1”。因此可以用“1”唯一标识“10”,建立“1”到10的映射即可。

那么在遍历中,分为出现“10”和不出现两种情况,在前者中每当扫描到“0”时直接忽略即可。不出现的情况则代表着手牌字符串的长度一定为5,因为除去“10”所有面值都只有一个字符。

2)整理出现的面值及其个数
为了方便判断牌型,我们需要知道出现了多少面值,以及每个面值出现了多少次。

所以在转换过程中用一个vecor记录出现的所有面值,用一个int数组存储每个面值大小出现的次数即可。0即未出现。

首先判定是否出现龙顺,即判断是否出现5个面值且分别为10、J、Q、K、A。若判断成功,则记录牌型并直接返回。

在遍历前,先将出现的面值按升序排序,方便区分顺子和大牌。

若不是龙顺,再开始依次遍历所有出现的面值,根据当前面值出现的次数以及之前玩家记录的牌型来判断出当前的新牌型。如,若存在一个面值出现3次,且该玩家已经记录为对子,说明该玩家更新为了三带二类型。

如果面值数量为5,若任意两个相邻面值之间的差大于1,就说明该手牌一定不是顺子,而是大牌。因此,当遍历结束后,牌型仍然一次未更新时,就代表着这个牌型一定是顺子。

在遍历过程中,当玩家牌型达到其所及的最高牌型时,即代表其最终牌型已确定,可直接记录对应特殊值并返回。这个意思就是,当一个牌型为对子或三个时,其仍可能升级为两对、三带二,但是诸如炸弹、三带二一旦出现就一定是最终牌型,不会再改变。

由于我们其实不太关心这些面值具体是多少,只用知道其特征来判断牌型即可。所以在判断牌型的过程中,我们就可以将对子、三个、炸弹等信息直接存入到结构体中。除此之外,我们可以利用有序的面值序列来存储面值和或是剩余牌面值等特殊信息。比如,对子中剩余牌的面值大小就一定为出现的所有面值之和减去已记录的两个对子面值。

3. 比较牌型

首先可明确的是,牌型越大的玩家排名一定越高。而牌型相同的玩家再根据其所属牌型的对应比较方法进行降序排列。

所以我们可以用一个8列的不规则数组来记录对应属于每个牌型的所有玩家。则大体的输出顺序一定是从牌型8的玩家到1的玩家。

显然,最简单的办法就是遍历到某牌型时调用其对应的专属比较函数对当前该类型中所有玩家进行排序,最后依次输出即可。

而每个牌型根据其比较方式,再加上名字字典序的比较,编写对应的自定义比较函数即可,利用sort就能实现对这些玩家的快速排序了。


  • 问题

其实这个题也并不难,虽然我的代码有点点长。但是在写完之后基本就知道自己一定会对。不过我前两次并没有ac!【本猪当时是震惊的】

返回去看,发现是一个很小的粗心点没有纠正,扼杀了我的第二次一次ac体验:

  • 存储特殊面值

由于我的玩家结构体中采用的是pair来记录特殊面值,因此在不同牌型中,哪个类型的面值放在first,哪个放在second,容易出错并且混淆。尤其是之后又有大量的自定义函数,其中包含了大量引用比较和判断。在第一遍没有细心或是写完没有检查的话,容易出错。

我出错的地方就是对子这个牌型。由于我设置所有第一次出现的对子都放在第二位,方便出现三带二时为三个的面值让路。因此只有一个对子的情况中,也仍然是pair的第二位存放的对子,而剩余三个值的和在第一位。这一个小点就连续出错在了判断、记录和值、比较函数三个地方。导致我失去了一次ac的机会【555】

  • 包含10

除此之外,在粗略的检查中我也发现了一个最开始设计有问题的地方:也就是若手牌含有10的遍历转换中,最开始我规定的是该情况下遍历长度为6,也就是只增加了一个0的位置,但是在我自己设计数据调试的时候就猛然醒悟,显然可能存在多个10,所以就改为了遍历字符串长度。于是我突然现在写着也醒悟了,我完全不用分开情况,只用遇到0就跳过就可以了🙂【…】


总结

  1. 差一点又可以一次ac😢但是这也给了我教训,如果是csp赛制,仍然是这样不够细心,又对自己的代码比较有信心的情况下,一次提交很可能有着细小的错误导致最后wa。其实这种情况很容易发生在第一二题,上次csp的第一题我就是这样的问题。还是要吸取教训,学会耐心检查

代码

//
//  main.cpp
//  lab2
//
//

#include <iostream>
#include <map>
#include <string>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;

map<char,int> value;             //牌面大小
vector<int> v;                  //记录一副牌中出现的所有大小
int same1[14];                  //记录每一个大小在一副牌中出现的次数

struct player
{
    string name;            //姓名
    string card;            //牌
    int types = 0;          //牌型
    pair<int, int> special = {0,0};         //特殊牌型的牌面
    //三带二,三在前;对子,和在前,对子在后;炸弹在前,剩余牌在后;大牌,和在前;三个在前,和在后
    int left = 0;
};

vector<player> order[9];                //记录每一种类型下的玩家

void Sort(player& thePlayer)
{
    int now = 0;
    
    v.clear();              //清零
    
    for( int i = 0 ; i < 14 ; i++ )
        same1[i] = 0;

    /*    if( thePlayer.card.size() == 5 )        //不含10
    {
        for( int i = 0 ; i < 5 ; i++ )
        {
            now = value[thePlayer.card[i]];
            
            //记录所有出现的数,以及次数
            
            if( same1[now] == 0 )       //第一次出现的数放入数组中
                v.push_back(now);
            
                
            same1[now]++;               //其对应大小的出现次数+1
        }
        
    }
    else                                    //含10
    {*/
    //将牌转换为值
    //将两种情况合二为一
        for( int i = 0 ; i < thePlayer.card.size() ; i++ )
        {
            if( thePlayer.card[i] == '0' )
                continue;
            
            now = value[thePlayer.card[i]];
            
            if( same1[now] == 0  )
                v.push_back(now);
        
            same1[now]++;
        }
//    }
    
    //龙顺首先排除
    if( v.size() == 5 && same1[10] == 1 && same1[11] == 1 && same1[12] == 1 && same1[13] == 1 && same1[1] == 1 )
    {
        thePlayer.types = 8;
        return;
    }
    
    sort(v.begin(), v.end());     //升序排序
    
    for( int i = 0 ; i < v.size() ; i++ )      //遍历牌型
    {
        if( same1[v[i]] == 2 )      //若当前数字出现两次
        {
            if( thePlayer.types == 0 )      //若当前没更新类型
            {
                thePlayer.types = 2;        //记为对子
                thePlayer.special.second = v[i];        //记录对子的值
            }
            else if( thePlayer.types == 2 )    //若已经为对子
            {
                thePlayer.types = 3;      //则更新为两队并返回,因为已经不会更高
                thePlayer.special.first = v[i];         //记录第二个对子
                thePlayer.left = accumulate(v.begin(), v.end(),0) - thePlayer.special.first - thePlayer.special.second;     //记录剩余牌
                return;
            }
            else if( thePlayer.types == 4 )    //若已经为三个
            {
                thePlayer.types = 5;                    //记录三个
                thePlayer.special.second = v[i];         //则更新为三带二并返回,因为已经不会更高
                return;
            }
        }
        else if( same1[v[i]] == 3 )     //若出现三次
        {
            thePlayer.special.first = v[i];         //记录三个
            
            if( thePlayer.types == 0 )      //若未更新则为三个
               thePlayer.types = 4;
           else if( thePlayer.types == 2 )      //若已经为对子则为三带二并返回
           {
               thePlayer.types = 5;
               return;
           }
        }
        else if( same1[v[i]] == 4 )         //若出现四次则为顺子并返回
        {
            thePlayer.special.first = v[i];     //记录炸弹
            
            if( i == 0 )                            //记录剩下的牌
                thePlayer.special.second = v[1];
            else
                thePlayer.special.second = v[0];
            
            thePlayer.types = 6;
            
            return;
        }
        
        if( v.size() == 5 )         //若一共有五个数
        {
            if( i > 0 && v[i] - v[i - 1] > 1  )  //只要出现任意两个数之间的差大于1,则为大牌
            {
                thePlayer.types = 1;
                thePlayer.special.first = accumulate(v.begin(), v.end(),0);     //记录大牌的牌面之和
                return;
            }
        }
    }
    
    if( thePlayer.types == 0 )          //若仍然未更新,则为顺子
    {
        thePlayer.types = 7;
        thePlayer.special.first = v[4];         //记录顺子的最大值
    }
    else if ( thePlayer.types == 2 )        //记录对子中剩下三张牌的和
        thePlayer.special.first = accumulate(v.begin(), v.end(),0) - thePlayer.special.second;
    else if ( thePlayer.types == 4 )        //记录三个中剩下两张牌的和
        thePlayer.special.second = accumulate(v.begin(), v.end(),0) - thePlayer.special.first;
    
}

bool cmp1(player& p1,player& p2)        //大牌
{
    if( p1.special.first != p2.special.first )      //首先根据和降序排列
        return p1.special.first > p2.special.first;
    return p1.name < p2.name;                       //其次根据名字字典序升序
}

bool cmp2(player& p1,player& p2)        //对子
{
    if( p1.special.second != p2.special.second )      //首先根据对子降序排列
        return p1.special.second > p2.special.second;
    else if( p1.special.first != p2.special.first )       //其次根据剩下三张牌的和
        return p1.special.first > p2.special.first;
    return p1.name < p2.name;                       //最后根据名字字典序
}

bool cmp3(player& p1,player& p2)        //两对
{   //首先比较大的对子,其次是小的对子,然后是最后的牌,最后是名字
    if( max(p1.special.first,p1.special.second) != max(p2.special.first,p2.special.second) )
        return max(p1.special.first,p1.special.second) > max(p2.special.first,p2.special.second);
    else if( min(p1.special.first,p1.special.second) != min(p2.special.first,p2.special.second) )
        return min(p1.special.first,p1.special.second) > min(p2.special.first,p2.special.second);
    else if ( p1.left != p2.left )
        return p1.left > p2.left;
    return p1.name < p2.name;
}

bool cmp4(player& p1,player& p2)        //三个
{
    if( p1.special.first != p2.special.first )      //首先根据三个升序排列
        return p1.special.first > p2.special.first;
    else if( p1.special.second != p2.special.second )       //其次根据剩下两张牌的和
        return p1.special.second > p2.special.second;
    return p1.name < p2.name;                       //最后根据名字字典序
}

bool cmp5(player& p1,player& p2)        //三带二
{
    if( p1.special.first != p2.special.first )      //首先根据三个升序排列
        return p1.special.first > p2.special.first;
    else if( p1.special.second != p2.special.second )       //其次根据对子
        return p1.special.second > p2.special.second;
    return p1.name < p2.name;                       //最后根据名字字典序
}

bool cmp6(player& p1,player& p2)        //炸弹
{
    if( p1.special.first != p2.special.first )      //首先根据炸弹升序排列
        return p1.special.first > p2.special.first;
    else if( p1.special.second != p2.special.second )       //其次根据剩下牌
        return p1.special.second > p2.special.second;
    return p1.name < p2.name;                       //最后根据名字字典序
}

bool cmp7(player& p1,player& p2)        //顺子
{
    if( p1.special.first != p2.special.first )      //首先根据最大值排列
        return p1.special.first > p2.special.first;
    return p1.name < p2.name;                       //其次根据名字字典序
}

bool cmp8(player& p1,player& p2)        //龙顺
{
    return p1.name < p2.name;                       //根据名字字典序
}

int main()
{
    ios::sync_with_stdio(false);
    
    value['A'] = 1;
    value['2'] = 2;
    value['3'] = 3;
    value['4'] = 4;
    value['5'] = 5;
    value['6'] = 6;
    value['7'] = 7;
    value['8'] = 8;
    value['9'] = 9;
    value['1'] = 10;
    value['J'] = 11;
    value['Q'] = 12;
    value['K'] = 13;

    
    int n = 0;
    string s,s1;
    
    while( cin>>n )
    {
        for( int i = 0 ; i < 9 ; i++ )      //清空
            order[i].clear();
            
        player thePlayer[n];
        
        for( int i = 0 ; i < n ; i++ )
        {
            cin>>s>>s1;
            thePlayer[i].name = s;
            thePlayer[i].card = s1;
        }
        
        for( int i = 0 ; i < n ; i++ )
        {
            Sort(thePlayer[i]);         //整理牌型
            order[thePlayer[i].types].push_back(thePlayer[i]);      //将玩家放入到其对应类型中
        }
        
        for( int i = 8 ; i >= 1 ; i-- )     //从最大类型开始逐一遍历
        {
            if( order[i].empty() )      //若当前类型没有玩家则跳过
                continue;
            
            if( order[i].size() == 1 )      //若只有一个玩家,则直接输出并跳过
            {
                cout<<order[i][0].name<<endl;
                continue;
            }
            
            switch (i)      //否则对该类型的所有玩家进行排序
            {
                case 1:
                    sort(order[i].begin(), order[i].end(), cmp1);
                    break;
                case 2:
                    sort(order[i].begin(), order[i].end(), cmp2);
                    break;
                case 3:
                    sort(order[i].begin(), order[i].end(), cmp3);
                    break;
                case 4:
                    sort(order[i].begin(), order[i].end(), cmp4);
                    break;
                case 5:
                    sort(order[i].begin(), order[i].end(), cmp5);
                    break;
                case 6:
                    sort(order[i].begin(), order[i].end(), cmp6);
                    break;
                case 7:
                    sort(order[i].begin(), order[i].end(), cmp7);
                    break;
                case 8:
                    sort(order[i].begin(), order[i].end(), cmp8);
                    break;
                default:
                    break;
            }
            
            for( int j = 0 ; j < order[i].size() ; j++ )        //输出该类型所有玩家
                cout<<order[i][j].name<<endl;
            
        }
        
    }
    
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天翊藉君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值